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    }