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

Last change on this file since 17335 was 17188, checked in by Klumbumbus, 4 years ago

fix #19851 - Fix shortcut names

  • Property svn:eol-style set to native
File size: 10.6 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.Collections;
12import java.util.LinkedList;
13import java.util.List;
14
15import javax.swing.JOptionPane;
16
17import org.openstreetmap.josm.command.AddCommand;
18import org.openstreetmap.josm.command.ChangeNodesCommand;
19import org.openstreetmap.josm.command.Command;
20import org.openstreetmap.josm.command.SequenceCommand;
21import org.openstreetmap.josm.data.UndoRedoHandler;
22import org.openstreetmap.josm.data.coor.EastNorth;
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.coor.PolarCoor;
25import org.openstreetmap.josm.data.osm.DataSet;
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.data.projection.ProjectionRegistry;
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("Tools: {0}", tr("Create Circle")),
61 KeyEvent.VK_O, Shortcut.SHIFT), true, "createcircle", true);
62 setHelpId(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 static 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 }
93 count[imax]++;
94 remainder[imax] = 0;
95 assign++;
96 }
97 return count;
98 }
99
100 /**
101 * Class designed to create a couple between a node and its angle relative to the center of the circle.
102 */
103 private static class PolarNode implements Comparable<PolarNode> {
104 private final double a;
105 private final Node node;
106
107 PolarNode(EastNorth center, Node n) {
108 this.a = PolarCoor.computeAngle(n.getEastNorth(), center);
109 this.node = n;
110 }
111
112 @Override
113 public int compareTo(PolarNode o) {
114 return Double.compare(a, o.a);
115 }
116 }
117
118 @Override
119 public void actionPerformed(ActionEvent e) {
120 if (!isEnabled())
121 return;
122 runOn(getLayerManager().getEditDataSet());
123 }
124
125 /**
126 * Run the action on the given dataset.
127 * @param ds dataset
128 * @since 14542
129 */
130 public static void runOn(DataSet ds) {
131 List<Node> nodes = new ArrayList<>(ds.getSelectedNodes());
132 Collection<Way> ways = ds.getSelectedWays();
133
134 Way existingWay = null;
135
136 // special case if no single nodes are selected and exactly one way is:
137 // then use the way's nodes
138 if (nodes.isEmpty() && (ways.size() == 1)) {
139 existingWay = ways.iterator().next();
140 for (Node n : existingWay.getNodes()) {
141 if (!nodes.contains(n)) {
142 nodes.add(n);
143 }
144 }
145 }
146
147 if (nodes.size() < 2 || nodes.size() > 3) {
148 new Notification(
149 tr("Please select exactly two or three nodes or one way with exactly two or three nodes."))
150 .setIcon(JOptionPane.INFORMATION_MESSAGE)
151 .setDuration(Notification.TIME_LONG)
152 .show();
153 return;
154 }
155
156 EastNorth center;
157
158 if (nodes.size() == 2) {
159 // diameter: two single nodes needed or a way with two nodes
160 EastNorth n1 = nodes.get(0).getEastNorth();
161 EastNorth n2 = nodes.get(1).getEastNorth();
162
163 center = n1.getCenter(n2);
164 } else {
165 // triangle: three single nodes needed or a way with three nodes
166 center = Geometry.getCenter(nodes);
167 if (center == null) {
168 notifyNodesNotOnCircle();
169 return;
170 }
171 }
172
173 // calculate the radius (r)
174 EastNorth n1 = nodes.get(0).getEastNorth();
175 double r = n1.distance(center);
176
177 // see #10777
178 LatLon ll1 = ProjectionRegistry.getProjection().eastNorth2latlon(n1);
179 LatLon ll2 = ProjectionRegistry.getProjection().eastNorth2latlon(center);
180
181 double radiusInMeters = ll1.greatCircleDistance(ll2);
182
183 int numberOfNodesInCircle = (int) Math.ceil(6.0 * Math.pow(radiusInMeters, 0.5));
184 // an odd number of nodes makes the distribution uneven
185 if ((numberOfNodesInCircle % 2) != 0) {
186 // add 1 to make it even
187 numberOfNodesInCircle += 1;
188 }
189 if (numberOfNodesInCircle < 6) {
190 numberOfNodesInCircle = 6;
191 }
192
193 // Order nodes by angle
194 final PolarNode[] angles = nodes.stream()
195 .map(n -> new PolarNode(center, n))
196 .sorted()
197 .toArray(PolarNode[]::new);
198 int[] count = distributeNodes(angles,
199 numberOfNodesInCircle >= nodes.size() ? (numberOfNodesInCircle - nodes.size()) : 0);
200
201 // now we can start doing things to OSM data
202 Collection<Command> cmds = new LinkedList<>();
203
204 // build a way for the circle
205 List<Node> nodesToAdd = new ArrayList<>();
206 for (int i = 0; i < nodes.size(); i++) {
207 nodesToAdd.add(angles[i].node);
208 double delta = angles[(i+1) % nodes.size()].a - angles[i].a;
209 if (delta < 0)
210 delta += 2*Math.PI;
211 for (int j = 0; j < count[i]; j++) {
212 double alpha = angles[i].a + (j+1)*delta/(count[i]+1);
213 double x = center.east() + r*Math.cos(alpha);
214 double y = center.north() + r*Math.sin(alpha);
215 LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(new EastNorth(x, y));
216 if (new Node(new EastNorth(x, y)).isOutSideWorld()) {
217 notifyNodesOutsideWorld();
218 return;
219 }
220 Node n = new Node(ll);
221 nodesToAdd.add(n);
222 cmds.add(new AddCommand(ds, n));
223 }
224 }
225 nodesToAdd.add(nodesToAdd.get(0)); // close the circle
226 if (existingWay != null && existingWay.getNodesCount() >= 3) {
227 nodesToAdd = orderNodesByWay(nodesToAdd, existingWay);
228 } else {
229 nodesToAdd = orderNodesByTrafficHand(nodesToAdd);
230 }
231 if (existingWay == null) {
232 Way newWay = new Way();
233 newWay.setNodes(nodesToAdd);
234 cmds.add(new AddCommand(ds, newWay));
235 } else {
236 cmds.add(new ChangeNodesCommand(ds, existingWay, nodesToAdd));
237 }
238
239 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create Circle"), cmds));
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 static List<Node> orderNodesByTrafficHand(List<Node> nodes) {
248 boolean rightHandTraffic = nodes.stream().allMatch(n -> RightAndLefthandTraffic.isRightHandTraffic(n.getCoor()));
249 if (rightHandTraffic == Geometry.isClockwise(nodes)) {
250 Collections.reverse(nodes);
251 }
252 return nodes;
253 }
254
255 /**
256 * Order nodes according to way direction.
257 * @param nodes Nodes list to be ordered.
258 * @param way Way used to determine direction.
259 * @return Modified nodes list with same direction as way.
260 */
261 private static List<Node> orderNodesByWay(List<Node> nodes, Way way) {
262 List<Node> wayNodes = way.getNodes();
263 if (!way.isClosed()) {
264 wayNodes.add(wayNodes.get(0));
265 }
266 if (Geometry.isClockwise(wayNodes) != Geometry.isClockwise(nodes)) {
267 Collections.reverse(nodes);
268 }
269 return nodes;
270 }
271
272 private static void notifyNodesNotOnCircle() {
273 new Notification(
274 tr("Those nodes are not in a circle. Aborting."))
275 .setIcon(JOptionPane.WARNING_MESSAGE)
276 .show();
277 }
278
279 private static void notifyNodesOutsideWorld() {
280 new Notification(tr("Cannot add a node outside of the world."))
281 .setIcon(JOptionPane.WARNING_MESSAGE)
282 .show();
283 }
284
285 @Override
286 protected void updateEnabledState() {
287 updateEnabledStateOnCurrentSelection();
288 }
289
290 @Override
291 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
292 updateEnabledStateOnModifiableSelection(selection);
293 }
294}
Note: See TracBrowser for help on using the repository browser.