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

Last change on this file since 16442 was 16442, checked in by simon04, 4 years ago

Fix typo "referrer"

  • 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("Tool: {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 List<Way> usedWays = new ArrayList<>(1);
123 usedWays.add(closedWay);
124 nodes = collectNodesAnticlockwise(usedWays);
125 } else if (!ways.isEmpty() && checkWaysArePolygon(ways)) {
126 // Case 2
127 List<Node> inside = new ArrayList<>();
128 List<Node> outside = new ArrayList<>();
129
130 for (Node n: nodes) {
131 boolean isInside = ways.stream().anyMatch(w -> w.getNodes().contains(n));
132 if (isInside)
133 inside.add(n);
134 else
135 outside.add(n);
136 }
137
138 if (outside.size() == 1 && inside.isEmpty()) {
139 center = outside.get(0).getEastNorth();
140 } else if (outside.size() == 1 && inside.size() == 1) {
141 center = outside.get(0).getEastNorth();
142 radius = center.distance(inside.get(0).getEastNorth());
143 } else if (inside.size() == 2 && outside.isEmpty()) {
144 // 2 nodes inside, define diameter
145 EastNorth en0 = inside.get(0).getEastNorth();
146 EastNorth en1 = inside.get(1).getEastNorth();
147 center = new EastNorth((en0.east() + en1.east()) / 2, (en0.north() + en1.north()) / 2);
148 radius = en0.distance(en1) / 2;
149 }
150
151 fixNodes.addAll(inside);
152 fixNodes.addAll(collectNodesWithExternReferrers(ways));
153 nodes = collectNodesAnticlockwise(ways);
154 if (nodes.size() < 4) {
155 new Notification(
156 tr("Not enough nodes in selected ways."))
157 .setIcon(JOptionPane.INFORMATION_MESSAGE)
158 .setDuration(Notification.TIME_SHORT)
159 .show();
160 return;
161 }
162 } else if (ways.isEmpty() && nodes.size() > 3) {
163 // Case 3
164 fixNodes.addAll(nodes);
165 // No need to reorder nodes since all are fix
166 } else {
167 // Invalid action
168 new Notification(
169 tr("Please select at least four nodes."))
170 .setIcon(JOptionPane.INFORMATION_MESSAGE)
171 .setDuration(Notification.TIME_SHORT)
172 .show();
173 return;
174 }
175
176 if (center == null) {
177 // Compute the center of nodes
178 center = Geometry.getCenter(nodes);
179 if (center == null) {
180 new Notification(tr("Cannot determine center of selected nodes."))
181 .setIcon(JOptionPane.INFORMATION_MESSAGE)
182 .setDuration(Notification.TIME_SHORT)
183 .show();
184 return;
185 }
186 }
187
188 // Now calculate the average distance to each node from the
189 // center. This method is ok as long as distances are short
190 // relative to the distance from the N or S poles.
191 if (radius == 0) {
192 for (Node n : nodes) {
193 radius += center.distance(n.getEastNorth());
194 }
195 radius = radius / nodes.size();
196 }
197
198 if (!actionAllowed(nodes)) return;
199
200 Collection<Command> cmds = new LinkedList<>();
201
202 // Move each node to that distance from the center.
203 // Nodes that are not "fix" will be adjust making regular arcs.
204 int nodeCount = nodes.size();
205 // Search first fixed node
206 int startPosition;
207 for (startPosition = 0; startPosition < nodeCount; startPosition++) {
208 if (fixNodes.contains(nodes.get(startPosition % nodeCount)))
209 break;
210 }
211 int i = startPosition; // Start position for current arc
212 int j; // End position for current arc
213 while (i < startPosition + nodeCount) {
214 for (j = i + 1; j < startPosition + nodeCount; j++) {
215 if (fixNodes.contains(nodes.get(j % nodeCount)))
216 break;
217 }
218 Node first = nodes.get(i % nodeCount);
219 PolarCoor pcFirst = new PolarCoor(radius, PolarCoor.computeAngle(first.getEastNorth(), center), center);
220 cmds.add(createMoveCommand(first, pcFirst));
221 if (j > i + 1) {
222 double delta;
223 if (j == i + nodeCount) {
224 delta = 2 * Math.PI / nodeCount;
225 } else {
226 PolarCoor pcLast = new PolarCoor(nodes.get(j % nodeCount).getEastNorth(), center);
227 delta = pcLast.angle - pcFirst.angle;
228 if (delta < 0) // Assume each PolarCoor.angle is in range ]-pi; pi]
229 delta += 2*Math.PI;
230 delta /= j - i;
231 }
232 for (int k = i+1; k < j; k++) {
233 PolarCoor p = new PolarCoor(radius, pcFirst.angle + (k-i)*delta, center);
234 cmds.add(createMoveCommand(nodes.get(k % nodeCount), p));
235 }
236 }
237 i = j; // Update start point for next iteration
238 }
239
240 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Align Nodes in Circle"), cmds));
241 }
242
243 /**
244 * Collect all nodes with more than one referrer.
245 * @param ways Ways from witch nodes are selected
246 * @return List of nodes with more than one referrer
247 */
248 private static List<Node> collectNodesWithExternReferrers(List<Way> ways) {
249 return ways.stream().flatMap(w -> w.getNodes().stream()).filter(n -> n.getReferrers().size() > 1).collect(Collectors.toList());
250 }
251
252 /**
253 * Assuming all ways can be joined into polygon, create an ordered list of node.
254 * @param ways List of ways to be joined
255 * @return Nodes anticlockwise ordered
256 */
257 private static List<Node> collectNodesAnticlockwise(List<Way> ways) {
258 List<Node> nodes = new ArrayList<>();
259 Node firstNode = ways.get(0).firstNode();
260 Node lastNode = null;
261 Way lastWay = null;
262 while (firstNode != lastNode) {
263 if (lastNode == null) lastNode = firstNode;
264 for (Way way: ways) {
265 if (way == lastWay) continue;
266 if (way.firstNode() == lastNode) {
267 List<Node> wayNodes = way.getNodes();
268 for (int i = 0; i < wayNodes.size() - 1; i++) {
269 nodes.add(wayNodes.get(i));
270 }
271 lastNode = way.lastNode();
272 lastWay = way;
273 break;
274 }
275 if (way.lastNode() == lastNode) {
276 List<Node> wayNodes = way.getNodes();
277 for (int i = wayNodes.size() - 1; i > 0; i--) {
278 nodes.add(wayNodes.get(i));
279 }
280 lastNode = way.firstNode();
281 lastWay = way;
282 break;
283 }
284 }
285 }
286 // Check if nodes are in anticlockwise order
287 int nc = nodes.size();
288 double area = 0;
289 for (int i = 0; i < nc; i++) {
290 EastNorth p1 = nodes.get(i).getEastNorth();
291 EastNorth p2 = nodes.get((i+1) % nc).getEastNorth();
292 area += p1.east()*p2.north() - p2.east()*p1.north();
293 }
294 if (area < 0)
295 Collections.reverse(nodes);
296 return nodes;
297 }
298
299 /**
300 * Check if one or more nodes are outside of download area
301 * @param nodes Nodes to check
302 * @return true if action can be done
303 */
304 private static boolean actionAllowed(Collection<Node> nodes) {
305 boolean outside = nodes.stream().anyMatch(Node::isOutsideDownloadArea);
306 if (outside)
307 new Notification(
308 tr("One or more nodes involved in this action is outside of the downloaded area."))
309 .setIcon(JOptionPane.WARNING_MESSAGE)
310 .setDuration(Notification.TIME_SHORT)
311 .show();
312 return true;
313 }
314
315 @Override
316 protected void updateEnabledState() {
317 DataSet ds = getLayerManager().getEditDataSet();
318 setEnabled(ds != null && !ds.selectionEmpty());
319 }
320
321 @Override
322 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
323 updateEnabledStateOnModifiableSelection(selection);
324 }
325
326 /**
327 * Determines if ways can be joined into a polygon.
328 * @param ways The ways collection to check
329 * @return true if all ways can be joined into a polygon
330 */
331 private static boolean checkWaysArePolygon(Collection<Way> ways) {
332 // For each way, nodes strictly between first and last should't be reference by an other way
333 for (Way way: ways) {
334 for (Node node: way.getNodes()) {
335 if (way.isFirstLastNode(node)) continue;
336 if (ways.stream().filter(wayOther -> way != wayOther).anyMatch(wayOther -> node.getReferrers().contains(wayOther))) {
337 return false;
338 }
339 }
340 }
341 // Test if ways can be joined
342 Way currentWay = null;
343 Node startNode = null, endNode = null;
344 int used = 0;
345 while (true) {
346 Way nextWay = null;
347 for (Way w: ways) {
348 if (w.isClosed()) return ways.size() == 1;
349 if (w == currentWay) continue;
350 if (currentWay == null) {
351 nextWay = w;
352 startNode = w.firstNode();
353 endNode = w.lastNode();
354 break;
355 }
356 if (w.firstNode() == endNode) {
357 nextWay = w;
358 endNode = w.lastNode();
359 break;
360 }
361 if (w.lastNode() == endNode) {
362 nextWay = w;
363 endNode = w.firstNode();
364 break;
365 }
366 }
367 if (nextWay == null) return false;
368 used += 1;
369 currentWay = nextWay;
370 if (endNode == startNode) return used == ways.size();
371 }
372 }
373}
Note: See TracBrowser for help on using the repository browser.