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

Last change on this file since 11187 was 10467, checked in by Don-vip, 8 years ago

fix #13037 - Small fixes for unit tests (patch by michael2402) - gsoc-core

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