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.io.Serializable;
010import java.util.Collection;
011import java.util.Collections;
012import java.util.Comparator;
013import java.util.HashMap;
014import java.util.LinkedList;
015import java.util.List;
016import java.util.Map;
017import java.util.Set;
018import java.util.SortedSet;
019import java.util.TreeSet;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.command.ChangeCommand;
023import org.openstreetmap.josm.command.Command;
024import org.openstreetmap.josm.command.MoveCommand;
025import org.openstreetmap.josm.command.SequenceCommand;
026import org.openstreetmap.josm.data.coor.EastNorth;
027import org.openstreetmap.josm.data.osm.Node;
028import org.openstreetmap.josm.data.osm.OsmPrimitive;
029import org.openstreetmap.josm.data.osm.Way;
030import org.openstreetmap.josm.data.osm.WaySegment;
031import org.openstreetmap.josm.data.projection.Projections;
032import org.openstreetmap.josm.tools.Geometry;
033import org.openstreetmap.josm.tools.MultiMap;
034import org.openstreetmap.josm.tools.Shortcut;
035
036/**
037 * Action allowing to join a node to a nearby way, operating on two modes:<ul>
038 * <li><b>Join Node to Way</b>: Include a node into the nearest way segments. The node does not move</li>
039 * <li><b>Move Node onto Way</b>: Move the node onto the nearest way segments and include it</li>
040 * </ul>
041 * @since 466
042 */
043public class JoinNodeWayAction extends JosmAction {
044
045    protected final boolean joinWayToNode;
046
047    protected JoinNodeWayAction(boolean joinWayToNode, String name, String iconName, String tooltip,
048            Shortcut shortcut, boolean registerInToolbar) {
049        super(name, iconName, tooltip, shortcut, registerInToolbar);
050        this.joinWayToNode = joinWayToNode;
051    }
052
053    /**
054     * Constructs a Join Node to Way action.
055     * @return the Join Node to Way action
056     */
057    public static JoinNodeWayAction createJoinNodeToWayAction() {
058        JoinNodeWayAction action = new JoinNodeWayAction(false,
059                tr("Join Node to Way"), /* ICON */ "joinnodeway",
060                tr("Include a node into the nearest way segments"),
061                Shortcut.registerShortcut("tools:joinnodeway", tr("Tool: {0}", tr("Join Node to Way")),
062                        KeyEvent.VK_J, Shortcut.DIRECT), true);
063        action.putValue("help", ht("/Action/JoinNodeWay"));
064        return action;
065    }
066
067    /**
068     * Constructs a Move Node onto Way action.
069     * @return the Move Node onto Way action
070     */
071    public static JoinNodeWayAction createMoveNodeOntoWayAction() {
072        JoinNodeWayAction action = new JoinNodeWayAction(true,
073                tr("Move Node onto Way"), /* ICON*/ "movenodeontoway",
074                tr("Move the node onto the nearest way segments and include it"),
075                Shortcut.registerShortcut("tools:movenodeontoway", tr("Tool: {0}", tr("Move Node onto Way")),
076                        KeyEvent.VK_N, Shortcut.DIRECT), true);
077        action.putValue("help", ht("/Action/MoveNodeWay"));
078        return action;
079    }
080
081    @Override
082    public void actionPerformed(ActionEvent e) {
083        if (!isEnabled())
084            return;
085        Collection<Node> selectedNodes = getCurrentDataSet().getSelectedNodes();
086        Collection<Command> cmds = new LinkedList<>();
087        Map<Way, MultiMap<Integer, Node>> data = new HashMap<>();
088
089        // If the user has selected some ways, only join the node to these.
090        boolean restrictToSelectedWays =
091                !getCurrentDataSet().getSelectedWays().isEmpty();
092
093        // Planning phase: decide where we'll insert the nodes and put it all in "data"
094        for (Node node : selectedNodes) {
095            List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(
096                    Main.map.mapView.getPoint(node), OsmPrimitive.isSelectablePredicate);
097
098            MultiMap<Way, Integer> insertPoints = new MultiMap<>();
099            for (WaySegment ws : wss) {
100                // Maybe cleaner to pass a "isSelected" predicate to getNearestWaySegments, but this is less invasive.
101                if (restrictToSelectedWays && !ws.way.isSelected()) {
102                    continue;
103                }
104
105                if (!ws.getFirstNode().equals(node) && !ws.getSecondNode().equals(node)) {
106                    insertPoints.put(ws.way, ws.lowerIndex);
107                }
108            }
109            for (Map.Entry<Way, Set<Integer>> entry : insertPoints.entrySet()) {
110                final Way w = entry.getKey();
111                final Set<Integer> insertPointsForWay = entry.getValue();
112                for (int i : pruneSuccs(insertPointsForWay)) {
113                    MultiMap<Integer, Node> innerMap;
114                    if (!data.containsKey(w)) {
115                        innerMap = new MultiMap<>();
116                    } else {
117                        innerMap = data.get(w);
118                    }
119                    innerMap.put(i, node);
120                    data.put(w, innerMap);
121                }
122            }
123        }
124
125        // Execute phase: traverse the structure "data" and finally put the nodes into place
126        for (Map.Entry<Way, MultiMap<Integer, Node>> entry : data.entrySet()) {
127            final Way w = entry.getKey();
128            final MultiMap<Integer, Node> innerEntry = entry.getValue();
129
130            List<Integer> segmentIndexes = new LinkedList<>();
131            segmentIndexes.addAll(innerEntry.keySet());
132            Collections.sort(segmentIndexes, Collections.reverseOrder());
133
134            List<Node> wayNodes = w.getNodes();
135            for (Integer segmentIndex : segmentIndexes) {
136                final Set<Node> nodesInSegment = innerEntry.get(segmentIndex);
137                if (joinWayToNode) {
138                    for (Node node : nodesInSegment) {
139                        EastNorth newPosition = Geometry.closestPointToSegment(w.getNode(segmentIndex).getEastNorth(),
140                                                                            w.getNode(segmentIndex+1).getEastNorth(),
141                                                                            node.getEastNorth());
142                        MoveCommand c = new MoveCommand(node, Projections.inverseProject(newPosition));
143                        // Avoid moving a given node several times at the same position in case of overlapping ways
144                        if (!cmds.contains(c)) {
145                            cmds.add(c);
146                        }
147                    }
148                }
149                List<Node> nodesToAdd = new LinkedList<>();
150                nodesToAdd.addAll(nodesInSegment);
151                Collections.sort(nodesToAdd, new NodeDistanceToRefNodeComparator(
152                        w.getNode(segmentIndex), w.getNode(segmentIndex+1), !joinWayToNode));
153                wayNodes.addAll(segmentIndex + 1, nodesToAdd);
154            }
155            Way wnew = new Way(w);
156            wnew.setNodes(wayNodes);
157            cmds.add(new ChangeCommand(w, wnew));
158        }
159
160        if (cmds.isEmpty()) return;
161        Main.main.undoRedo.add(new SequenceCommand(getValue(NAME).toString(), cmds));
162        Main.map.repaint();
163    }
164
165    private static SortedSet<Integer> pruneSuccs(Collection<Integer> is) {
166        SortedSet<Integer> is2 = new TreeSet<>();
167        for (int i : is) {
168            if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
169                is2.add(i);
170            }
171        }
172        return is2;
173    }
174
175    /**
176     * Sorts collinear nodes by their distance to a common reference node.
177     */
178    private static class NodeDistanceToRefNodeComparator implements Comparator<Node>, Serializable {
179
180        private static final long serialVersionUID = 1L;
181
182        private final EastNorth refPoint;
183        private EastNorth refPoint2;
184        private final boolean projectToSegment;
185
186        NodeDistanceToRefNodeComparator(Node referenceNode, Node referenceNode2, boolean projectFirst) {
187            refPoint = referenceNode.getEastNorth();
188            refPoint2 = referenceNode2.getEastNorth();
189            projectToSegment = projectFirst;
190        }
191
192        @Override
193        public int compare(Node first, Node second) {
194            EastNorth firstPosition = first.getEastNorth();
195            EastNorth secondPosition = second.getEastNorth();
196
197            if (projectToSegment) {
198                firstPosition = Geometry.closestPointToSegment(refPoint, refPoint2, firstPosition);
199                secondPosition = Geometry.closestPointToSegment(refPoint, refPoint2, secondPosition);
200            }
201
202            double distanceFirst = firstPosition.distance(refPoint);
203            double distanceSecond = secondPosition.distance(refPoint);
204            double difference =  distanceFirst - distanceSecond;
205
206            if (difference > 0.0) return 1;
207            if (difference < 0.0) return -1;
208            return 0;
209        }
210    }
211
212    @Override
213    protected void updateEnabledState() {
214        if (getCurrentDataSet() == null) {
215            setEnabled(false);
216        } else {
217            updateEnabledState(getCurrentDataSet().getSelected());
218        }
219    }
220
221    @Override
222    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
223        setEnabled(selection != null && !selection.isEmpty());
224    }
225}