001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.io.Serializable; 010import java.util.Collection; 011import java.util.Collections; 012import java.util.Comparator; 013import java.util.HashMap; 014import java.util.LinkedList; 015import java.util.List; 016import java.util.Map; 017import java.util.Set; 018import java.util.SortedSet; 019import java.util.TreeSet; 020 021import org.openstreetmap.josm.command.ChangeCommand; 022import org.openstreetmap.josm.command.Command; 023import org.openstreetmap.josm.command.MoveCommand; 024import org.openstreetmap.josm.command.SequenceCommand; 025import org.openstreetmap.josm.data.UndoRedoHandler; 026import org.openstreetmap.josm.data.coor.EastNorth; 027import org.openstreetmap.josm.data.osm.DataSet; 028import org.openstreetmap.josm.data.osm.Node; 029import org.openstreetmap.josm.data.osm.OsmPrimitive; 030import org.openstreetmap.josm.data.osm.Way; 031import org.openstreetmap.josm.data.osm.WaySegment; 032import org.openstreetmap.josm.data.projection.ProjectionRegistry; 033import org.openstreetmap.josm.gui.MainApplication; 034import org.openstreetmap.josm.gui.MapView; 035import org.openstreetmap.josm.tools.Geometry; 036import org.openstreetmap.josm.tools.MultiMap; 037import org.openstreetmap.josm.tools.Shortcut; 038 039/** 040 * Action allowing to join a node to a nearby way, operating on two modes:<ul> 041 * <li><b>Join Node to Way</b>: Include a node into the nearest way segments. The node does not move</li> 042 * <li><b>Move Node onto Way</b>: Move the node onto the nearest way segments and include it</li> 043 * </ul> 044 * @since 466 045 */ 046public class JoinNodeWayAction extends JosmAction { 047 048 protected final boolean joinWayToNode; 049 050 protected JoinNodeWayAction(boolean joinWayToNode, String name, String iconName, String tooltip, 051 Shortcut shortcut, boolean registerInToolbar) { 052 super(name, iconName, tooltip, shortcut, registerInToolbar); 053 this.joinWayToNode = joinWayToNode; 054 } 055 056 /** 057 * Constructs a Join Node to Way action. 058 * @return the Join Node to Way action 059 */ 060 public static JoinNodeWayAction createJoinNodeToWayAction() { 061 JoinNodeWayAction action = new JoinNodeWayAction(false, 062 tr("Join Node to Way"), /* ICON */ "joinnodeway", 063 tr("Include a node into the nearest way segments"), 064 Shortcut.registerShortcut("tools:joinnodeway", tr("Tool: {0}", tr("Join Node to Way")), 065 KeyEvent.VK_J, Shortcut.DIRECT), true); 066 action.setHelpId(ht("/Action/JoinNodeWay")); 067 return action; 068 } 069 070 /** 071 * Constructs a Move Node onto Way action. 072 * @return the Move Node onto Way action 073 */ 074 public static JoinNodeWayAction createMoveNodeOntoWayAction() { 075 JoinNodeWayAction action = new JoinNodeWayAction(true, 076 tr("Move Node onto Way"), /* ICON*/ "movenodeontoway", 077 tr("Move the node onto the nearest way segments and include it"), 078 Shortcut.registerShortcut("tools:movenodeontoway", tr("Tool: {0}", tr("Move Node onto Way")), 079 KeyEvent.VK_N, Shortcut.DIRECT), true); 080 action.setHelpId(ht("/Action/MoveNodeWay")); 081 return action; 082 } 083 084 @Override 085 public void actionPerformed(ActionEvent e) { 086 if (!isEnabled()) 087 return; 088 DataSet ds = getLayerManager().getEditDataSet(); 089 Collection<Node> selectedNodes = ds.getSelectedNodes(); 090 Collection<Command> cmds = new LinkedList<>(); 091 Map<Way, MultiMap<Integer, Node>> data = new HashMap<>(); 092 093 // If the user has selected some ways, only join the node to these. 094 boolean restrictToSelectedWays = !ds.getSelectedWays().isEmpty(); 095 096 // Planning phase: decide where we'll insert the nodes and put it all in "data" 097 MapView mapView = MainApplication.getMap().mapView; 098 for (Node node : selectedNodes) { 099 List<WaySegment> wss = mapView.getNearestWaySegments(mapView.getPoint(node), OsmPrimitive::isSelectable); 100 MultiMap<Way, Integer> insertPoints = new MultiMap<>(); 101 for (WaySegment ws : wss) { 102 // Maybe cleaner to pass a "isSelected" predicate to getNearestWaySegments, but this is less invasive. 103 if (restrictToSelectedWays && !ws.way.isSelected()) { 104 continue; 105 } 106 107 if (!ws.getFirstNode().equals(node) && !ws.getSecondNode().equals(node)) { 108 insertPoints.put(ws.way, ws.lowerIndex); 109 } 110 } 111 for (Map.Entry<Way, Set<Integer>> entry : insertPoints.entrySet()) { 112 final Way w = entry.getKey(); 113 final Set<Integer> insertPointsForWay = entry.getValue(); 114 for (int i : pruneSuccs(insertPointsForWay)) { 115 MultiMap<Integer, Node> innerMap; 116 if (!data.containsKey(w)) { 117 innerMap = new MultiMap<>(); 118 } else { 119 innerMap = data.get(w); 120 } 121 innerMap.put(i, node); 122 data.put(w, innerMap); 123 } 124 } 125 } 126 127 // Execute phase: traverse the structure "data" and finally put the nodes into place 128 for (Map.Entry<Way, MultiMap<Integer, Node>> entry : data.entrySet()) { 129 final Way w = entry.getKey(); 130 final MultiMap<Integer, Node> innerEntry = entry.getValue(); 131 132 List<Integer> segmentIndexes = new LinkedList<>(); 133 segmentIndexes.addAll(innerEntry.keySet()); 134 segmentIndexes.sort(Collections.reverseOrder()); 135 136 List<Node> wayNodes = w.getNodes(); 137 for (Integer segmentIndex : segmentIndexes) { 138 final Set<Node> nodesInSegment = innerEntry.get(segmentIndex); 139 if (joinWayToNode) { 140 for (Node node : nodesInSegment) { 141 EastNorth newPosition = Geometry.closestPointToSegment( 142 w.getNode(segmentIndex).getEastNorth(), 143 w.getNode(segmentIndex+1).getEastNorth(), 144 node.getEastNorth()); 145 MoveCommand c = new MoveCommand( 146 node, ProjectionRegistry.getProjection().eastNorth2latlon(newPosition)); 147 // Avoid moving a given node several times at the same position in case of overlapping ways 148 if (!cmds.contains(c)) { 149 cmds.add(c); 150 } 151 } 152 } 153 List<Node> nodesToAdd = new LinkedList<>(); 154 nodesToAdd.addAll(nodesInSegment); 155 nodesToAdd.sort(new NodeDistanceToRefNodeComparator( 156 w.getNode(segmentIndex), w.getNode(segmentIndex+1), !joinWayToNode)); 157 wayNodes.addAll(segmentIndex + 1, nodesToAdd); 158 } 159 Way wnew = new Way(w); 160 wnew.setNodes(wayNodes); 161 cmds.add(new ChangeCommand(ds, w, wnew)); 162 } 163 164 if (cmds.isEmpty()) return; 165 UndoRedoHandler.getInstance().add(new SequenceCommand(getValue(NAME).toString(), cmds)); 166 } 167 168 private static SortedSet<Integer> pruneSuccs(Collection<Integer> is) { 169 SortedSet<Integer> is2 = new TreeSet<>(); 170 for (int i : is) { 171 if (!is2.contains(i - 1) && !is2.contains(i + 1)) { 172 is2.add(i); 173 } 174 } 175 return is2; 176 } 177 178 /** 179 * Sorts collinear nodes by their distance to a common reference node. 180 */ 181 private static class NodeDistanceToRefNodeComparator implements Comparator<Node>, Serializable { 182 183 private static final long serialVersionUID = 1L; 184 185 private final EastNorth refPoint; 186 private final EastNorth refPoint2; 187 private final boolean projectToSegment; 188 189 NodeDistanceToRefNodeComparator(Node referenceNode, Node referenceNode2, boolean projectFirst) { 190 refPoint = referenceNode.getEastNorth(); 191 refPoint2 = referenceNode2.getEastNorth(); 192 projectToSegment = projectFirst; 193 } 194 195 @Override 196 public int compare(Node first, Node second) { 197 EastNorth firstPosition = first.getEastNorth(); 198 EastNorth secondPosition = second.getEastNorth(); 199 200 if (projectToSegment) { 201 firstPosition = Geometry.closestPointToSegment(refPoint, refPoint2, firstPosition); 202 secondPosition = Geometry.closestPointToSegment(refPoint, refPoint2, secondPosition); 203 } 204 205 double distanceFirst = firstPosition.distance(refPoint); 206 double distanceSecond = secondPosition.distance(refPoint); 207 return Double.compare(distanceFirst, distanceSecond); 208 } 209 } 210 211 @Override 212 protected void updateEnabledState() { 213 updateEnabledStateOnCurrentSelection(); 214 } 215 216 @Override 217 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 218 updateEnabledStateOnModifiableSelection(selection); 219 } 220}