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