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

Last change on this file since 8447 was 8419, checked in by Don-vip, 9 years ago

Sonar: various code style cleanup:

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