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