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