001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.ActionEvent;
007import java.awt.event.KeyEvent;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.List;
011import java.util.Set;
012
013import org.openstreetmap.josm.actions.mapmode.DrawAction;
014import org.openstreetmap.josm.command.ChangeCommand;
015import org.openstreetmap.josm.command.SelectCommand;
016import org.openstreetmap.josm.command.SequenceCommand;
017import org.openstreetmap.josm.data.UndoRedoHandler;
018import org.openstreetmap.josm.data.osm.DataSet;
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.MainApplication;
023import org.openstreetmap.josm.gui.MapFrame;
024import org.openstreetmap.josm.tools.Shortcut;
025import org.openstreetmap.josm.tools.Utils;
026
027/**
028 * Follow line action - Makes easier to draw a line that shares points with another line
029 *
030 * Aimed at those who want to draw two or more lines related with
031 * each other, but carry different information (i.e. a river acts as boundary at
032 * some part of its course. It preferable to have a separated boundary line than to
033 * mix totally different kind of features in one single way).
034 *
035 * @author Germán Márquez Mejía
036 */
037public class FollowLineAction extends JosmAction {
038
039    /**
040     * Constructs a new {@code FollowLineAction}.
041     */
042    public FollowLineAction() {
043        super(
044                tr("Follow line"),
045                "followline",
046                tr("Continues drawing a line that shares nodes with another line."),
047                Shortcut.registerShortcut("tools:followline", tr(
048                "Tool: {0}", tr("Follow")),
049                KeyEvent.VK_F, Shortcut.DIRECT), true);
050    }
051
052    @Override
053    protected void updateEnabledState() {
054        updateEnabledStateOnCurrentSelection();
055    }
056
057    @Override
058    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
059        updateEnabledStateOnModifiableSelection(selection);
060    }
061
062    @Override
063    public void actionPerformed(ActionEvent evt) {
064        DataSet ds = getLayerManager().getEditDataSet();
065        if (ds == null)
066            return;
067        MapFrame map = MainApplication.getMap();
068        if (!(map.mapMode instanceof DrawAction)) return; // We are not on draw mode
069
070        Collection<Node> selectedPoints = ds.getSelectedNodes();
071        Collection<Way> selectedLines = ds.getSelectedWays();
072        if ((selectedPoints.size() > 1) || (selectedLines.size() != 1)) // Unsuitable selection
073            return;
074
075        Node last = ((DrawAction) map.mapMode).getCurrentBaseNode();
076        if (last == null)
077            return;
078        Way follower = selectedLines.iterator().next();
079        if (follower.isClosed())    /* Don't loop until OOM */
080            return;
081        Node prev = follower.getNode(1);
082        boolean reversed = true;
083        if (follower.lastNode().equals(last)) {
084            prev = follower.getNode(follower.getNodesCount() - 2);
085            reversed = false;
086        }
087        List<OsmPrimitive> referrers = last.getReferrers();
088        if (referrers.size() < 2) return; // There's nothing to follow
089
090        Node newPoint = null;
091        for (final Way toFollow : Utils.filteredCollection(referrers, Way.class)) {
092            if (toFollow.equals(follower)) {
093                continue;
094            }
095            Set<Node> points = toFollow.getNeighbours(last);
096            points.remove(prev);
097            if (points.isEmpty())     // No candidate -> consider next way
098                continue;
099            if (points.size() > 1)    // Ambiguous junction?
100                return;
101
102            // points contains exactly one element
103            Node newPointCandidate = points.iterator().next();
104
105            if ((newPoint != null) && (newPoint != newPointCandidate))
106                return;         // Ambiguous junction, force to select next
107
108            newPoint = newPointCandidate;
109        }
110        if (newPoint != null) {
111            Way newFollower = new Way(follower);
112            if (reversed) {
113                newFollower.addNode(0, newPoint);
114            } else {
115                newFollower.addNode(newPoint);
116            }
117            UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Follow line"),
118                    new ChangeCommand(ds, follower, newFollower),
119                    new SelectCommand(ds, newFollower.isClosed() // see #10028 - unselect last node when closing a way
120                            ? Arrays.<OsmPrimitive>asList(follower)
121                            : Arrays.<OsmPrimitive>asList(follower, newPoint)
122                    ))
123            );
124            // "viewport following" mode for tracing long features
125            // from aerial imagery or GPS tracks.
126            if (DrawAction.VIEWPORT_FOLLOWING.get()) {
127                map.mapView.smoothScrollTo(newPoint.getEastNorth());
128            }
129        }
130    }
131}