001//License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.event.ActionEvent;
008import java.awt.event.KeyEvent;
009import java.math.BigDecimal;
010import java.math.MathContext;
011import java.util.Collection;
012import java.util.HashSet;
013import java.util.LinkedList;
014import java.util.List;
015import java.util.Set;
016
017import javax.swing.JOptionPane;
018
019import org.openstreetmap.josm.Main;
020import org.openstreetmap.josm.command.Command;
021import org.openstreetmap.josm.command.MoveCommand;
022import org.openstreetmap.josm.command.SequenceCommand;
023import org.openstreetmap.josm.data.coor.EastNorth;
024import org.openstreetmap.josm.data.osm.Node;
025import org.openstreetmap.josm.data.osm.OsmPrimitive;
026import org.openstreetmap.josm.data.osm.Way;
027import org.openstreetmap.josm.gui.Notification;
028import org.openstreetmap.josm.tools.Geometry;
029import org.openstreetmap.josm.tools.Shortcut;
030
031/**
032 * Aligns all selected nodes within a circle. (Useful for roundabouts)
033 *
034 * @author Matthew Newton
035 * @author Petr DlouhĂ˝
036 * @author Teemu Koskinen
037 */
038public final class AlignInCircleAction extends JosmAction {
039
040    /**
041     * Constructs a new {@code AlignInCircleAction}.
042     */
043    public AlignInCircleAction() {
044        super(tr("Align Nodes in Circle"), "aligncircle", tr("Move the selected nodes into a circle."),
045                Shortcut.registerShortcut("tools:aligncircle", tr("Tool: {0}", tr("Align Nodes in Circle")),
046                        KeyEvent.VK_O, Shortcut.DIRECT), true);
047        putValue("help", ht("/Action/AlignInCircle"));
048    }
049
050    public double distance(EastNorth n, EastNorth m) {
051        double easd, nord;
052        easd = n.east() - m.east();
053        nord = n.north() - m.north();
054        return Math.sqrt(easd * easd + nord * nord);
055    }
056
057    public class PolarCoor {
058        double radius;
059        double angle;
060        EastNorth origin = new EastNorth(0, 0);
061        double azimuth = 0;
062
063        PolarCoor(double radius, double angle) {
064            this(radius, angle, new EastNorth(0, 0), 0);
065        }
066
067        PolarCoor(double radius, double angle, EastNorth origin, double azimuth) {
068            this.radius = radius;
069            this.angle = angle;
070            this.origin = origin;
071            this.azimuth = azimuth;
072        }
073
074        PolarCoor(EastNorth en) {
075            this(en, new EastNorth(0, 0), 0);
076        }
077
078        PolarCoor(EastNorth en, EastNorth origin, double azimuth) {
079            radius = distance(en, origin);
080            angle = Math.atan2(en.north() - origin.north(), en.east() - origin.east());
081            this.origin = origin;
082            this.azimuth = azimuth;
083        }
084
085        public EastNorth toEastNorth() {
086            return new EastNorth(radius * Math.cos(angle - azimuth) + origin.east(), radius * Math.sin(angle - azimuth)
087                    + origin.north());
088        }
089    }
090
091    @Override
092    public void actionPerformed(ActionEvent e) {
093        if (!isEnabled())
094            return;
095
096        Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
097        List<Node> nodes = new LinkedList<Node>();
098        List<Way> ways = new LinkedList<Way>();
099        EastNorth center = null;
100        double radius = 0;
101        boolean regular = false;
102
103        for (OsmPrimitive osm : sel) {
104            if (osm instanceof Node) {
105                nodes.add((Node) osm);
106            } else if (osm instanceof Way) {
107                ways.add((Way) osm);
108            }
109        }
110
111        // special case if no single nodes are selected and exactly one way is:
112        // then use the way's nodes
113        if ((nodes.size() <= 2) && (ways.size() == 1)) {
114            Way way = ways.get(0);
115
116            // some more special combinations:
117            // When is selected node that is part of the way, then make a regular polygon, selected
118            // node doesn't move.
119            // I haven't got better idea, how to activate that function.
120            //
121            // When one way and one node is selected, set center to position of that node.
122            // When one more node, part of the way, is selected, set the radius equal to the
123            // distance between two nodes.
124            if (nodes.size() > 0) {
125                if (nodes.size() == 1 && way.containsNode(nodes.get(0)) && allowRegularPolygon(way.getNodes())) {
126                    regular = true;
127                } else if (nodes.size() >= 2) {
128                    center = nodes.get(way.containsNode(nodes.get(0)) ? 1 : 0).getEastNorth();
129                    if (nodes.size() == 2) {
130                        radius = distance(nodes.get(0).getEastNorth(), nodes.get(1).getEastNorth());
131                    }
132                }
133                nodes.clear();
134            }
135
136            for (Node n : way.getNodes()) {
137                if (!nodes.contains(n)) {
138                    nodes.add(n);
139                }
140            }
141        }
142
143        if (nodes.size() < 4) {
144            new Notification(
145                    tr("Please select at least four nodes."))
146                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
147                    .setDuration(Notification.TIME_SHORT)
148                    .show();
149            return;
150        }
151
152        // Reorder the nodes if they didn't come from a single way
153        if (ways.size() != 1) {
154            // First calculate the average point
155
156            BigDecimal east = BigDecimal.ZERO;
157            BigDecimal north = BigDecimal.ZERO;
158
159            for (Node n : nodes) {
160                BigDecimal x = new BigDecimal(n.getEastNorth().east());
161                BigDecimal y = new BigDecimal(n.getEastNorth().north());
162                east = east.add(x, MathContext.DECIMAL128);
163                north = north.add(y, MathContext.DECIMAL128);
164            }
165            BigDecimal nodesSize = new BigDecimal(nodes.size());
166            east = east.divide(nodesSize, MathContext.DECIMAL128);
167            north = north.divide(nodesSize, MathContext.DECIMAL128);
168
169            EastNorth average = new EastNorth(east.doubleValue(), north.doubleValue());
170            List<Node> newNodes = new LinkedList<Node>();
171
172            // Then reorder them based on heading from the average point
173            while (!nodes.isEmpty()) {
174                double maxHeading = -1.0;
175                Node maxNode = null;
176                for (Node n : nodes) {
177                    double heading = average.heading(n.getEastNorth());
178                    if (heading > maxHeading) {
179                        maxHeading = heading;
180                        maxNode = n;
181                    }
182                }
183                newNodes.add(maxNode);
184                nodes.remove(maxNode);
185            }
186
187            nodes = newNodes;
188        }
189
190        if (center == null) {
191            // Compute the centroid of nodes
192            center = Geometry.getCentroid(nodes);
193        }
194        // Node "center" now is central to all selected nodes.
195
196        // Now calculate the average distance to each node from the
197        // centre. This method is ok as long as distances are short
198        // relative to the distance from the N or S poles.
199        if (radius == 0) {
200            for (Node n : nodes) {
201                radius += distance(center, n.getEastNorth());
202            }
203            radius = radius / nodes.size();
204        }
205
206        Collection<Command> cmds = new LinkedList<Command>();
207
208        PolarCoor pc;
209
210        if (regular) { // Make a regular polygon
211            double angle = Math.PI * 2 / nodes.size();
212            pc = new PolarCoor(nodes.get(0).getEastNorth(), center, 0);
213
214            if (pc.angle > (new PolarCoor(nodes.get(1).getEastNorth(), center, 0).angle)) {
215                angle *= -1;
216            }
217
218            pc.radius = radius;
219            for (Node n : nodes) {
220                EastNorth no = pc.toEastNorth();
221                cmds.add(new MoveCommand(n, no.east() - n.getEastNorth().east(), no.north() - n.getEastNorth().north()));
222                pc.angle += angle;
223            }
224        } else { // Move each node to that distance from the centre.
225            for (Node n : nodes) {
226                pc = new PolarCoor(n.getEastNorth(), center, 0);
227                pc.radius = radius;
228                EastNorth no = pc.toEastNorth();
229                cmds.add(new MoveCommand(n, no.east() - n.getEastNorth().east(), no.north() - n.getEastNorth().north()));
230            }
231        }
232
233        Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Circle"), cmds));
234        Main.map.repaint();
235    }
236
237    @Override
238    protected void updateEnabledState() {
239        setEnabled(getCurrentDataSet() != null && !getCurrentDataSet().getSelected().isEmpty());
240    }
241
242    @Override
243    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
244        setEnabled(selection != null && !selection.isEmpty());
245    }
246
247    /**
248     * Determines if a regular polygon is allowed to be created with the given nodes collection.
249     * @param nodes The nodes collection to check.
250     * @return true if all nodes in the given collection are referred by the same object, and no other one (see #8431)
251     */
252    protected boolean allowRegularPolygon(Collection<Node> nodes) {
253        Set<OsmPrimitive> allReferrers = new HashSet<OsmPrimitive>();
254        for (Node n : nodes) {
255            List<OsmPrimitive> referrers = n.getReferrers();
256            if (referrers.size() > 1 || (allReferrers.addAll(referrers) && allReferrers.size() > 1)) {
257                return false;
258            }
259        }
260        return true;
261    }
262}