source: josm/trunk/src/org/openstreetmap/josm/actions/CreateCircleAction.java

Last change on this file was 18494, checked in by taylor.smock, 8 months ago

Fix #22115: Extract methods from LatLon into ILatLon where they are generally applicable

This also removes calls to Node#getCoor where possible, which reduces
the number of memory allocations in SearchCompiler#match, and overall
allocations due to Node#getCoor

  • Property svn:eol-style set to native
File size: 11.1 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;
14import java.util.Objects;
15
16import javax.swing.JOptionPane;
17
18import org.openstreetmap.josm.command.AddCommand;
19import org.openstreetmap.josm.command.ChangeNodesCommand;
20import org.openstreetmap.josm.command.Command;
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.LatLon;
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.data.projection.ProjectionRegistry;
31import org.openstreetmap.josm.gui.Notification;
32import org.openstreetmap.josm.tools.Geometry;
33import org.openstreetmap.josm.tools.RightAndLefthandTraffic;
34import org.openstreetmap.josm.tools.Shortcut;
35
36/**
37 * - Create a new circle from two selected nodes or a way with 2 nodes which represent the diameter of the circle.
38 * - Create a new circle from three selected nodes--or a way with 3 nodes.
39 * - Useful for roundabouts
40 *
41 * Notes:
42 *   * If a way is selected, it is changed. If nodes are selected a new way is created.
43 *     So if you've got a way with nodes it makes a difference between running this on the way or the nodes!
44 *   * The existing nodes are retained, and additional nodes are inserted regularly
45 *     to achieve the desired number of nodes (`createcircle.nodecount`).
46 * BTW: Someone might want to implement projection corrections for this...
47 *
48 * @author Henry Loenwind
49 * @author Sebastian Masch
50 * @author Alain Delplanque
51 *
52 * @since 996
53 */
54public final class CreateCircleAction extends JosmAction {
55
56    /**
57     * Constructs a new {@code CreateCircleAction}.
58     */
59    public CreateCircleAction() {
60        super(tr("Create Circle"), "aligncircle", tr("Create a circle from three selected nodes."),
61            Shortcut.registerShortcut("tools:createcircle", tr("Tools: {0}", tr("Create Circle")),
62            KeyEvent.VK_O, Shortcut.SHIFT), true, "createcircle", true);
63        setHelpId(ht("/Action/CreateCircle"));
64    }
65
66    /**
67     * Distributes nodes according to the algorithm of election with largest remainder.
68     * @param angles Array of PolarNode ordered by increasing angles
69     * @param nodesCount Number of nodes to be distributed
70     * @return Array of number of nodes to put in each arc
71     */
72    private static int[] distributeNodes(PolarNode[] angles, int nodesCount) {
73        int[] count = new int[angles.length];
74        double[] width = new double[angles.length];
75        double[] remainder = new double[angles.length];
76        for (int i = 0; i < angles.length; i++) {
77            width[i] = angles[(i+1) % angles.length].a - angles[i].a;
78            if (width[i] < 0)
79                width[i] += 2*Math.PI;
80        }
81        int assign = 0;
82        for (int i = 0; i < angles.length; i++) {
83            double part = width[i] / 2.0 / Math.PI * nodesCount;
84            count[i] = (int) Math.floor(part);
85            remainder[i] = part - count[i];
86            assign += count[i];
87        }
88        while (assign < nodesCount) {
89            int imax = 0;
90            for (int i = 1; i < angles.length; i++) {
91                if (remainder[i] > remainder[imax])
92                    imax = i;
93            }
94            count[imax]++;
95            remainder[imax] = 0;
96            assign++;
97        }
98        return count;
99    }
100
101    /**
102     * Class designed to create a couple between a node and its angle relative to the center of the circle.
103     */
104    private static class PolarNode implements Comparable<PolarNode> {
105        private final double a;
106        private final Node node;
107
108        PolarNode(EastNorth center, Node n) {
109            this.a = PolarCoor.computeAngle(n.getEastNorth(), center);
110            this.node = n;
111        }
112
113        @Override
114        public int compareTo(PolarNode o) {
115            return Double.compare(a, o.a);
116        }
117
118        @Override
119        public int hashCode() {
120            return Objects.hash(a, node);
121        }
122
123        @Override
124        public boolean equals(Object obj) {
125            if (this == obj)
126                return true;
127            if (obj == null || getClass() != obj.getClass())
128                return false;
129            PolarNode other = (PolarNode) obj;
130            return Double.doubleToLongBits(a) == Double.doubleToLongBits(other.a) && Objects.equals(node, other.node);
131        }
132    }
133
134    @Override
135    public void actionPerformed(ActionEvent e) {
136        if (!isEnabled())
137            return;
138        runOn(getLayerManager().getEditDataSet());
139    }
140
141    /**
142     * Run the action on the given dataset.
143     * @param ds dataset
144     * @since 14542
145     */
146    public static void runOn(DataSet ds) {
147        List<Node> nodes = new ArrayList<>(ds.getSelectedNodes());
148        Collection<Way> ways = ds.getSelectedWays();
149
150        Way existingWay = null;
151
152        // special case if no single nodes are selected and exactly one way is:
153        // then use the way's nodes
154        if (nodes.isEmpty() && (ways.size() == 1)) {
155            existingWay = ways.iterator().next();
156            for (Node n : existingWay.getNodes()) {
157                if (!nodes.contains(n)) {
158                    nodes.add(n);
159                }
160            }
161        }
162
163        if (nodes.size() < 2 || nodes.size() > 3) {
164            new Notification(
165                    tr("Please select exactly two or three nodes or one way with exactly two or three nodes."))
166                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
167                    .setDuration(Notification.TIME_LONG)
168                    .show();
169            return;
170        }
171
172        EastNorth center;
173
174        if (nodes.size() == 2) {
175            // diameter: two single nodes needed or a way with two nodes
176            EastNorth n1 = nodes.get(0).getEastNorth();
177            EastNorth n2 = nodes.get(1).getEastNorth();
178
179            center = n1.getCenter(n2);
180        } else {
181            // triangle: three single nodes needed or a way with three nodes
182            center = Geometry.getCenter(nodes);
183            if (center == null) {
184                notifyNodesNotOnCircle();
185                return;
186            }
187        }
188
189        // calculate the radius (r)
190        EastNorth n1 = nodes.get(0).getEastNorth();
191        double r = n1.distance(center);
192
193        // see #10777
194        LatLon ll2 = ProjectionRegistry.getProjection().eastNorth2latlon(center);
195
196        double radiusInMeters = nodes.get(0).greatCircleDistance(ll2);
197
198        int numberOfNodesInCircle = (int) Math.ceil(6.0 * Math.pow(radiusInMeters, 0.5));
199        // an odd number of nodes makes the distribution uneven
200        if ((numberOfNodesInCircle % 2) != 0) {
201            // add 1 to make it even
202            numberOfNodesInCircle += 1;
203        }
204        if (numberOfNodesInCircle < 6) {
205            numberOfNodesInCircle = 6;
206        }
207
208        // Order nodes by angle
209        final PolarNode[] angles = nodes.stream()
210                .map(n -> new PolarNode(center, n))
211                .sorted()
212                .toArray(PolarNode[]::new);
213        int[] count = distributeNodes(angles,
214                numberOfNodesInCircle >= nodes.size() ? (numberOfNodesInCircle - nodes.size()) : 0);
215
216        // now we can start doing things to OSM data
217        Collection<Command> cmds = new LinkedList<>();
218
219        // build a way for the circle
220        List<Node> nodesToAdd = new ArrayList<>();
221        for (int i = 0; i < nodes.size(); i++) {
222            nodesToAdd.add(angles[i].node);
223            double delta = angles[(i+1) % nodes.size()].a - angles[i].a;
224            if (delta < 0)
225                delta += 2*Math.PI;
226            for (int j = 0; j < count[i]; j++) {
227                double alpha = angles[i].a + (j+1)*delta/(count[i]+1);
228                double x = center.east() + r*Math.cos(alpha);
229                double y = center.north() + r*Math.sin(alpha);
230                LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(new EastNorth(x, y));
231                if (new Node(new EastNorth(x, y)).isOutSideWorld()) {
232                    notifyNodesOutsideWorld();
233                    return;
234                }
235                Node n = new Node(ll);
236                nodesToAdd.add(n);
237                cmds.add(new AddCommand(ds, n));
238            }
239        }
240        nodesToAdd.add(nodesToAdd.get(0)); // close the circle
241        if (existingWay != null && existingWay.getNodesCount() >= 3) {
242            nodesToAdd = orderNodesByWay(nodesToAdd, existingWay);
243        } else {
244            nodesToAdd = orderNodesByTrafficHand(nodesToAdd);
245        }
246        if (existingWay == null) {
247            Way newWay = new Way();
248            newWay.setNodes(nodesToAdd);
249            cmds.add(new AddCommand(ds, newWay));
250        } else {
251            cmds.add(new ChangeNodesCommand(ds, existingWay, nodesToAdd));
252        }
253
254        UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create Circle"), cmds));
255    }
256
257    /**
258     * Order nodes according to left/right hand traffic.
259     * @param nodes Nodes list to be ordered.
260     * @return Modified nodes list ordered according hand traffic.
261     */
262    private static List<Node> orderNodesByTrafficHand(List<Node> nodes) {
263        boolean rightHandTraffic = nodes.stream().allMatch(n -> RightAndLefthandTraffic.isRightHandTraffic(n.getCoor()));
264        if (rightHandTraffic == Geometry.isClockwise(nodes)) {
265            Collections.reverse(nodes);
266        }
267        return nodes;
268    }
269
270    /**
271     * Order nodes according to way direction.
272     * @param nodes Nodes list to be ordered.
273     * @param way Way used to determine direction.
274     * @return Modified nodes list with same direction as way.
275     */
276    private static List<Node> orderNodesByWay(List<Node> nodes, Way way) {
277        List<Node> wayNodes = way.getNodes();
278        if (!way.isClosed()) {
279            wayNodes.add(wayNodes.get(0));
280        }
281        if (Geometry.isClockwise(wayNodes) != Geometry.isClockwise(nodes)) {
282            Collections.reverse(nodes);
283        }
284        return nodes;
285    }
286
287    private static void notifyNodesNotOnCircle() {
288        new Notification(
289                tr("Those nodes are not in a circle. Aborting."))
290                .setIcon(JOptionPane.WARNING_MESSAGE)
291                .show();
292    }
293
294    private static void notifyNodesOutsideWorld() {
295        new Notification(tr("Cannot add a node outside of the world."))
296        .setIcon(JOptionPane.WARNING_MESSAGE)
297        .show();
298    }
299
300    @Override
301    protected void updateEnabledState() {
302        updateEnabledStateOnCurrentSelection();
303    }
304
305    @Override
306    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
307        updateEnabledStateOnModifiableSelection(selection);
308    }
309}
Note: See TracBrowser for help on using the repository browser.