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

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

fix #10168 - double default value of createcircle.nodecount from 8 to 16

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