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

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

sonar - squid:S1854 - Dead stores should be removed

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