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}