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