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