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

Last change on this file was 18494, checked in by taylor.smock, 23 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.