001 // License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.actions;
003
004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005 import static org.openstreetmap.josm.tools.I18n.tr;
006
007 import java.awt.event.ActionEvent;
008 import java.awt.event.KeyEvent;
009 import java.util.ArrayList;
010 import java.util.Collection;
011 import java.util.LinkedList;
012 import java.util.List;
013
014 import javax.swing.JOptionPane;
015
016 import org.openstreetmap.josm.Main;
017 import org.openstreetmap.josm.command.AddCommand;
018 import org.openstreetmap.josm.command.ChangeCommand;
019 import org.openstreetmap.josm.command.Command;
020 import org.openstreetmap.josm.command.DeleteCommand;
021 import org.openstreetmap.josm.command.SequenceCommand;
022 import org.openstreetmap.josm.data.coor.EastNorth;
023 import org.openstreetmap.josm.data.osm.Node;
024 import org.openstreetmap.josm.data.osm.OsmPrimitive;
025 import org.openstreetmap.josm.data.osm.Way;
026 import org.openstreetmap.josm.tools.Shortcut;
027
028 /**
029 * - Create a new circle from two selected nodes or a way with 2 nodes which represent the diameter of the circle.
030 * - Create a new circle from three selected nodes--or a way with 3 nodes.
031 * - Useful for roundabouts
032 *
033 * Note: If a way is selected, it is changed. If nodes are selected a new way is created.
034 * So if you've got a way with nodes it makes a difference between running this on the way or the nodes!
035 *
036 * BTW: Someone might want to implement projection corrections for this...
037 *
038 * @author Henry Loenwind, based on much copy&Paste from other Actions.
039 * @author Sebastian Masch
040 */
041 public final class CreateCircleAction extends JosmAction {
042
043 public CreateCircleAction() {
044 super(tr("Create Circle"), "createcircle", tr("Create a circle from three selected nodes."),
045 Shortcut.registerShortcut("tools:createcircle", tr("Tool: {0}", tr("Create Circle")),
046 KeyEvent.VK_O, Shortcut.SHIFT), true);
047 putValue("help", ht("/Action/CreateCircle"));
048 }
049
050 private double calcang(double xc, double yc, double x, double y) {
051 // calculate the angle from xc|yc to x|y
052 if (xc == x && yc == y)
053 return 0; // actually invalid, but we won't have this case in this context
054 double yd = Math.abs(y - yc);
055 if (yd == 0 && xc < x)
056 return 0;
057 if (yd == 0 && xc > x)
058 return Math.PI;
059 double xd = Math.abs(x - xc);
060 double a = Math.atan2(xd, yd);
061 if (y > yc) {
062 a = Math.PI - a;
063 }
064 if (x < xc) {
065 a = -a;
066 }
067 a = 1.5*Math.PI + a;
068 if (a < 0) {
069 a += 2*Math.PI;
070 }
071 if (a >= 2*Math.PI) {
072 a -= 2*Math.PI;
073 }
074 return a;
075 }
076
077 public void actionPerformed(ActionEvent e) {
078 if (!isEnabled())
079 return;
080
081 int numberOfNodesInCircle = Main.pref.getInteger("createcircle.nodecount", 8);
082 if (numberOfNodesInCircle < 1) {
083 numberOfNodesInCircle = 1;
084 } else if (numberOfNodesInCircle > 100) {
085 numberOfNodesInCircle = 100;
086 }
087
088 Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
089 List<Node> nodes = new LinkedList<Node>();
090 Way existingWay = null;
091
092 for (OsmPrimitive osm : sel)
093 if (osm instanceof Node) {
094 nodes.add((Node)osm);
095 }
096
097 // special case if no single nodes are selected and exactly one way is:
098 // then use the way's nodes
099 if ((nodes.size() == 0) && (sel.size() == 1)) {
100 for (OsmPrimitive osm : sel)
101 if (osm instanceof Way) {
102 existingWay = ((Way)osm);
103 for (Node n : ((Way)osm).getNodes())
104 {
105 if(!nodes.contains(n)) {
106 nodes.add(n);
107 }
108 }
109 }
110 }
111
112 // now we can start doing things to OSM data
113 Collection<Command> cmds = new LinkedList<Command>();
114
115 if (nodes.size() == 2) {
116 // diameter: two single nodes needed or a way with two nodes
117
118 Node n1 = nodes.get(0);
119 double x1 = n1.getEastNorth().east();
120 double y1 = n1.getEastNorth().north();
121 Node n2 = nodes.get(1);
122 double x2 = n2.getEastNorth().east();
123 double y2 = n2.getEastNorth().north();
124
125 // calculate the center (xc/yc)
126 double xc = 0.5 * (x1 + x2);
127 double yc = 0.5 * (y1 + y2);
128
129 // calculate the radius (r)
130 double r = Math.sqrt(Math.pow(xc-x1,2) + Math.pow(yc-y1,2));
131
132 // find where to put the existing nodes
133 double a1 = calcang(xc, yc, x1, y1);
134 double a2 = calcang(xc, yc, x2, y2);
135 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
136
137 // build a way for the circle
138 List<Node> wayToAdd = new ArrayList<Node>();
139
140 for (int i = 1; i <= numberOfNodesInCircle; i++) {
141 double a = a2 + 2*Math.PI*(1.0 - i/(double)numberOfNodesInCircle); // "1-" to get it clock-wise
142
143 // insert existing nodes if they fit before this new node (999 means "already added this node")
144 if ((a1 < 999) && (a1 > a - 1E-9) && (a1 < a + 1E-9)) {
145 wayToAdd.add(n1);
146 a1 = 999;
147 }
148 else if ((a2 < 999) && (a2 > a - 1E-9) && (a2 < a + 1E-9)) {
149 wayToAdd.add(n2);
150 a2 = 999;
151 }
152 else {
153 // get the position of the new node and insert it
154 double x = xc + r*Math.cos(a);
155 double y = yc + r*Math.sin(a);
156 Node n = new Node(Main.getProjection().eastNorth2latlon(new EastNorth(x,y)));
157 wayToAdd.add(n);
158 cmds.add(new AddCommand(n));
159 }
160 }
161 wayToAdd.add(wayToAdd.get(0)); // close the circle
162 if (existingWay == null) {
163 Way newWay = new Way();
164 newWay.setNodes(wayToAdd);
165 cmds.add(new AddCommand(newWay));
166 } else {
167 Way newWay = new Way(existingWay);
168 newWay.setNodes(wayToAdd);
169 cmds.add(new ChangeCommand(existingWay, newWay));
170 }
171
172 // the first node may be unused/abandoned if createcircle.nodecount is odd
173 if (a1 < 999) {
174 // if it is, delete it
175 List<OsmPrimitive> parents = n1.getReferrers();
176 if (parents.isEmpty() || ((parents.size() == 1) && (parents.contains(existingWay)))) {
177 cmds.add(new DeleteCommand(n1));
178 }
179
180 // or insert it
181 // wayToAdd.nodes.add((numberOfNodesInCircle - 1) / 2, n1);
182 }
183
184 } else if (nodes.size() == 3) {
185 // triangle: three single nodes needed or a way with three nodes
186
187 // let's get some shorter names
188 Node n1 = nodes.get(0);
189 double x1 = n1.getEastNorth().east();
190 double y1 = n1.getEastNorth().north();
191 Node n2 = nodes.get(1);
192 double x2 = n2.getEastNorth().east();
193 double y2 = n2.getEastNorth().north();
194 Node n3 = nodes.get(2);
195 double x3 = n3.getEastNorth().east();
196 double y3 = n3.getEastNorth().north();
197
198 // calculate the center (xc/yc)
199 double s = 0.5*((x2 - x3)*(x1 - x3) - (y2 - y3)*(y3 - y1));
200 double sUnder = (x1 - x2)*(y3 - y1) - (y2 - y1)*(x1 - x3);
201
202 if (sUnder == 0) {
203 JOptionPane.showMessageDialog(
204 Main.parent,
205 tr("Those nodes are not in a circle. Aborting."),
206 tr("Warning"),
207 JOptionPane.WARNING_MESSAGE
208 );
209 return;
210 }
211
212 s /= sUnder;
213
214 double xc = 0.5*(x1 + x2) + s*(y2 - y1);
215 double yc = 0.5*(y1 + y2) + s*(x1 - x2);
216
217 // calculate the radius (r)
218 double r = Math.sqrt(Math.pow(xc-x1,2) + Math.pow(yc-y1,2));
219
220 // find where to put the existing nodes
221 double a1 = calcang(xc, yc, x1, y1);
222 double a2 = calcang(xc, yc, x2, y2);
223 double a3 = calcang(xc, yc, x3, y3);
224 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
225 if (a2 < a3) { double at = a2; Node nt = n2; a2 = a3; n2 = n3; a3 = at; n3 = nt; }
226 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; }
227
228 // build a way for the circle
229 List<Node> wayToAdd = new ArrayList<Node>();
230 for (int i = 1; i <= numberOfNodesInCircle; i++) {
231 double a = 2*Math.PI*(1.0 - i/(double)numberOfNodesInCircle); // "1-" to get it clock-wise
232 // insert existing nodes if they fit before this new node (999 means "already added this node")
233 if (a1 < 999 && a1 > a) {
234 wayToAdd.add(n1);
235 a1 = 999;
236 }
237 if (a2 < 999 && a2 > a) {
238 wayToAdd.add(n2);
239 a2 = 999;
240 }
241 if (a3 < 999 && a3 > a) {
242 wayToAdd.add(n3);
243 a3 = 999;
244 }
245 // get the position of the new node and insert it
246 double x = xc + r*Math.cos(a);
247 double y = yc + r*Math.sin(a);
248 Node n = new Node(Main.getProjection().eastNorth2latlon(new EastNorth(x,y)));
249 wayToAdd.add(n);
250 cmds.add(new AddCommand(n));
251 }
252 wayToAdd.add(wayToAdd.get(0)); // close the circle
253 if (existingWay == null) {
254 Way newWay = new Way();
255 newWay.setNodes(wayToAdd);
256 cmds.add(new AddCommand(newWay));
257 } else {
258 Way newWay = new Way(existingWay);
259 newWay.setNodes(wayToAdd);
260 cmds.add(new ChangeCommand(existingWay, newWay));
261 }
262
263 } else {
264 JOptionPane.showMessageDialog(
265 Main.parent,
266 tr("Please select exactly two or three nodes or one way with exactly two or three nodes."),
267 tr("Information"),
268 JOptionPane.INFORMATION_MESSAGE
269 );
270 return;
271 }
272
273 Main.main.undoRedo.add(new SequenceCommand(tr("Create Circle"), cmds));
274 Main.map.repaint();
275 }
276
277 @Override
278 protected void updateEnabledState() {
279 if (getCurrentDataSet() == null) {
280 setEnabled(false);
281 } else {
282 updateEnabledState(getCurrentDataSet().getSelected());
283 }
284 }
285
286 @Override
287 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
288 setEnabled(selection != null && !selection.isEmpty());
289 }
290 }