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

Last change on this file since 14134 was 14134, checked in by Don-vip, 4 months ago

see #15229 - deprecate Main*.undoRedo - make UndoRedoHandler a singleton

  • 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.UndoRedoHandler;
23import org.openstreetmap.josm.data.coor.EastNorth;
24import org.openstreetmap.josm.data.coor.PolarCoor;
25import org.openstreetmap.josm.data.osm.DataSet;
26import org.openstreetmap.josm.data.osm.Node;
27import org.openstreetmap.josm.data.osm.OsmPrimitive;
28import org.openstreetmap.josm.data.osm.Way;
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        UndoRedoHandler.getInstance().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.