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