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

Last change on this file since 13955 was 13434, checked in by Don-vip, 6 years ago

see #8039, see #10456 - support read-only data layers

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