source: josm/trunk/src/org/openstreetmap/josm/actions/JoinNodeWayAction.java@ 16770

Last change on this file since 16770 was 16202, checked in by GerdP, 4 years ago

fix #18990: Unable to Join Node To Way when way is the same one that the node belongs to

  • Property svn:eol-style set to native
File size: 11.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.io.Serializable;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.Comparator;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.LinkedHashMap;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19import java.util.Map.Entry;
20import java.util.Set;
21import java.util.TreeMap;
22
23import javax.swing.JOptionPane;
24
25import org.openstreetmap.josm.command.ChangeCommand;
26import org.openstreetmap.josm.command.Command;
27import org.openstreetmap.josm.command.MoveCommand;
28import org.openstreetmap.josm.command.SequenceCommand;
29import org.openstreetmap.josm.data.UndoRedoHandler;
30import org.openstreetmap.josm.data.coor.EastNorth;
31import org.openstreetmap.josm.data.osm.DataSet;
32import org.openstreetmap.josm.data.osm.Node;
33import org.openstreetmap.josm.data.osm.OsmPrimitive;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.data.osm.WaySegment;
36import org.openstreetmap.josm.data.projection.ProjectionRegistry;
37import org.openstreetmap.josm.gui.MainApplication;
38import org.openstreetmap.josm.gui.MapView;
39import org.openstreetmap.josm.gui.Notification;
40import org.openstreetmap.josm.tools.Geometry;
41import org.openstreetmap.josm.tools.MultiMap;
42import org.openstreetmap.josm.tools.Shortcut;
43
44/**
45 * Action allowing to join a node to a nearby way, operating on two modes:<ul>
46 * <li><b>Join Node to Way</b>: Include a node into the nearest way segments. The node does not move</li>
47 * <li><b>Move Node onto Way</b>: Move the node onto the nearest way segments and include it</li>
48 * </ul>
49 * @since 466
50 */
51public class JoinNodeWayAction extends JosmAction {
52
53 protected final boolean joinWayToNode;
54
55 protected JoinNodeWayAction(boolean joinWayToNode, String name, String iconName, String tooltip,
56 Shortcut shortcut, boolean registerInToolbar) {
57 super(name, iconName, tooltip, shortcut, registerInToolbar);
58 this.joinWayToNode = joinWayToNode;
59 }
60
61 /**
62 * Constructs a Join Node to Way action.
63 * @return the Join Node to Way action
64 */
65 public static JoinNodeWayAction createJoinNodeToWayAction() {
66 JoinNodeWayAction action = new JoinNodeWayAction(false,
67 tr("Join Node to Way"), /* ICON */ "joinnodeway",
68 tr("Include a node into the nearest way segments"),
69 Shortcut.registerShortcut("tools:joinnodeway", tr("Tool: {0}", tr("Join Node to Way")),
70 KeyEvent.VK_J, Shortcut.DIRECT), true);
71 action.setHelpId(ht("/Action/JoinNodeWay"));
72 return action;
73 }
74
75 /**
76 * Constructs a Move Node onto Way action.
77 * @return the Move Node onto Way action
78 */
79 public static JoinNodeWayAction createMoveNodeOntoWayAction() {
80 JoinNodeWayAction action = new JoinNodeWayAction(true,
81 tr("Move Node onto Way"), /* ICON*/ "movenodeontoway",
82 tr("Move the node onto the nearest way segments and include it"),
83 Shortcut.registerShortcut("tools:movenodeontoway", tr("Tool: {0}", tr("Move Node onto Way")),
84 KeyEvent.VK_N, Shortcut.DIRECT), true);
85 action.setHelpId(ht("/Action/MoveNodeWay"));
86 return action;
87 }
88
89 @Override
90 public void actionPerformed(ActionEvent e) {
91 if (!isEnabled())
92 return;
93 DataSet ds = getLayerManager().getEditDataSet();
94 Collection<Node> selectedNodes = ds.getSelectedNodes();
95 Collection<Command> cmds = new LinkedList<>();
96 Map<Way, MultiMap<Integer, Node>> data = new LinkedHashMap<>();
97
98 // If the user has selected some ways, only join the node to these.
99 boolean restrictToSelectedWays = !ds.getSelectedWays().isEmpty();
100
101 // Planning phase: decide where we'll insert the nodes and put it all in "data"
102 MapView mapView = MainApplication.getMap().mapView;
103 for (Node node : selectedNodes) {
104 List<WaySegment> wss = mapView.getNearestWaySegments(mapView.getPoint(node), OsmPrimitive::isSelectable);
105 // we cannot trust the order of elements in wss because it was calculated based on rounded position value of node
106 TreeMap<Double, List<WaySegment>> nearestMap = new TreeMap<>();
107 EastNorth en = node.getEastNorth();
108 for (WaySegment ws : wss) {
109 // Maybe cleaner to pass a "isSelected" predicate to getNearestWaySegments, but this is less invasive.
110 if (restrictToSelectedWays && !ws.way.isSelected()) {
111 continue;
112 }
113 /* perpendicular distance squared
114 * loose some precision to account for possible deviations in the calculation above
115 * e.g. if identical (A and B) come about reversed in another way, values may differ
116 * -- zero out least significant 32 dual digits of mantissa..
117 */
118 double distSq = en.distanceSq(Geometry.closestPointToSegment(ws.getFirstNode().getEastNorth(),
119 ws.getSecondNode().getEastNorth(), en));
120 // resolution in numbers with large exponent not needed here..
121 distSq = Double.longBitsToDouble(Double.doubleToLongBits(distSq) >> 32 << 32);
122 List<WaySegment> wslist = nearestMap.computeIfAbsent(distSq, k -> new LinkedList<>());
123 wslist.add(ws);
124 }
125 Set<Way> seenWays = new HashSet<>();
126 Double usedDist = null;
127 while (!nearestMap.isEmpty()) {
128 Entry<Double, List<WaySegment>> entry = nearestMap.pollFirstEntry();
129 if (usedDist != null) {
130 double delta = entry.getKey() - usedDist;
131 if (delta > 1e-4)
132 break;
133 }
134 for (WaySegment ws : entry.getValue()) {
135 // only use the closest WaySegment of each way and ignore those that already contain the node
136 if (!ws.getFirstNode().equals(node) && !ws.getSecondNode().equals(node)
137 && !seenWays.contains(ws.way)) {
138 if (usedDist == null)
139 usedDist = entry.getKey();
140 MultiMap<Integer, Node> innerMap = data.get(ws.way);
141 if (innerMap == null) {
142 innerMap = new MultiMap<>();
143 data.put(ws.way, innerMap);
144 }
145 innerMap.put(ws.lowerIndex, node);
146 seenWays.add(ws.way);
147 }
148 }
149 }
150 }
151
152 // Execute phase: traverse the structure "data" and finally put the nodes into place
153 Map<Node, EastNorth> movedNodes = new HashMap<>();
154 for (Map.Entry<Way, MultiMap<Integer, Node>> entry : data.entrySet()) {
155 final Way w = entry.getKey();
156 final MultiMap<Integer, Node> innerEntry = entry.getValue();
157
158 List<Integer> segmentIndexes = new LinkedList<>();
159 segmentIndexes.addAll(innerEntry.keySet());
160 segmentIndexes.sort(Collections.reverseOrder());
161
162 List<Node> wayNodes = w.getNodes();
163 for (Integer segmentIndex : segmentIndexes) {
164 final Set<Node> nodesInSegment = innerEntry.get(segmentIndex);
165 if (joinWayToNode) {
166 for (Node node : nodesInSegment) {
167 EastNorth newPosition = Geometry.closestPointToSegment(
168 w.getNode(segmentIndex).getEastNorth(),
169 w.getNode(segmentIndex+1).getEastNorth(),
170 node.getEastNorth());
171 EastNorth prevMove = movedNodes.get(node);
172 if (prevMove != null) {
173 if (!prevMove.equalsEpsilon(newPosition, 1e-4)) {
174 // very unlikely: node has same distance to multiple ways which are not nearly overlapping
175 new Notification(tr("Multiple target ways, no common point found. Nothing was changed."))
176 .setIcon(JOptionPane.INFORMATION_MESSAGE)
177 .show();
178 return;
179 }
180 continue;
181 }
182 MoveCommand c = new MoveCommand(node,
183 ProjectionRegistry.getProjection().eastNorth2latlon(newPosition));
184 cmds.add(c);
185 movedNodes.put(node, newPosition);
186 }
187 }
188 List<Node> nodesToAdd = new LinkedList<>();
189 nodesToAdd.addAll(nodesInSegment);
190 nodesToAdd.sort(new NodeDistanceToRefNodeComparator(
191 w.getNode(segmentIndex), w.getNode(segmentIndex+1), !joinWayToNode));
192 wayNodes.addAll(segmentIndex + 1, nodesToAdd);
193 }
194 Way wnew = new Way(w);
195 wnew.setNodes(wayNodes);
196 cmds.add(new ChangeCommand(ds, w, wnew));
197 }
198
199 if (cmds.isEmpty()) return;
200 UndoRedoHandler.getInstance().add(new SequenceCommand(getValue(NAME).toString(), cmds));
201 }
202
203 /**
204 * Sorts collinear nodes by their distance to a common reference node.
205 */
206 private static class NodeDistanceToRefNodeComparator implements Comparator<Node>, Serializable {
207
208 private static final long serialVersionUID = 1L;
209
210 private final EastNorth refPoint;
211 private final EastNorth refPoint2;
212 private final boolean projectToSegment;
213
214 NodeDistanceToRefNodeComparator(Node referenceNode, Node referenceNode2, boolean projectFirst) {
215 refPoint = referenceNode.getEastNorth();
216 refPoint2 = referenceNode2.getEastNorth();
217 projectToSegment = projectFirst;
218 }
219
220 @Override
221 public int compare(Node first, Node second) {
222 EastNorth firstPosition = first.getEastNorth();
223 EastNorth secondPosition = second.getEastNorth();
224
225 if (projectToSegment) {
226 firstPosition = Geometry.closestPointToSegment(refPoint, refPoint2, firstPosition);
227 secondPosition = Geometry.closestPointToSegment(refPoint, refPoint2, secondPosition);
228 }
229
230 double distanceFirst = firstPosition.distance(refPoint);
231 double distanceSecond = secondPosition.distance(refPoint);
232 return Double.compare(distanceFirst, distanceSecond);
233 }
234 }
235
236 @Override
237 protected void updateEnabledState() {
238 updateEnabledStateOnCurrentSelection();
239 }
240
241 @Override
242 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
243 updateEnabledStateOnModifiableSelection(selection);
244 }
245}
Note: See TracBrowser for help on using the repository browser.