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

Last change on this file since 12716 was 12641, checked in by Don-vip, 7 years ago

see #15182 - deprecate Main.main.undoRedo. Replacement: gui.MainApplication.undoRedo

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