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