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.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.util.Collection;
011import java.util.HashSet;
012import java.util.Iterator;
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.osm.Node;
024import org.openstreetmap.josm.data.osm.OsmPrimitive;
025import org.openstreetmap.josm.data.osm.Way;
026import org.openstreetmap.josm.gui.Notification;
027import org.openstreetmap.josm.tools.Shortcut;
028
029/**
030 * Distributes the selected nodes to equal distances along a line.
031 *
032 * @author Teemu Koskinen
033 */
034public final class DistributeAction extends JosmAction {
035
036    private static final String ACTION_SHORT_NAME = marktr("Distribute Nodes");
037
038    /**
039     * Constructs a new {@code DistributeAction}.
040     */
041    public DistributeAction() {
042        super(tr(ACTION_SHORT_NAME), "distribute",
043              tr("Distribute the selected nodes to equal distances along a line."),
044              Shortcut.registerShortcut("tools:distribute",
045                                        tr("Tool: {0}", tr(ACTION_SHORT_NAME)),
046                                        KeyEvent.VK_B, Shortcut.SHIFT),
047              true);
048        putValue("help", ht("/Action/DistributeNodes"));
049    }
050
051    /**
052     * Perform action.
053     * Select method according to user selection.
054     * Case 1: One Way (no self-crossing) and at most 2 nodes contains by this way:
055     *     Distribute nodes keeping order along the way
056     * Case 2: Other
057     *     Distribute nodes
058     */
059    @Override
060    public void actionPerformed(ActionEvent e) {
061        if (!isEnabled())
062            return;
063
064        // Collect user selected objects
065        Collection<OsmPrimitive> selected = getCurrentDataSet().getSelected();
066        Collection<Way> ways = new LinkedList<>();
067        Collection<Node> nodes = new HashSet<>();
068        for (OsmPrimitive osm : selected) {
069            if (osm instanceof Node) {
070                nodes.add((Node) osm);
071            } else if (osm instanceof Way) {
072                ways.add((Way) osm);
073            }
074        }
075
076        Set<Node> ignoredNodes = removeNodesWithoutCoordinates(nodes);
077        if (!ignoredNodes.isEmpty()) {
078            Main.warn(tr("Ignoring {0} nodes with null coordinates", ignoredNodes.size()));
079            ignoredNodes.clear();
080        }
081
082        // Switch between algorithms
083        Collection<Command> cmds;
084        if (checkDistributeWay(ways, nodes)) {
085            cmds = distributeWay(ways, nodes);
086        } else if (checkDistributeNodes(ways, nodes)) {
087            cmds = distributeNodes(nodes);
088        } else {
089            new Notification(
090                             tr("Please select :\n" +
091                                "* One no self-crossing way with at most two of its nodes;\n" +
092                                "* Three nodes."))
093                .setIcon(JOptionPane.INFORMATION_MESSAGE)
094                .setDuration(Notification.TIME_SHORT)
095                .show();
096            return;
097        }
098
099        if (cmds.isEmpty()) {
100            return;
101        }
102
103        // Do it!
104        Main.main.undoRedo.add(new SequenceCommand(tr(ACTION_SHORT_NAME), cmds));
105        Main.map.repaint();
106    }
107
108    /**
109     * Test if one way, no self-crossing, is selected with at most two of its nodes.
110     * @param ways Selected ways
111     * @param nodes Selected nodes
112     * @return true in this case
113     */
114    private static boolean checkDistributeWay(Collection<Way> ways, Collection<Node> nodes) {
115        if (ways.size() == 1 && nodes.size() <= 2) {
116            Way w = ways.iterator().next();
117            Set<Node> unduplicated = new HashSet<>(w.getNodes());
118            if (unduplicated.size() != w.getNodesCount()) {
119                // No self crossing way
120                return false;
121            }
122            for (Node node: nodes) {
123                if (!w.containsNode(node)) {
124                    return false;
125                }
126            }
127            return true;
128        }
129        return false;
130    }
131
132    /**
133     * Distribute nodes contained by a way, keeping nodes order.
134     * If one or two nodes are selected, keep these nodes in place.
135     * @param ways Selected ways, must be collection of size 1.
136     * @param nodes Selected nodes, at most two nodes.
137     * @return Collection of command to be executed.
138     */
139    private static Collection<Command> distributeWay(Collection<Way> ways,
140                                              Collection<Node> nodes) {
141        Way w = ways.iterator().next();
142        Collection<Command> cmds = new LinkedList<>();
143
144        if (w.getNodesCount() == nodes.size() || w.getNodesCount() <= 2) {
145            // Nothing to do
146            return cmds;
147        }
148
149        double xa, ya; // Start point
150        double dx, dy; // Segment increment
151        if (nodes.isEmpty()) {
152            Node na = w.firstNode();
153            nodes.add(na);
154            Node nb = w.lastNode();
155            nodes.add(nb);
156            xa = na.getEastNorth().east();
157            ya = na.getEastNorth().north();
158            dx = (nb.getEastNorth().east() - xa) / (w.getNodesCount() - 1);
159            dy = (nb.getEastNorth().north() - ya) / (w.getNodesCount() - 1);
160        } else if (nodes.size() == 1) {
161            Node n = nodes.iterator().next();
162            int nIdx = w.getNodes().indexOf(n);
163            Node na = w.firstNode();
164            Node nb = w.lastNode();
165            dx = (nb.getEastNorth().east() - na.getEastNorth().east()) /
166                (w.getNodesCount() - 1);
167            dy = (nb.getEastNorth().north() - na.getEastNorth().north()) /
168                (w.getNodesCount() - 1);
169            xa = n.getEastNorth().east() - dx * nIdx;
170            ya = n.getEastNorth().north() - dy * nIdx;
171        } else {
172            Iterator<Node> it = nodes.iterator();
173            Node na = it.next();
174            Node nb = it.next();
175            List<Node> wayNodes = w.getNodes();
176            int naIdx = wayNodes.indexOf(na);
177            int nbIdx = wayNodes.indexOf(nb);
178            dx = (nb.getEastNorth().east() - na.getEastNorth().east()) / (nbIdx - naIdx);
179            dy = (nb.getEastNorth().north() - na.getEastNorth().north()) / (nbIdx - naIdx);
180            xa = na.getEastNorth().east() - dx * naIdx;
181            ya = na.getEastNorth().north() - dy * naIdx;
182        }
183
184        for (int i = 0; i < w.getNodesCount(); i++) {
185            Node n = w.getNode(i);
186            if (!n.isLatLonKnown() || nodes.contains(n)) {
187                continue;
188            }
189            double x = xa + i * dx;
190            double y = ya + i * dy;
191            cmds.add(new MoveCommand(n, x - n.getEastNorth().east(),
192                                     y - n.getEastNorth().north()));
193        }
194        return cmds;
195    }
196
197    /**
198     * Test if nodes oriented algorithm applies to the selection.
199     * @param ways Selected ways
200     * @param nodes Selected nodes
201     * @return true in this case
202     */
203    private static Boolean checkDistributeNodes(Collection<Way> ways, Collection<Node> nodes) {
204        return ways.isEmpty() && nodes.size() >= 3;
205    }
206
207    /**
208     * Distribute nodes when only nodes are selected.
209     * The general algorithm here is to find the two selected nodes
210     * that are furthest apart, and then to distribute all other selected
211     * nodes along the straight line between these nodes.
212     * @param nodes nodes to distribute
213     * @return Commands to execute to perform action
214     */
215    private static Collection<Command> distributeNodes(Collection<Node> nodes) {
216        // Find from the selected nodes two that are the furthest apart.
217        // Let's call them A and B.
218        double distance = 0;
219
220        Node nodea = null;
221        Node nodeb = null;
222
223        Collection<Node> itnodes = new LinkedList<>(nodes);
224        for (Node n : nodes) {
225            itnodes.remove(n);
226            for (Node m : itnodes) {
227                double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth()));
228                if (dist > distance) {
229                    nodea = n;
230                    nodeb = m;
231                    distance = dist;
232                }
233            }
234        }
235
236        // Remove the nodes A and B from the list of nodes to move
237        nodes.remove(nodea);
238        nodes.remove(nodeb);
239
240        // Find out co-ords of A and B
241        double ax = nodea.getEastNorth().east();
242        double ay = nodea.getEastNorth().north();
243        double bx = nodeb.getEastNorth().east();
244        double by = nodeb.getEastNorth().north();
245
246        // A list of commands to do
247        Collection<Command> cmds = new LinkedList<>();
248
249        // Amount of nodes between A and B plus 1
250        int num = nodes.size()+1;
251
252        // Current number of node
253        int pos = 0;
254        while (!nodes.isEmpty()) {
255            pos++;
256            Node s = null;
257
258            // Find the node that is furthest from B (i.e. closest to A)
259            distance = 0.0;
260            for (Node n : nodes) {
261                double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth()));
262                if (dist > distance) {
263                    s = n;
264                    distance = dist;
265                }
266            }
267
268            // First move the node to A's position, then move it towards B
269            double dx = ax - s.getEastNorth().east() + (bx-ax)*pos/num;
270            double dy = ay - s.getEastNorth().north() + (by-ay)*pos/num;
271
272            cmds.add(new MoveCommand(s, dx, dy));
273
274            //remove moved node from the list
275            nodes.remove(s);
276        }
277
278        return cmds;
279    }
280
281    /**
282     * Remove nodes without knowned coordinates from a collection.
283     * @param col Collection of nodes to check
284     * @return Set of nodes without coordinates
285     */
286    private static Set<Node> removeNodesWithoutCoordinates(Collection<Node> col) {
287        Set<Node> result = new HashSet<>();
288        for (Iterator<Node> it = col.iterator(); it.hasNext();) {
289            Node n = it.next();
290            if (!n.isLatLonKnown()) {
291                it.remove();
292                result.add(n);
293            }
294        }
295        return result;
296    }
297
298    @Override
299    protected void updateEnabledState() {
300        if (getCurrentDataSet() == null) {
301            setEnabled(false);
302        } else {
303            updateEnabledState(getCurrentDataSet().getSelected());
304        }
305    }
306
307    @Override
308    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
309        setEnabled(selection != null && !selection.isEmpty());
310    }
311}