source: josm/trunk/src/org/openstreetmap/josm/actions/AlignInCircleAction.java@ 10413

Last change on this file since 10413 was 10383, checked in by Don-vip, 8 years ago

replace .get*Selected().isEmpty() by .selectionEmpty()

  • Property svn:eol-style set to native
File size: 15.9 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.Collections;
12import java.util.HashSet;
13import java.util.LinkedList;
14import java.util.List;
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.DataSet;
25import org.openstreetmap.josm.data.osm.Node;
26import org.openstreetmap.josm.data.osm.OsmPrimitive;
27import org.openstreetmap.josm.data.osm.Way;
28import org.openstreetmap.josm.gui.Notification;
29import org.openstreetmap.josm.tools.Geometry;
30import org.openstreetmap.josm.tools.Shortcut;
31
32/**
33 * Aligns all selected nodes within a circle. (Useful for roundabouts)
34 *
35 * @author Matthew Newton
36 * @author Petr Dlouhý
37 * @author Teemu Koskinen
38 * @author Alain Delplanque
39 *
40 * @since 146
41 */
42public final class AlignInCircleAction extends JosmAction {
43
44 /**
45 * Constructs a new {@code AlignInCircleAction}.
46 */
47 public AlignInCircleAction() {
48 super(tr("Align Nodes in Circle"), "aligncircle", tr("Move the selected nodes into a circle."),
49 Shortcut.registerShortcut("tools:aligncircle", tr("Tool: {0}", tr("Align Nodes in Circle")),
50 KeyEvent.VK_O, Shortcut.DIRECT), true);
51 putValue("help", ht("/Action/AlignInCircle"));
52 }
53
54 private static double distance(EastNorth n, EastNorth m) {
55 double easd, nord;
56 easd = n.east() - m.east();
57 nord = n.north() - m.north();
58 return Math.sqrt(easd * easd + nord * nord);
59 }
60
61 public static class PolarCoor {
62 private double radius;
63 private double angle;
64 private EastNorth origin = new EastNorth(0, 0);
65 private double azimuth;
66
67 PolarCoor(double radius, double angle) {
68 this(radius, angle, new EastNorth(0, 0), 0);
69 }
70
71 PolarCoor(double radius, double angle, EastNorth origin, double azimuth) {
72 this.radius = radius;
73 this.angle = angle;
74 this.origin = origin;
75 this.azimuth = azimuth;
76 }
77
78 PolarCoor(EastNorth en) {
79 this(en, new EastNorth(0, 0), 0);
80 }
81
82 PolarCoor(EastNorth en, EastNorth origin, double azimuth) {
83 radius = distance(en, origin);
84 angle = Math.atan2(en.north() - origin.north(), en.east() - origin.east());
85 this.origin = origin;
86 this.azimuth = azimuth;
87 }
88
89 public EastNorth toEastNorth() {
90 return new EastNorth(radius * Math.cos(angle - azimuth) + origin.east(), radius * Math.sin(angle - azimuth)
91 + origin.north());
92 }
93
94 /**
95 * Create a MoveCommand to move a node to this PolarCoor.
96 * @param n Node to move
97 * @return new MoveCommand
98 */
99 public MoveCommand createMoveCommand(Node n) {
100 EastNorth en = toEastNorth();
101 return new MoveCommand(n, en.east() - n.getEastNorth().east(), en.north() - n.getEastNorth().north());
102 }
103 }
104
105
106 /**
107 * Perform AlignInCircle action.
108 *
109 * A fixed node is a node for which it is forbidden to change the angle relative to center of the circle.
110 * All other nodes are uniformly distributed.
111 *
112 * Case 1: One unclosed way.
113 * --> allow action, and align selected way nodes
114 * If nodes contained by this way are selected, there are fix.
115 * If nodes outside from the way are selected there are ignored.
116 *
117 * Case 2: One or more ways are selected and can be joined into a polygon
118 * --> allow action, and align selected ways nodes
119 * If 1 node outside of way is selected, it became center
120 * If 1 node outside and 1 node inside are selected there define center and radius
121 * If no outside node and 2 inside nodes are selected those 2 nodes define diameter
122 * In all other cases outside nodes are ignored
123 * In all cases, selected nodes are fix, nodes with more than one referrers are fix
124 * (first referrer is the selected way)
125 *
126 * Case 3: Only nodes are selected
127 * --> Align these nodes, all are fix
128 */
129 @Override
130 public void actionPerformed(ActionEvent e) {
131 if (!isEnabled())
132 return;
133
134 Collection<OsmPrimitive> sel = getLayerManager().getEditDataSet().getSelected();
135 List<Node> nodes = new LinkedList<>();
136 // fixNodes: All nodes for which the angle relative to center should not be modified
137 Set<Node> fixNodes = new HashSet<>();
138 List<Way> ways = new LinkedList<>();
139 EastNorth center = null;
140 double radius = 0;
141
142 for (OsmPrimitive osm : sel) {
143 if (osm instanceof Node) {
144 nodes.add((Node) osm);
145 } else if (osm instanceof Way) {
146 ways.add((Way) osm);
147 }
148 }
149
150 if (ways.size() == 1 && !ways.get(0).isClosed()) {
151 // Case 1
152 Way w = ways.get(0);
153 fixNodes.add(w.firstNode());
154 fixNodes.add(w.lastNode());
155 fixNodes.addAll(nodes);
156 fixNodes.addAll(collectNodesWithExternReferers(ways));
157 // Temporary closed way used to reorder nodes
158 Way closedWay = new Way(w);
159 closedWay.addNode(w.firstNode());
160 List<Way> usedWays = new ArrayList<>(1);
161 usedWays.add(closedWay);
162 nodes = collectNodesAnticlockwise(usedWays);
163 } else if (!ways.isEmpty() && checkWaysArePolygon(ways)) {
164 // Case 2
165 List<Node> inside = new ArrayList<>();
166 List<Node> outside = new ArrayList<>();
167
168 for (Node n: nodes) {
169 boolean isInside = false;
170 for (Way w: ways) {
171 if (w.getNodes().contains(n)) {
172 isInside = true;
173 break;
174 }
175 }
176 if (isInside)
177 inside.add(n);
178 else
179 outside.add(n);
180 }
181
182 if (outside.size() == 1 && inside.isEmpty()) {
183 center = outside.get(0).getEastNorth();
184 } else if (outside.size() == 1 && inside.size() == 1) {
185 center = outside.get(0).getEastNorth();
186 radius = distance(center, inside.get(0).getEastNorth());
187 } else if (inside.size() == 2 && outside.isEmpty()) {
188 // 2 nodes inside, define diameter
189 EastNorth en0 = inside.get(0).getEastNorth();
190 EastNorth en1 = inside.get(1).getEastNorth();
191 center = new EastNorth((en0.east() + en1.east()) / 2, (en0.north() + en1.north()) / 2);
192 radius = distance(en0, en1) / 2;
193 }
194
195 fixNodes.addAll(inside);
196 fixNodes.addAll(collectNodesWithExternReferers(ways));
197 nodes = collectNodesAnticlockwise(ways);
198 if (nodes.size() < 4) {
199 new Notification(
200 tr("Not enough nodes in selected ways."))
201 .setIcon(JOptionPane.INFORMATION_MESSAGE)
202 .setDuration(Notification.TIME_SHORT)
203 .show();
204 return;
205 }
206 } else if (ways.isEmpty() && nodes.size() > 3) {
207 // Case 3
208 fixNodes.addAll(nodes);
209 // No need to reorder nodes since all are fix
210 } else {
211 // Invalid action
212 new Notification(
213 tr("Please select at least four nodes."))
214 .setIcon(JOptionPane.INFORMATION_MESSAGE)
215 .setDuration(Notification.TIME_SHORT)
216 .show();
217 return;
218 }
219
220 if (center == null) {
221 // Compute the center of nodes
222 center = Geometry.getCenter(nodes);
223 if (center == null) {
224 new Notification(tr("Cannot determine center of selected nodes."))
225 .setIcon(JOptionPane.INFORMATION_MESSAGE)
226 .setDuration(Notification.TIME_SHORT)
227 .show();
228 return;
229 }
230 }
231
232 // Now calculate the average distance to each node from the
233 // center. This method is ok as long as distances are short
234 // relative to the distance from the N or S poles.
235 if (radius == 0) {
236 for (Node n : nodes) {
237 radius += distance(center, n.getEastNorth());
238 }
239 radius = radius / nodes.size();
240 }
241
242 if (!actionAllowed(nodes)) return;
243
244 Collection<Command> cmds = new LinkedList<>();
245
246 // Move each node to that distance from the center.
247 // Nodes that are not "fix" will be adjust making regular arcs.
248 int nodeCount = nodes.size();
249 // Search first fixed node
250 int startPosition;
251 for (startPosition = 0; startPosition < nodeCount; startPosition++) {
252 if (fixNodes.contains(nodes.get(startPosition % nodeCount)))
253 break;
254 }
255 int i = startPosition; // Start position for current arc
256 int j; // End position for current arc
257 while (i < startPosition + nodeCount) {
258 for (j = i + 1; j < startPosition + nodeCount; j++) {
259 if (fixNodes.contains(nodes.get(j % nodeCount)))
260 break;
261 }
262 Node first = nodes.get(i % nodeCount);
263 PolarCoor pcFirst = new PolarCoor(first.getEastNorth(), center, 0);
264 pcFirst.radius = radius;
265 cmds.add(pcFirst.createMoveCommand(first));
266 if (j > i + 1) {
267 double delta;
268 if (j == i + nodeCount) {
269 delta = 2 * Math.PI / nodeCount;
270 } else {
271 PolarCoor pcLast = new PolarCoor(nodes.get(j % nodeCount).getEastNorth(), center, 0);
272 delta = pcLast.angle - pcFirst.angle;
273 if (delta < 0) // Assume each PolarCoor.angle is in range ]-pi; pi]
274 delta += 2*Math.PI;
275 delta /= j - i;
276 }
277 for (int k = i+1; k < j; k++) {
278 PolarCoor p = new PolarCoor(radius, pcFirst.angle + (k-i)*delta, center, 0);
279 cmds.add(p.createMoveCommand(nodes.get(k % nodeCount)));
280 }
281 }
282 i = j; // Update start point for next iteration
283 }
284
285 Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Circle"), cmds));
286 Main.map.repaint();
287 }
288
289 /**
290 * Collect all nodes with more than one referrer.
291 * @param ways Ways from witch nodes are selected
292 * @return List of nodes with more than one referrer
293 */
294 private static List<Node> collectNodesWithExternReferers(List<Way> ways) {
295 List<Node> withReferrers = new ArrayList<>();
296 for (Way w: ways) {
297 for (Node n: w.getNodes()) {
298 if (n.getReferrers().size() > 1) {
299 withReferrers.add(n);
300 }
301 }
302 }
303 return withReferrers;
304 }
305
306 /**
307 * Assuming all ways can be joined into polygon, create an ordered list of node.
308 * @param ways List of ways to be joined
309 * @return Nodes anticlockwise ordered
310 */
311 private static List<Node> collectNodesAnticlockwise(List<Way> ways) {
312 List<Node> nodes = new ArrayList<>();
313 Node firstNode = ways.get(0).firstNode();
314 Node lastNode = null;
315 Way lastWay = null;
316 while (firstNode != lastNode) {
317 if (lastNode == null) lastNode = firstNode;
318 for (Way way: ways) {
319 if (way == lastWay) continue;
320 if (way.firstNode() == lastNode) {
321 List<Node> wayNodes = way.getNodes();
322 for (int i = 0; i < wayNodes.size() - 1; i++) {
323 nodes.add(wayNodes.get(i));
324 }
325 lastNode = way.lastNode();
326 lastWay = way;
327 break;
328 }
329 if (way.lastNode() == lastNode) {
330 List<Node> wayNodes = way.getNodes();
331 for (int i = wayNodes.size() - 1; i > 0; i--) {
332 nodes.add(wayNodes.get(i));
333 }
334 lastNode = way.firstNode();
335 lastWay = way;
336 break;
337 }
338 }
339 }
340 // Check if nodes are in anticlockwise order
341 int nc = nodes.size();
342 double area = 0;
343 for (int i = 0; i < nc; i++) {
344 EastNorth p1 = nodes.get(i).getEastNorth();
345 EastNorth p2 = nodes.get((i+1) % nc).getEastNorth();
346 area += p1.east()*p2.north() - p2.east()*p1.north();
347 }
348 if (area < 0)
349 Collections.reverse(nodes);
350 return nodes;
351 }
352
353 /**
354 * Check if one or more nodes are outside of download area
355 * @param nodes Nodes to check
356 * @return true if action can be done
357 */
358 private static boolean actionAllowed(Collection<Node> nodes) {
359 boolean outside = false;
360 for (Node n: nodes) {
361 if (n.isOutsideDownloadArea()) {
362 outside = true;
363 break;
364 }
365 }
366 if (outside)
367 new Notification(
368 tr("One or more nodes involved in this action is outside of the downloaded area."))
369 .setIcon(JOptionPane.WARNING_MESSAGE)
370 .setDuration(Notification.TIME_SHORT)
371 .show();
372 return true;
373 }
374
375 @Override
376 protected void updateEnabledState() {
377 DataSet ds = getLayerManager().getEditDataSet();
378 setEnabled(ds != null && !ds.selectionEmpty());
379 }
380
381 @Override
382 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
383 setEnabled(selection != null && !selection.isEmpty());
384 }
385
386 /**
387 * Determines if ways can be joined into a polygon.
388 * @param ways The ways collection to check
389 * @return true if all ways can be joined into a polygon
390 */
391 protected static boolean checkWaysArePolygon(Collection<Way> ways) {
392 // For each way, nodes strictly between first and last should't be reference by an other way
393 for (Way way: ways) {
394 for (Node node: way.getNodes()) {
395 if (way.isFirstLastNode(node)) continue;
396 for (Way wayOther: ways) {
397 if (way == wayOther) continue;
398 if (node.getReferrers().contains(wayOther)) return false;
399 }
400 }
401 }
402 // Test if ways can be joined
403 Way currentWay = null;
404 Node startNode = null, endNode = null;
405 int used = 0;
406 while (true) {
407 Way nextWay = null;
408 for (Way w: ways) {
409 if (w.isClosed()) return ways.size() == 1;
410 if (w == currentWay) continue;
411 if (currentWay == null) {
412 nextWay = w;
413 startNode = w.firstNode();
414 endNode = w.lastNode();
415 break;
416 }
417 if (w.firstNode() == endNode) {
418 nextWay = w;
419 endNode = w.lastNode();
420 break;
421 }
422 if (w.lastNode() == endNode) {
423 nextWay = w;
424 endNode = w.firstNode();
425 break;
426 }
427 }
428 if (nextWay == null) return false;
429 used += 1;
430 currentWay = nextWay;
431 if (endNode == startNode) return used == ways.size();
432 }
433 }
434}
Note: See TracBrowser for help on using the repository browser.