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