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

Last change on this file since 8303 was 8303, checked in by Balaitous, 9 years ago

fix #7421 - Circle created from way heads always clockwise

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