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.ArrayList;
010import java.util.Collection;
011import java.util.List;
012
013import javax.swing.JOptionPane;
014
015import org.openstreetmap.josm.Main;
016import org.openstreetmap.josm.command.Command;
017import org.openstreetmap.josm.command.MoveCommand;
018import org.openstreetmap.josm.command.SequenceCommand;
019import org.openstreetmap.josm.data.osm.Node;
020import org.openstreetmap.josm.data.osm.OsmPrimitive;
021import org.openstreetmap.josm.data.osm.Way;
022import org.openstreetmap.josm.gui.Notification;
023import org.openstreetmap.josm.tools.Shortcut;
024
025/**
026 * Aligns all selected nodes into a straight line (useful for
027 * roads that should be straight, but have side roads and
028 * therefore need multiple nodes)
029 *
030 * @author Matthew Newton
031 */
032public final class AlignInLineAction extends JosmAction {
033
034    /**
035     * Constructs a new {@code AlignInLineAction}.
036     */
037    public AlignInLineAction() {
038        super(tr("Align Nodes in Line"), "alignline", tr("Move the selected nodes in to a line."),
039                Shortcut.registerShortcut("tools:alignline", tr("Tool: {0}", tr("Align Nodes in Line")), KeyEvent.VK_L, Shortcut.DIRECT), true);
040        putValue("help", ht("/Action/AlignInLine"));
041    }
042
043    // the joy of single return values only...
044    private void nodePairFurthestApart(List<Node> nodes, Node[] resultOut) {
045        if(resultOut.length < 2)
046            throw new IllegalArgumentException();
047        // Find from the selected nodes two that are the furthest apart.
048        // Let's call them A and B.
049        double distance = 0;
050
051        Node nodea = null;
052        Node nodeb = null;
053
054        for (int i = 0; i < nodes.size()-1; i++) {
055            Node n = nodes.get(i);
056            for (int j = i+1; j < nodes.size(); j++) {
057                Node m = nodes.get(j);
058                double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth()));
059                if (dist > distance) {
060                    nodea = n;
061                    nodeb = m;
062                    distance = dist;
063                }
064            }
065        }
066        resultOut[0] = nodea;
067        resultOut[1] = nodeb;
068    }
069
070    private void showWarning() {
071        new Notification(
072                tr("Please select at least three nodes."))
073                .setIcon(JOptionPane.INFORMATION_MESSAGE)
074                .show();
075    }
076
077    private static int indexWrap(int size, int i) {
078        i = i % size; // -2 % 5 = -2, -7 % 5 = -2, -5 % 5 = 0
079        if (i < 0) {
080            i = size + i;
081        }
082        return i;
083    }
084    // get the node in w at index i relative to refI
085    private static Node getNodeRelative(Way w, int refI, int i) {
086        int absI = indexWrap(w.getNodesCount(), refI + i);
087        if(w.isClosed() && refI + i < 0) {
088            absI--;  // node duplicated in closed ways
089        }
090        return w.getNode(absI);
091    }
092
093    /**
094     * The general algorithm here is to find the two selected nodes
095     * that are furthest apart, and then to align all other selected
096     * nodes onto the straight line between these nodes.
097     */
098
099
100    /**
101     * Operation depends on the selected objects:
102     */
103    @Override
104    public void actionPerformed(ActionEvent e) {
105        if (!isEnabled())
106            return;
107
108        Node[] anchors = new Node[2]; // oh, java I love you so much..
109
110        List<Node> selectedNodes = new ArrayList<Node>(getCurrentDataSet().getSelectedNodes());
111        Collection<Way> selectedWays = getCurrentDataSet().getSelectedWays();
112        List<Node> nodes = new ArrayList<Node>();
113
114        //// Decide what to align based on selection:
115
116        /// Only ways selected -> Align their nodes.
117        if ((selectedNodes.isEmpty()) && (selectedWays.size() == 1)) { // TODO: handle multiple ways
118            for (Way way : selectedWays) {
119                nodes.addAll(way.getNodes());
120            }
121            // use the nodes furthest apart as anchors
122            nodePairFurthestApart(nodes, anchors);
123        }
124        /// More than 3 nodes selected -> align those nodes
125        else if(selectedNodes.size() >= 3) {
126            nodes.addAll(selectedNodes);
127            // use the nodes furthest apart as anchors
128            nodePairFurthestApart(nodes, anchors);
129        }
130        /// One node selected -> align that node to the relevant neighbors
131        else if (selectedNodes.size() == 1) {
132            Node n = selectedNodes.iterator().next();
133
134            Way w = null;
135            if(selectedWays.size() == 1) {
136                w = selectedWays.iterator().next();
137                if (!w.containsNode(n))
138                    // warning
139                    return;
140            } else {
141                List<Way> refWays = OsmPrimitive.getFilteredList(n.getReferrers(), Way.class);
142                if (refWays.size() == 1) { // node used in only one way
143                    w = refWays.iterator().next();
144                }
145            }
146            if (w == null || w.getNodesCount() < 3)
147                // warning, need at least 3 nodes
148                return;
149
150            // Find anchors
151            int nodeI = w.getNodes().indexOf(n);
152            // End-node in non-circular way selected: align this node with the two neighbors.
153            if ((nodeI == 0 || nodeI == w.getNodesCount()-1) && !w.isClosed()) {
154                int direction = nodeI == 0 ? 1 : -1;
155                anchors[0] = w.getNode(nodeI + direction);
156                anchors[1] = w.getNode(nodeI + direction*2);
157            } else {
158                // o---O---o
159                anchors[0] = getNodeRelative(w, nodeI, 1);
160                anchors[1] = getNodeRelative(w, nodeI, -1);
161            }
162            nodes.add(n);
163        }
164
165        if (anchors[0] == null || anchors[1] == null) {
166            showWarning();
167            return;
168        }
169
170
171        Collection<Command> cmds = new ArrayList<Command>(nodes.size());
172
173        createAlignNodesCommands(anchors, nodes, cmds);
174
175        // Do it!
176        Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Line"), cmds));
177        Main.map.repaint();
178    }
179
180    private void createAlignNodesCommands(Node[] anchors, Collection<Node> nodes, Collection<Command> cmds) {
181        Node nodea = anchors[0];
182        Node nodeb = anchors[1];
183
184        // The anchors are aligned per definition
185        nodes.remove(nodea);
186        nodes.remove(nodeb);
187
188        // Find out co-ords of A and B
189        double ax = nodea.getEastNorth().east();
190        double ay = nodea.getEastNorth().north();
191        double bx = nodeb.getEastNorth().east();
192        double by = nodeb.getEastNorth().north();
193
194        // OK, for each node to move, work out where to move it!
195        for (Node n : nodes) {
196            // Get existing co-ords of node to move
197            double nx = n.getEastNorth().east();
198            double ny = n.getEastNorth().north();
199
200            if (ax == bx) {
201                // Special case if AB is vertical...
202                nx = ax;
203            } else if (ay == by) {
204                // ...or horizontal
205                ny = ay;
206            } else {
207                // Otherwise calculate position by solving y=mx+c
208                double m1 = (by - ay) / (bx - ax);
209                double c1 = ay - (ax * m1);
210                double m2 = (-1) / m1;
211                double c2 = n.getEastNorth().north() - (n.getEastNorth().east() * m2);
212
213                nx = (c2 - c1) / (m1 - m2);
214                ny = (m1 * nx) + c1;
215            }
216            double newX = nx - n.getEastNorth().east();
217            double newY = ny - n.getEastNorth().north();
218            // Add the command to move the node to its new position.
219            cmds.add(new MoveCommand(n, newX, newY));
220        }
221    }
222
223    @Override
224    protected void updateEnabledState() {
225        setEnabled(getCurrentDataSet() != null && !getCurrentDataSet().getSelected().isEmpty());
226    }
227
228    @Override
229    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
230        setEnabled(selection != null && !selection.isEmpty());
231    }
232}