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

Last change on this file since 13494 was 13434, checked in by Don-vip, 6 years ago

see #8039, see #10456 - support read-only data layers

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