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

Last change on this file since 6742 was 6672, checked in by Don-vip, 10 years ago

fix #9565 - "create circle" creates enormous circles of ways with three nodes

  • Property svn:eol-style set to native
File size: 11.2 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.LinkedList;
12import java.util.List;
13
14import javax.swing.JOptionPane;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.command.AddCommand;
18import org.openstreetmap.josm.command.ChangeCommand;
19import org.openstreetmap.josm.command.Command;
20import org.openstreetmap.josm.command.DeleteCommand;
21import org.openstreetmap.josm.command.SequenceCommand;
22import org.openstreetmap.josm.data.coor.EastNorth;
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Way;
27import org.openstreetmap.josm.gui.Notification;
28import org.openstreetmap.josm.tools.Shortcut;
29
30/**
31 * - Create a new circle from two selected nodes or a way with 2 nodes which represent the diameter of the circle.
32 * - Create a new circle from three selected nodes--or a way with 3 nodes.
33 * - Useful for roundabouts
34 *
35 * Note: If a way is selected, it is changed. If nodes are selected a new way is created.
36 * So if you've got a way with nodes it makes a difference between running this on the way or the nodes!
37 *
38 * BTW: Someone might want to implement projection corrections for this...
39 *
40 * @author Henry Loenwind, based on much copy&Paste from other Actions.
41 * @author Sebastian Masch
42 */
43public final class CreateCircleAction extends JosmAction {
44
45 /**
46 * Constructs a new {@code CreateCircleAction}.
47 */
48 public CreateCircleAction() {
49 super(tr("Create Circle"), "createcircle", tr("Create a circle from three selected nodes."),
50 Shortcut.registerShortcut("tools:createcircle", tr("Tool: {0}", tr("Create Circle")),
51 KeyEvent.VK_O, Shortcut.SHIFT), true);
52 putValue("help", ht("/Action/CreateCircle"));
53 }
54
55 private double calcang(double xc, double yc, double x, double y) {
56 // calculate the angle from xc|yc to x|y
57 if (xc == x && yc == y)
58 return 0; // actually invalid, but we won't have this case in this context
59 double yd = Math.abs(y - yc);
60 if (yd == 0 && xc < x)
61 return 0;
62 if (yd == 0 && xc > x)
63 return Math.PI;
64 double xd = Math.abs(x - xc);
65 double a = Math.atan2(xd, yd);
66 if (y > yc) {
67 a = Math.PI - a;
68 }
69 if (x < xc) {
70 a = -a;
71 }
72 a = 1.5*Math.PI + a;
73 if (a < 0) {
74 a += 2*Math.PI;
75 }
76 if (a >= 2*Math.PI) {
77 a -= 2*Math.PI;
78 }
79 return a;
80 }
81
82 @Override
83 public void actionPerformed(ActionEvent e) {
84 if (!isEnabled())
85 return;
86
87 int numberOfNodesInCircle = Main.pref.getInteger("createcircle.nodecount", 8);
88 if (numberOfNodesInCircle < 1) {
89 numberOfNodesInCircle = 1;
90 } else if (numberOfNodesInCircle > 100) {
91 numberOfNodesInCircle = 100;
92 }
93
94 Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
95 List<Node> nodes = new LinkedList<Node>();
96 Way existingWay = null;
97
98 for (OsmPrimitive osm : sel)
99 if (osm instanceof Node) {
100 nodes.add((Node)osm);
101 }
102
103 // special case if no single nodes are selected and exactly one way is:
104 // then use the way's nodes
105 if (nodes.isEmpty() && (sel.size() == 1)) {
106 for (OsmPrimitive osm : sel)
107 if (osm instanceof Way) {
108 existingWay = ((Way)osm);
109 for (Node n : ((Way)osm).getNodes())
110 {
111 if(!nodes.contains(n)) {
112 nodes.add(n);
113 }
114 }
115 }
116 }
117
118 // now we can start doing things to OSM data
119 Collection<Command> cmds = new LinkedList<Command>();
120
121 if (nodes.size() == 2) {
122 // diameter: two single nodes needed or a way with two nodes
123
124 Node n1 = nodes.get(0);
125 double x1 = n1.getEastNorth().east();
126 double y1 = n1.getEastNorth().north();
127 Node n2 = nodes.get(1);
128 double x2 = n2.getEastNorth().east();
129 double y2 = n2.getEastNorth().north();
130
131 // calculate the center (xc/yc)
132 double xc = 0.5 * (x1 + x2);
133 double yc = 0.5 * (y1 + y2);
134
135 // calculate the radius (r)
136 double r = Math.sqrt(Math.pow(xc-x1,2) + Math.pow(yc-y1,2));
137
138 // find where to put the existing nodes
139 double a1 = calcang(xc, yc, x1, y1);
140 double a2 = calcang(xc, yc, x2, y2);
141 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
142
143 // build a way for the circle
144 List<Node> wayToAdd = new ArrayList<Node>(numberOfNodesInCircle + 1);
145
146 for (int i = 1; i <= numberOfNodesInCircle; i++) {
147 double a = a2 + 2*Math.PI*(1.0 - i/(double)numberOfNodesInCircle); // "1-" to get it clock-wise
148
149 // insert existing nodes if they fit before this new node (999 means "already added this node")
150 if ((a1 < 999) && (a1 > a - 1E-9) && (a1 < a + 1E-9)) {
151 wayToAdd.add(n1);
152 a1 = 999;
153 }
154 else if ((a2 < 999) && (a2 > a - 1E-9) && (a2 < a + 1E-9)) {
155 wayToAdd.add(n2);
156 a2 = 999;
157 }
158 else {
159 // get the position of the new node and insert it
160 double x = xc + r*Math.cos(a);
161 double y = yc + r*Math.sin(a);
162 Node n = new Node(Main.getProjection().eastNorth2latlon(new EastNorth(x,y)));
163 wayToAdd.add(n);
164 cmds.add(new AddCommand(n));
165 }
166 }
167 wayToAdd.add(wayToAdd.get(0)); // close the circle
168 if (existingWay == null) {
169 Way newWay = new Way();
170 newWay.setNodes(wayToAdd);
171 cmds.add(new AddCommand(newWay));
172 } else {
173 Way newWay = new Way(existingWay);
174 newWay.setNodes(wayToAdd);
175 cmds.add(new ChangeCommand(existingWay, newWay));
176 }
177
178 // the first node may be unused/abandoned if createcircle.nodecount is odd
179 if (a1 < 999) {
180 // if it is, delete it
181 List<OsmPrimitive> parents = n1.getReferrers();
182 if (parents.isEmpty() || ((parents.size() == 1) && (parents.contains(existingWay)))) {
183 cmds.add(new DeleteCommand(n1));
184 }
185 }
186
187 } else if (nodes.size() == 3) {
188 // triangle: three single nodes needed or a way with three nodes
189
190 // let's get some shorter names
191 Node n1 = nodes.get(0);
192 double x1 = n1.getEastNorth().east();
193 double y1 = n1.getEastNorth().north();
194 Node n2 = nodes.get(1);
195 double x2 = n2.getEastNorth().east();
196 double y2 = n2.getEastNorth().north();
197 Node n3 = nodes.get(2);
198 double x3 = n3.getEastNorth().east();
199 double y3 = n3.getEastNorth().north();
200
201 // calculate the center (xc/yc)
202 double s = 0.5*((x2 - x3)*(x1 - x3) - (y2 - y3)*(y3 - y1));
203 double sUnder = (x1 - x2)*(y3 - y1) - (y2 - y1)*(x1 - x3);
204
205 if (sUnder == 0) {
206 notifyNodesNotOnCircle();
207 return;
208 }
209
210 s /= sUnder;
211
212 double xc = 0.5*(x1 + x2) + s*(y2 - y1);
213 double yc = 0.5*(y1 + y2) + s*(x1 - x2);
214
215 // calculate the radius (r)
216 double r = Math.sqrt(Math.pow(xc-x1,2) + Math.pow(yc-y1,2));
217
218 // find where to put the existing nodes
219 double a1 = calcang(xc, yc, x1, y1);
220 double a2 = calcang(xc, yc, x2, y2);
221 double a3 = calcang(xc, yc, x3, y3);
222 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
223 if (a2 < a3) { double at = a2; Node nt = n2; a2 = a3; n2 = n3; a3 = at; n3 = nt; }
224 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
225
226 // build a way for the circle
227 List<Node> wayToAdd = new ArrayList<Node>();
228 for (int i = 1; i <= numberOfNodesInCircle; i++) {
229 double a = 2*Math.PI*(1.0 - i/(double)numberOfNodesInCircle); // "1-" to get it clock-wise
230 // insert existing nodes if they fit before this new node (999 means "already added this node")
231 if (a1 < 999 && a1 > a) {
232 wayToAdd.add(n1);
233 a1 = 999;
234 }
235 if (a2 < 999 && a2 > a) {
236 wayToAdd.add(n2);
237 a2 = 999;
238 }
239 if (a3 < 999 && a3 > a) {
240 wayToAdd.add(n3);
241 a3 = 999;
242 }
243 // get the position of the new node and insert it
244 double x = xc + r*Math.cos(a);
245 double y = yc + r*Math.sin(a);
246 LatLon ll = Main.getProjection().eastNorth2latlon(new EastNorth(x,y));
247 if (ll.isOutSideWorld()) {
248 notifyNodesNotOnCircle();
249 return;
250 }
251 Node n = new Node(ll);
252 wayToAdd.add(n);
253 cmds.add(new AddCommand(n));
254 }
255 wayToAdd.add(wayToAdd.get(0)); // close the circle
256 if (existingWay == null) {
257 Way newWay = new Way();
258 newWay.setNodes(wayToAdd);
259 cmds.add(new AddCommand(newWay));
260 } else {
261 Way newWay = new Way(existingWay);
262 newWay.setNodes(wayToAdd);
263 cmds.add(new ChangeCommand(existingWay, newWay));
264 }
265
266 } else {
267 new Notification(
268 tr("Please select exactly two or three nodes or one way with exactly two or three nodes."))
269 .setIcon(JOptionPane.INFORMATION_MESSAGE)
270 .setDuration(Notification.TIME_LONG)
271 .show();
272 return;
273 }
274
275 Main.main.undoRedo.add(new SequenceCommand(tr("Create Circle"), cmds));
276 Main.map.repaint();
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.