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

Last change on this file since 6892 was 6892, checked in by bastiK, 10 years ago

applied #9223 - Allow align in circle for multiple ways (patch by Balaitous)

  • Property svn:eol-style set to native
File size: 16.0 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.math.BigDecimal;
10import java.math.MathContext;
11import java.util.Collection;
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.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.Geometry;
29import org.openstreetmap.josm.tools.Shortcut;
30
31/**
32 * Aligns all selected nodes within a circle. (Useful for roundabouts)
33 *
34 * @since 146
35 *
36 * @author Matthew Newton
37 * @author Petr Dlouhý
38 * @author Teemu Koskinen
39 */
40public final class AlignInCircleAction extends JosmAction {
41
42 /**
43 * Constructs a new {@code AlignInCircleAction}.
44 */
45 public AlignInCircleAction() {
46 super(tr("Align Nodes in Circle"), "aligncircle", tr("Move the selected nodes into a circle."),
47 Shortcut.registerShortcut("tools:aligncircle", tr("Tool: {0}", tr("Align Nodes in Circle")),
48 KeyEvent.VK_O, Shortcut.DIRECT), true);
49 putValue("help", ht("/Action/AlignInCircle"));
50 }
51
52 private static double distance(EastNorth n, EastNorth m) {
53 double easd, nord;
54 easd = n.east() - m.east();
55 nord = n.north() - m.north();
56 return Math.sqrt(easd * easd + nord * nord);
57 }
58
59 public static class PolarCoor {
60 double radius;
61 double angle;
62 EastNorth origin = new EastNorth(0, 0);
63 double azimuth = 0;
64
65 PolarCoor(double radius, double angle) {
66 this(radius, angle, new EastNorth(0, 0), 0);
67 }
68
69 PolarCoor(double radius, double angle, EastNorth origin, double azimuth) {
70 this.radius = radius;
71 this.angle = angle;
72 this.origin = origin;
73 this.azimuth = azimuth;
74 }
75
76 PolarCoor(EastNorth en) {
77 this(en, new EastNorth(0, 0), 0);
78 }
79
80 PolarCoor(EastNorth en, EastNorth origin, double azimuth) {
81 radius = distance(en, origin);
82 angle = Math.atan2(en.north() - origin.north(), en.east() - origin.east());
83 this.origin = origin;
84 this.azimuth = azimuth;
85 }
86
87 public EastNorth toEastNorth() {
88 return new EastNorth(radius * Math.cos(angle - azimuth) + origin.east(), radius * Math.sin(angle - azimuth)
89 + origin.north());
90 }
91
92 /**
93 * Create a MoveCommand to move a node to this PolarCoor.
94 * @param n Node to move
95 * @return new MoveCommand
96 */
97 public MoveCommand createMoveCommand(Node n) {
98 EastNorth en = toEastNorth();
99 return new MoveCommand(n, en.east() - n.getEastNorth().east(), en.north() - n.getEastNorth().north());
100 }
101 }
102
103 @Override
104 public void actionPerformed(ActionEvent e) {
105 if (!isEnabled())
106 return;
107
108 Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
109 List<Node> nodes = new LinkedList<Node>();
110 List<Way> ways = new LinkedList<Way>();
111 EastNorth center = null;
112 double radius = 0;
113 boolean regular = false;
114
115 for (OsmPrimitive osm : sel) {
116 if (osm instanceof Node) {
117 nodes.add((Node) osm);
118 } else if (osm instanceof Way) {
119 ways.add((Way) osm);
120 }
121 }
122
123 // special case if no single nodes are selected and exactly one way is:
124 // then use the way's nodes
125 if ((nodes.size() <= 2) && checkWaysArePolygon(ways)) {
126 // some more special combinations:
127 // When is selected node that is part of the way, then make a regular polygon, selected
128 // node doesn't move.
129 // I haven't got better idea, how to activate that function.
130 //
131 // When one way and one node is selected, set center to position of that node.
132 // When one more node, part of the way, is selected, set the radius equal to the
133 // distance between two nodes.
134 if (nodes.size() == 1 && ways.size() == 1) {
135 // Regular polygons are allowed only if there is just one way
136 // Should be remove regular are now default for all nodes with no more than 1 referrer.
137 Way way = ways.get(0);
138 if (nodes.size() == 1 && way.containsNode(nodes.get(0)) && allowRegularPolygon(way.getNodes()))
139 regular = true;
140 }
141 if (nodes.size() >= 1) {
142 boolean[] isContained = new boolean[nodes.size()];
143 for(int i = 0; i < nodes.size(); i++) {
144 Node n = nodes.get(i);
145 isContained[i] = false;
146 for(Way way: ways)
147 if(way.containsNode(n)) {
148 isContained[i] = true;
149 break;
150 }
151 }
152 if(nodes.size() == 1) {
153 if(!isContained[0])
154 center = nodes.get(0).getEastNorth();
155 } else {
156 if(!isContained[0] && !isContained[1]) {
157 // 2 nodes outside of way, can't choose one as center
158 new Notification(
159 tr("Please select only one node as center."))
160 .setIcon(JOptionPane.INFORMATION_MESSAGE)
161 .setDuration(Notification.TIME_SHORT)
162 .show();
163 return;
164 } else if (!isContained[0] || !isContained[1]) {
165 // 1 node inside and 1 outside, outside is center, inside node define radius
166 center = nodes.get(isContained[0] ? 1 : 0).getEastNorth();
167 radius = distance(nodes.get(0).getEastNorth(), nodes.get(1).getEastNorth());
168 } else {
169 // 2 nodes inside, define diameter
170 EastNorth en0 = nodes.get(0).getEastNorth();
171 EastNorth en1 = nodes.get(1).getEastNorth();
172 center = new EastNorth((en0.east() + en1.east()) / 2, (en0.north() + en1.north()) / 2);
173 radius = distance(en0, en1) / 2;
174 }
175 }
176 }
177 nodes.clear();
178
179 for(Way way: ways)
180 for (Node n : way.getNodes()) {
181 if (!nodes.contains(n)) {
182 nodes.add(n);
183 }
184 }
185 }
186
187 if (nodes.size() < 4) {
188 new Notification(
189 tr("Please select at least four nodes."))
190 .setIcon(JOptionPane.INFORMATION_MESSAGE)
191 .setDuration(Notification.TIME_SHORT)
192 .show();
193 return;
194 }
195
196 // Reorder the nodes if they didn't come from a single way
197 if (ways.size() != 1) {
198 // First calculate the average point
199
200 BigDecimal east = BigDecimal.ZERO;
201 BigDecimal north = BigDecimal.ZERO;
202
203 for (Node n : nodes) {
204 BigDecimal x = new BigDecimal(n.getEastNorth().east());
205 BigDecimal y = new BigDecimal(n.getEastNorth().north());
206 east = east.add(x, MathContext.DECIMAL128);
207 north = north.add(y, MathContext.DECIMAL128);
208 }
209 BigDecimal nodesSize = new BigDecimal(nodes.size());
210 east = east.divide(nodesSize, MathContext.DECIMAL128);
211 north = north.divide(nodesSize, MathContext.DECIMAL128);
212
213 EastNorth average = new EastNorth(east.doubleValue(), north.doubleValue());
214 List<Node> newNodes = new LinkedList<Node>();
215
216 // Then reorder them based on heading from the average point
217 while (!nodes.isEmpty()) {
218 double maxHeading = -1.0;
219 Node maxNode = null;
220 for (Node n : nodes) {
221 double heading = average.heading(n.getEastNorth());
222 if (heading > maxHeading) {
223 maxHeading = heading;
224 maxNode = n;
225 }
226 }
227 newNodes.add(maxNode);
228 nodes.remove(maxNode);
229 }
230
231 nodes = newNodes;
232 }
233
234 if (center == null) {
235 // Compute the centroid of nodes
236 center = Geometry.getCentroid(nodes);
237 }
238 // Node "center" now is central to all selected nodes.
239
240 // Now calculate the average distance to each node from the
241 // centre. This method is ok as long as distances are short
242 // relative to the distance from the N or S poles.
243 if (radius == 0) {
244 for (Node n : nodes) {
245 radius += distance(center, n.getEastNorth());
246 }
247 radius = radius / nodes.size();
248 }
249
250 if(!actionAllowed(nodes)) return;
251
252 Collection<Command> cmds = new LinkedList<Command>();
253
254 PolarCoor pc;
255
256 if (regular) { // Make a regular polygon
257 double angle = Math.PI * 2 / nodes.size();
258 pc = new PolarCoor(nodes.get(0).getEastNorth(), center, 0);
259
260 if (pc.angle > (new PolarCoor(nodes.get(1).getEastNorth(), center, 0).angle)) {
261 angle *= -1;
262 }
263
264 pc.radius = radius;
265 for (Node n : nodes) {
266 EastNorth no = pc.toEastNorth();
267 cmds.add(new MoveCommand(n, no.east() - n.getEastNorth().east(), no.north() - n.getEastNorth().north()));
268 pc.angle += angle;
269 }
270 } else { // Move each node to that distance from the center.
271 int nodeCount = nodes.size();
272 // Search first fixed node
273 int startPosition = 0;
274 for(startPosition = 0; startPosition < nodeCount; startPosition++)
275 if(isFixNode(nodes.get(startPosition % nodeCount), sel)) break;
276 int i = startPosition; // Start position for current arc
277 int j; // End position for current arc
278 while(i < startPosition + nodeCount) {
279 for(j = i + 1; j < startPosition + nodeCount; j++)
280 if(isFixNode(nodes.get(j % nodeCount), sel)) break;
281 Node first = nodes.get(i % nodeCount);
282 PolarCoor pcFirst = new PolarCoor(first.getEastNorth(), center, 0);
283 pcFirst.radius = radius;
284 cmds.add(pcFirst.createMoveCommand(first));
285 if(j > i + 1) {
286 double delta;
287 if(j == i + nodeCount) {
288 delta = 2 * Math.PI / nodeCount;
289 } else {
290 PolarCoor pcLast = new PolarCoor(nodes.get(j % nodeCount).getEastNorth(), center, 0);
291 delta = pcLast.angle - pcFirst.angle;
292 if(delta < 0) // Assume each PolarCoor.angle is in range ]-pi; pi]
293 delta += 2*Math.PI;
294 delta /= j - i;
295 }
296 for(int k = i+1; k < j; k++) {
297 PolarCoor p = new PolarCoor(radius, pcFirst.angle + (k-i)*delta, center, 0);
298 cmds.add(p.createMoveCommand(nodes.get(k % nodeCount)));
299 }
300 }
301 i = j; // Update start point for next iteration
302 }
303 }
304
305 Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Circle"), cmds));
306 Main.map.repaint();
307 }
308
309 /**
310 * Check if one or more nodes are outside of download area
311 * @param nodes Nodes to check
312 * @return true if action can be done
313 */
314 private boolean actionAllowed(Collection<Node> nodes) {
315 boolean outside = false;
316 for(Node n: nodes)
317 if(n.isOutsideDownloadArea()) {
318 outside = true;
319 break;
320 }
321 if(outside)
322 new Notification(
323 tr("One or more nodes involved in this action is outside of the downloaded area."))
324 .setIcon(JOptionPane.WARNING_MESSAGE)
325 .setDuration(Notification.TIME_SHORT)
326 .show();
327 return true;
328 }
329
330 /**
331 * Test if angle of a node can be change.
332 * @param n Node
333 * @param sel Selection which action is apply
334 * @return true is this node does't have a fix angle
335 */
336 private boolean isFixNode(Node n, Collection<OsmPrimitive> sel) {
337 List<OsmPrimitive> referrers = n.getReferrers();
338 if(referrers.isEmpty()) return false;
339 if(sel.contains(n) || referrers.size() > 1 || !sel.contains(referrers.get(0))) return true;
340 return false;
341 }
342
343 @Override
344 protected void updateEnabledState() {
345 setEnabled(getCurrentDataSet() != null && !getCurrentDataSet().getSelected().isEmpty());
346 }
347
348 @Override
349 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
350 setEnabled(selection != null && !selection.isEmpty());
351 }
352
353 /**
354 * Determines if a regular polygon is allowed to be created with the given nodes collection.
355 * @param nodes The nodes collection to check.
356 * @return true if all nodes in the given collection are referred by the same object, and no other one (see #8431)
357 */
358 protected static boolean allowRegularPolygon(Collection<Node> nodes) {
359 Set<OsmPrimitive> allReferrers = new HashSet<OsmPrimitive>();
360 for (Node n : nodes) {
361 List<OsmPrimitive> referrers = n.getReferrers();
362 if (referrers.size() > 1 || (allReferrers.addAll(referrers) && allReferrers.size() > 1)) {
363 return false;
364 }
365 }
366 return true;
367 }
368
369 /**
370 * Determines if ways can be joined into a polygon.
371 * @param ways The ways collection to check
372 * @return true if all ways can be joined into a polygon
373 */
374 protected static boolean checkWaysArePolygon(Collection<Way> ways) {
375 // For each way, nodes strictly between first and last should't be reference by an other way
376 for(Way way: ways) {
377 for(Node node: way.getNodes()) {
378 if(node == way.firstNode() || node == way.lastNode()) continue;
379 for(Way wayOther: ways) {
380 if(way == wayOther) continue;
381 if(node.getReferrers().contains(wayOther)) return false;
382 }
383 }
384 }
385 // Test if ways can be joined
386 Way currentWay = null;
387 Node startNode = null, endNode = null;
388 int used = 0;
389 while(true) {
390 Way nextWay = null;
391 for(Way w: ways) {
392 if(w.firstNode() == w.lastNode()) return ways.size() == 1;
393 if(w == currentWay) continue;
394 if(currentWay == null) {
395 nextWay = w;
396 startNode = w.firstNode();
397 endNode = w.lastNode();
398 break;
399 }
400 if(w.firstNode() == endNode) {
401 nextWay = w;
402 endNode = w.lastNode();
403 break;
404 }
405 if(w.lastNode() == endNode) {
406 nextWay = w;
407 endNode = w.firstNode();
408 break;
409 }
410 }
411 if(nextWay == null) return false;
412 used += 1;
413 currentWay = nextWay;
414 if(endNode == startNode) return used == ways.size();
415 }
416 }
417}
Note: See TracBrowser for help on using the repository browser.