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 /** 038 * Constructs a new {@code FollowLineAction}. 039 */ 040 public FollowLineAction() { 041 super( 042 tr("Follow line"), 043 "followline", 044 tr("Continues drawing a line that shares nodes with another line."), 045 Shortcut.registerShortcut("tools:followline", tr( 046 "Tool: {0}", tr("Follow")), 047 KeyEvent.VK_F, Shortcut.DIRECT), true); 048 } 049 050 @Override 051 protected void updateEnabledState() { 052 updateEnabledStateOnCurrentSelection(); 053 } 054 055 @Override 056 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 057 setEnabled(selection != null && !selection.isEmpty()); 058 } 059 060 @Override 061 public void actionPerformed(ActionEvent evt) { 062 OsmDataLayer osmLayer = Main.getLayerManager().getEditLayer(); 063 if (osmLayer == null) 064 return; 065 if (!(Main.map.mapMode instanceof DrawAction)) return; // We are not on draw mode 066 067 Collection<Node> selectedPoints = osmLayer.data.getSelectedNodes(); 068 Collection<Way> selectedLines = osmLayer.data.getSelectedWays(); 069 if ((selectedPoints.size() > 1) || (selectedLines.size() != 1)) // Unsuitable selection 070 return; 071 072 Node last = ((DrawAction) Main.map.mapMode).getCurrentBaseNode(); 073 if (last == null) 074 return; 075 Way follower = selectedLines.iterator().next(); 076 if (follower.isClosed()) /* Don't loop until OOM */ 077 return; 078 Node prev = follower.getNode(1); 079 boolean reversed = true; 080 if (follower.lastNode().equals(last)) { 081 prev = follower.getNode(follower.getNodesCount() - 2); 082 reversed = false; 083 } 084 List<OsmPrimitive> referrers = last.getReferrers(); 085 if (referrers.size() < 2) return; // There's nothing to follow 086 087 Node newPoint = null; 088 for (final Way toFollow : Utils.filteredCollection(referrers, Way.class)) { 089 if (toFollow.equals(follower)) { 090 continue; 091 } 092 Set<Node> points = toFollow.getNeighbours(last); 093 points.remove(prev); 094 if (points.isEmpty()) // No candidate -> consider next way 095 continue; 096 if (points.size() > 1) // Ambiguous junction? 097 return; 098 099 // points contains exactly one element 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 SequenceCommand(tr("Follow line"), 115 new ChangeCommand(follower, newFollower), 116 new SelectCommand(newFollower.isClosed() // see #10028 - unselect last node when closing a way 117 ? Arrays.<OsmPrimitive>asList(newFollower) 118 : Arrays.<OsmPrimitive>asList(newFollower, newPoint) 119 )) 120 ); 121 // "viewport following" mode for tracing long features 122 // from aerial imagery or GPS tracks. 123 if (Main.map.mapView.viewportFollowing) { 124 Main.map.mapView.smoothScrollTo(newPoint.getEastNorth()); 125 } 126 } 127 } 128}