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