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