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

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

fix #15538 - add robustness to AlignInLineAction

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