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

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

fix #8431, fix #9839 - fix issues with "align nodes in circle" action (patch by Balaitous)

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