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.util.Collection;
010import java.util.HashSet;
011import java.util.Iterator;
012import java.util.LinkedList;
013import java.util.Set;
014
015import javax.swing.JOptionPane;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.command.Command;
019import org.openstreetmap.josm.command.MoveCommand;
020import org.openstreetmap.josm.command.SequenceCommand;
021import org.openstreetmap.josm.data.osm.Node;
022import org.openstreetmap.josm.data.osm.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.Way;
024import org.openstreetmap.josm.gui.Notification;
025import org.openstreetmap.josm.tools.Shortcut;
026
027/**
028 * Distributes the selected nodes to equal distances along a line.
029 *
030 * @author Teemu Koskinen
031 */
032public final class DistributeAction extends JosmAction {
033
034    /**
035     * Constructs a new {@code DistributeAction}.
036     */
037    public DistributeAction() {
038        super(tr("Distribute Nodes"), "distribute", tr("Distribute the selected nodes to equal distances along a line."),
039                Shortcut.registerShortcut("tools:distribute", tr("Tool: {0}", tr("Distribute Nodes")), KeyEvent.VK_B,
040                Shortcut.SHIFT), true);
041        putValue("help", ht("/Action/DistributeNodes"));
042    }
043
044    /**
045     * The general algorithm here is to find the two selected nodes
046     * that are furthest apart, and then to distribute all other selected
047     * nodes along the straight line between these nodes.
048     */
049    @Override
050    public void actionPerformed(ActionEvent e) {
051        if (!isEnabled())
052            return;
053        Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
054        Collection<Node> nodes = new LinkedList<>();
055        Collection<Node> itnodes = new LinkedList<>();
056        for (OsmPrimitive osm : sel)
057            if (osm instanceof Node) {
058                nodes.add((Node)osm);
059                itnodes.add((Node)osm);
060            }
061        // special case if no single nodes are selected and exactly one way is:
062        // then use the way's nodes
063        if (nodes.isEmpty() && (sel.size() == 1)) {
064            for (OsmPrimitive osm : sel)
065                if (osm instanceof Way) {
066                    nodes.addAll(((Way)osm).getNodes());
067                    itnodes.addAll(((Way)osm).getNodes());
068                }
069        }
070
071        Set<Node> ignoredNodes = removeNodesWithoutCoordinates(nodes);
072        ignoredNodes.addAll(removeNodesWithoutCoordinates(itnodes));
073        if (!ignoredNodes.isEmpty()) {
074            Main.warn(tr("Ignoring {0} nodes with null coordinates", ignoredNodes.size()));
075            ignoredNodes.clear();
076        }
077
078        if (nodes.size() < 3) {
079            new Notification(
080                    tr("Please select at least three nodes."))
081                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
082                    .setDuration(Notification.TIME_SHORT)
083                    .show();
084            return;
085        }
086
087        // Find from the selected nodes two that are the furthest apart.
088        // Let's call them A and B.
089        double distance = 0;
090
091        Node nodea = null;
092        Node nodeb = null;
093
094        for (Node n : nodes) {
095            itnodes.remove(n);
096            for (Node m : itnodes) {
097                double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth()));
098                if (dist > distance) {
099                    nodea = n;
100                    nodeb = m;
101                    distance = dist;
102                }
103            }
104        }
105
106        // Remove the nodes A and B from the list of nodes to move
107        nodes.remove(nodea);
108        nodes.remove(nodeb);
109
110        // Find out co-ords of A and B
111        double ax = nodea.getEastNorth().east();
112        double ay = nodea.getEastNorth().north();
113        double bx = nodeb.getEastNorth().east();
114        double by = nodeb.getEastNorth().north();
115
116        // A list of commands to do
117        Collection<Command> cmds = new LinkedList<>();
118
119        // Amount of nodes between A and B plus 1
120        int num = nodes.size()+1;
121
122        // Current number of node
123        int pos = 0;
124        while (!nodes.isEmpty()) {
125            pos++;
126            Node s = null;
127
128            // Find the node that is furthest from B (i.e. closest to A)
129            distance = 0.0;
130            for (Node n : nodes) {
131                double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth()));
132                if (dist > distance) {
133                    s = n;
134                    distance = dist;
135                }
136            }
137
138            // First move the node to A's position, then move it towards B
139            double dx = ax - s.getEastNorth().east() + (bx-ax)*pos/num;
140            double dy = ay - s.getEastNorth().north() + (by-ay)*pos/num;
141
142            cmds.add(new MoveCommand(s, dx, dy));
143
144            //remove moved node from the list
145            nodes.remove(s);
146        }
147
148        // Do it!
149        Main.main.undoRedo.add(new SequenceCommand(tr("Distribute Nodes"), cmds));
150        Main.map.repaint();
151    }
152
153    private Set<Node> removeNodesWithoutCoordinates(Collection<Node> col) {
154        Set<Node> result = new HashSet<>();
155        for (Iterator<Node> it = col.iterator(); it.hasNext();) {
156            Node n = it.next();
157            if (n.getCoor() == null) {
158                it.remove();
159                result.add(n);
160            }
161        }
162        return result;
163    }
164
165    @Override
166    protected void updateEnabledState() {
167        if (getCurrentDataSet() == null) {
168            setEnabled(false);
169        } else {
170            updateEnabledState(getCurrentDataSet().getSelected());
171        }
172    }
173
174    @Override
175    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
176        setEnabled(selection != null && !selection.isEmpty());
177    }
178}