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

Last change on this file since 12855 was 12837, checked in by bastiK, 7 years ago

fixed #10777 - new algorithm to determine the node count in CreateCircleAction (patch by naoliv)

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