source: josm/trunk/src/org/openstreetmap/josm/actions/AlignInLineAction.java@ 12671

Last change on this file since 12671 was 12641, checked in by Don-vip, 7 years ago

see #15182 - deprecate Main.main.undoRedo. Replacement: gui.MainApplication.undoRedo

  • Property svn:eol-style set to native
File size: 16.6 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.util.ArrayList;
10import java.util.Collection;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.List;
14import java.util.Map;
15import java.util.Set;
16
17import javax.swing.JOptionPane;
18
19import org.openstreetmap.josm.command.Command;
20import org.openstreetmap.josm.command.MoveCommand;
21import org.openstreetmap.josm.command.SequenceCommand;
22import org.openstreetmap.josm.data.coor.EastNorth;
23import org.openstreetmap.josm.data.osm.DataSet;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Way;
27import org.openstreetmap.josm.gui.MainApplication;
28import org.openstreetmap.josm.gui.Notification;
29import org.openstreetmap.josm.tools.Logging;
30import org.openstreetmap.josm.tools.Shortcut;
31
32/**
33 * Aligns all selected nodes into a straight line (useful for roads that should be straight, but have side roads and
34 * therefore need multiple nodes)
35 *
36 * <pre>
37 * Case 1: 1 or 2 ways selected and no nodes selected: align nodes of ways taking care of intersection.
38 * Case 2: Single node selected and no ways selected: align this node relative to all referrer ways (2 at most).
39 * Case 3: Single node and ways selected: align this node relative to selected ways.
40 * Case 4.1: Only nodes selected, part of a non-closed way: align these nodes on the line passing through the
41 * extremity nodes (most distant in the way sequence). See https://josm.openstreetmap.de/ticket/9605#comment:3
42 * Case 4.2: Only nodes selected, part of a closed way: align these nodes on the line passing through the most distant nodes.
43 * Case 4.3: Only nodes selected, part of multiple ways: align these nodes on the line passing through the most distant nodes.
44 * </pre>
45 *
46 * @author Matthew Newton
47 */
48public final class AlignInLineAction extends JosmAction {
49
50 /**
51 * Constructs a new {@code AlignInLineAction}.
52 */
53 public AlignInLineAction() {
54 super(tr("Align Nodes in Line"), "alignline", tr("Move the selected nodes in to a line."),
55 Shortcut.registerShortcut("tools:alignline", tr("Tool: {0}", tr("Align Nodes in Line")), KeyEvent.VK_L, Shortcut.DIRECT), true);
56 putValue("help", ht("/Action/AlignInLine"));
57 }
58
59 /**
60 * InvalidSelection exception has to be raised when action can't be performed
61 */
62 static class InvalidSelection extends Exception {
63
64 /**
65 * Create an InvalidSelection exception with default message
66 */
67 InvalidSelection() {
68 super(tr("Please select at least three nodes."));
69 }
70
71 /**
72 * Create an InvalidSelection exception with specific message
73 * @param msg Message that will be displayed to the user
74 */
75 InvalidSelection(String msg) {
76 super(msg);
77 }
78 }
79
80 /**
81 * Return 2 nodes making up the line along which provided nodes must be aligned.
82 *
83 * @param nodes Nodes to be aligned.
84 * @return A array of two nodes.
85 * @throws IllegalArgumentException if nodes is empty
86 */
87 private static Node[] nodePairFurthestApart(List<Node> nodes) {
88 // Detect if selected nodes are on the same way.
89
90 // Get ways passing though all selected nodes.
91 Set<Way> waysRef = null;
92 for (Node n: nodes) {
93 Collection<Way> ref = n.getParentWays();
94 if (waysRef == null)
95 waysRef = new HashSet<>(ref);
96 else
97 waysRef.retainAll(ref);
98 }
99
100 if (waysRef == null) {
101 throw new IllegalArgumentException();
102 }
103
104 // Nodes belongs to multiple ways, return most distant nodes.
105 if (waysRef.size() != 1)
106 return nodeFurthestAppart(nodes);
107
108 // All nodes are part of the same way. See #9605.
109 Way way = waysRef.iterator().next();
110
111 if (way.isClosed()) {
112 // Align these nodes on the line passing through the most distant nodes.
113 return nodeFurthestAppart(nodes);
114 }
115
116 Node nodea = null;
117 Node nodeb = null;
118
119 // The way is open, align nodes on the line passing through the extremity nodes (most distant in the way
120 // sequence). See #9605#comment:3.
121 Set<Node> remainNodes = new HashSet<>(nodes);
122 for (Node n : way.getNodes()) {
123 if (!remainNodes.contains(n))
124 continue;
125 if (nodea == null)
126 nodea = n;
127 if (remainNodes.size() == 1) {
128 nodeb = remainNodes.iterator().next();
129 break;
130 }
131 remainNodes.remove(n);
132 }
133
134 return new Node[] {nodea, nodeb};
135 }
136
137 /**
138 * Return the two nodes the most distant from the provided list.
139 *
140 * @param nodes List of nodes to analyze.
141 * @return An array containing the two most distant nodes.
142 */
143 private static Node[] nodeFurthestAppart(List<Node> nodes) {
144 Node node1 = null, node2 = null;
145 double minSqDistance = 0;
146 int nb;
147
148 nb = nodes.size();
149 for (int i = 0; i < nb - 1; i++) {
150 Node n = nodes.get(i);
151 for (int j = i + 1; j < nb; j++) {
152 Node m = nodes.get(j);
153 double sqDist = n.getEastNorth().distanceSq(m.getEastNorth());
154 if (sqDist > minSqDistance) {
155 node1 = n;
156 node2 = m;
157 minSqDistance = sqDist;
158 }
159 }
160 }
161
162 return new Node[] {node1, node2};
163 }
164
165 /**
166 * Operation depends on the selected objects:
167 */
168 @Override
169 public void actionPerformed(ActionEvent e) {
170 if (!isEnabled())
171 return;
172
173 try {
174 MainApplication.undoRedo.add(buildCommand());
175 } catch (InvalidSelection except) {
176 Logging.debug(except);
177 new Notification(except.getMessage())
178 .setIcon(JOptionPane.INFORMATION_MESSAGE)
179 .show();
180 }
181 }
182
183 /**
184 * Builds "align in line" command depending on the selected objects.
185 * @return the resulting command to execute to perform action
186 * @throws InvalidSelection if a polygon is selected, or if a node is used by 3 or more ways
187 * @since 12562
188 */
189 public Command buildCommand() throws InvalidSelection {
190 DataSet ds = getLayerManager().getEditDataSet();
191 List<Node> selectedNodes = new ArrayList<>(ds.getSelectedNodes());
192 List<Way> selectedWays = new ArrayList<>(ds.getSelectedWays());
193 selectedWays.removeIf(OsmPrimitive::isIncomplete);
194
195 // Decide what to align based on selection:
196 if (selectedNodes.isEmpty() && !selectedWays.isEmpty()) {
197 // Only ways selected -> For each way align their nodes taking care of intersection
198 return alignMultiWay(selectedWays);
199 } else if (selectedNodes.size() == 1) {
200 // Only 1 node selected -> align this node relative to referers way
201 Node selectedNode = selectedNodes.get(0);
202 List<Way> involvedWays;
203 if (selectedWays.isEmpty())
204 // No selected way, all way containing this node are used
205 involvedWays = selectedNode.getParentWays();
206 else
207 // Selected way, use only these ways
208 involvedWays = selectedWays;
209 List<Line> lines = getInvolvedLines(selectedNode, involvedWays);
210 if (lines.size() > 2 || lines.isEmpty())
211 throw new InvalidSelection();
212 return alignSingleNode(selectedNodes.get(0), lines);
213 } else if (selectedNodes.size() >= 3) {
214 // More than 3 nodes and way(s) selected -> align selected nodes. Don't care of way(s).
215 return alignOnlyNodes(selectedNodes);
216 } else {
217 // All others cases are invalid
218 throw new InvalidSelection();
219 }
220 }
221
222 /**
223 * Align nodes in case 3 or more nodes are selected.
224 *
225 * @param nodes Nodes to be aligned.
226 * @return Command that perform action.
227 * @throws InvalidSelection If the nodes have same coordinates.
228 */
229 private static Command alignOnlyNodes(List<Node> nodes) throws InvalidSelection {
230 // Choose nodes used as anchor points for projection.
231 Node[] anchors = nodePairFurthestApart(nodes);
232 Collection<Command> cmds = new ArrayList<>(nodes.size());
233 Line line = new Line(anchors[0], anchors[1]);
234 for (Node node: nodes) {
235 if (node != anchors[0] && node != anchors[1])
236 cmds.add(line.projectionCommand(node));
237 }
238 return new SequenceCommand(tr("Align Nodes in Line"), cmds);
239 }
240
241 /**
242 * Align way in case of multiple way #6819
243 * @param ways Collection of way to align
244 * @return Command that perform action
245 * @throws InvalidSelection if a polygon is selected, or if a node is used by 3 or more ways
246 */
247 private static Command alignMultiWay(Collection<Way> ways) throws InvalidSelection {
248 // Collect all nodes and compute line equation
249 Set<Node> nodes = new HashSet<>();
250 Map<Way, Line> lines = new HashMap<>();
251 for (Way w: ways) {
252 if (w.isClosed())
253 throw new InvalidSelection(tr("Can not align a polygon. Abort."));
254 nodes.addAll(w.getNodes());
255 lines.put(w, new Line(w));
256 }
257 Collection<Command> cmds = new ArrayList<>(nodes.size());
258 List<Way> referers = new ArrayList<>(ways.size());
259 for (Node n: nodes) {
260 referers.clear();
261 for (OsmPrimitive o: n.getReferrers()) {
262 if (ways.contains(o))
263 referers.add((Way) o);
264 }
265 if (referers.size() == 1) {
266 Way way = referers.get(0);
267 if (way.isFirstLastNode(n)) continue;
268 cmds.add(lines.get(way).projectionCommand(n));
269 } else if (referers.size() == 2) {
270 Command cmd = lines.get(referers.get(0)).intersectionCommand(n, lines.get(referers.get(1)));
271 cmds.add(cmd);
272 } else
273 throw new InvalidSelection(tr("Intersection of three or more ways can not be solved. Abort."));
274 }
275 return new SequenceCommand(tr("Align Nodes in Line"), cmds);
276 }
277
278 /**
279 * Get lines useful to do alignment of a single node
280 * @param node Node to be aligned
281 * @param refWays Ways where useful lines will be searched
282 * @return List of useful lines
283 * @throws InvalidSelection if a node got more than 4 neighbours (self-crossing way)
284 */
285 private static List<Line> getInvolvedLines(Node node, List<Way> refWays) throws InvalidSelection {
286 List<Line> lines = new ArrayList<>();
287 List<Node> neighbors = new ArrayList<>();
288 for (Way way: refWays) {
289 List<Node> nodes = way.getNodes();
290 neighbors.clear();
291 for (int i = 1; i < nodes.size()-1; i++) {
292 if (nodes.get(i) == node) {
293 neighbors.add(nodes.get(i-1));
294 neighbors.add(nodes.get(i+1));
295 }
296 }
297 if (neighbors.isEmpty())
298 continue;
299 else if (neighbors.size() == 2)
300 // Non self crossing
301 lines.add(new Line(neighbors.get(0), neighbors.get(1)));
302 else if (neighbors.size() == 4) {
303 // Self crossing, have to make 2 lines with 4 neighbors
304 // see #9081 comment 6
305 EastNorth c = node.getEastNorth();
306 double[] angle = new double[4];
307 for (int i = 0; i < 4; i++) {
308 EastNorth p = neighbors.get(i).getEastNorth();
309 angle[i] = Math.atan2(p.north() - c.north(), p.east() - c.east());
310 }
311 double[] deltaAngle = new double[3];
312 for (int i = 0; i < 3; i++) {
313 deltaAngle[i] = angle[i+1] - angle[0];
314 if (deltaAngle[i] < 0)
315 deltaAngle[i] += 2*Math.PI;
316 }
317 int nb = 0;
318 if (deltaAngle[1] < deltaAngle[0]) nb++;
319 if (deltaAngle[2] < deltaAngle[0]) nb++;
320 if (nb == 1) {
321 // Align along [neighbors[0], neighbors[1]] and [neighbors[0], neighbors[2]]
322 lines.add(new Line(neighbors.get(0), neighbors.get(1)));
323 lines.add(new Line(neighbors.get(2), neighbors.get(3)));
324 } else {
325 // Align along [neighbors[0], neighbors[2]] and [neighbors[1], neighbors[3]]
326 lines.add(new Line(neighbors.get(0), neighbors.get(2)));
327 lines.add(new Line(neighbors.get(1), neighbors.get(3)));
328 }
329 } else
330 throw new InvalidSelection("cannot treat more than 4 neighbours, got "+neighbors.size());
331 }
332 return lines;
333 }
334
335 /**
336 * Align a single node relative to a set of lines #9081
337 * @param node Node to be aligned
338 * @param lines Lines to align node on
339 * @return Command that perform action
340 * @throws InvalidSelection if more than 2 lines
341 */
342 private static Command alignSingleNode(Node node, List<Line> lines) throws InvalidSelection {
343 if (lines.size() == 1)
344 return lines.get(0).projectionCommand(node);
345 else if (lines.size() == 2)
346 return lines.get(0).intersectionCommand(node, lines.get(1));
347 throw new InvalidSelection();
348 }
349
350 /**
351 * Class that represent a line
352 */
353 static class Line {
354
355 /**
356 * Line equation ax + by + c = 0
357 * Such as a^2 + b^2 = 1, ie (-b, a) is a unit vector of line
358 */
359 private double a, b, c;
360 /**
361 * (xM, yM) are coordinates of a point of the line
362 */
363 private double xM, yM;
364
365 /**
366 * Init a line by 2 nodes.
367 * @param first One point of the line
368 * @param last Other point of the line
369 * @throws InvalidSelection if nodes have same coordinates
370 */
371 Line(Node first, Node last) throws InvalidSelection {
372 xM = first.getEastNorth().getX();
373 yM = first.getEastNorth().getY();
374 double xB = last.getEastNorth().getX();
375 double yB = last.getEastNorth().getY();
376 a = yB - yM;
377 b = xM - xB;
378 double norm = Math.sqrt(a*a + b*b);
379 if (norm == 0)
380 throw new InvalidSelection("Nodes have same coordinates!");
381 a /= norm;
382 b /= norm;
383 c = -(a*xM + b*yM);
384 }
385
386 /**
387 * Init a line equation from a way.
388 * @param way Use extremity of this way to compute line equation
389 * @throws InvalidSelection if nodes have same coordinates
390 */
391 Line(Way way) throws InvalidSelection {
392 this(way.firstNode(), way.lastNode());
393 }
394
395 /**
396 * Orthogonal projection of a node N along this line.
397 * @param n Node to be projected
398 * @return The command that do the projection of this node
399 */
400 public Command projectionCommand(Node n) {
401 double s = (xM - n.getEastNorth().getX()) * a + (yM - n.getEastNorth().getY()) * b;
402 return new MoveCommand(n, a*s, b*s);
403 }
404
405 /**
406 * Intersection of two line.
407 * @param n Node to move to the intersection
408 * @param other Second line for intersection
409 * @return The command that move the node
410 * @throws InvalidSelection if two parallels ways found
411 */
412 public Command intersectionCommand(Node n, Line other) throws InvalidSelection {
413 double d = this.a * other.b - other.a * this.b;
414 if (Math.abs(d) < 10e-6)
415 // parallels lines
416 throw new InvalidSelection(tr("Two parallels ways found. Abort."));
417 double x = (this.b * other.c - other.b * this.c) / d;
418 double y = (other.a * this.c - this.a * other.c) / d;
419 return new MoveCommand(n, x - n.getEastNorth().getX(), y - n.getEastNorth().getY());
420 }
421 }
422
423 @Override
424 protected void updateEnabledState() {
425 DataSet ds = getLayerManager().getEditDataSet();
426 setEnabled(ds != null && !ds.selectionEmpty());
427 }
428
429 @Override
430 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
431 setEnabled(selection != null && !selection.isEmpty());
432 }
433}
Note: See TracBrowser for help on using the repository browser.