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