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

Last change on this file since 8470 was 8460, checked in by Don-vip, 9 years ago

simplify usage of Way.firstNode()

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