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

Last change on this file since 17379 was 17188, checked in by Klumbumbus, 4 years ago

fix #19851 - Fix shortcut names

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