source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/DrawAction.java @ 12316

Last change on this file since 12316 was 12316, checked in by michael2402, 21 months ago

Add a method to undo/redo to get the last command.

  • Property svn:eol-style set to native
File size: 52.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.BasicStroke;
10import java.awt.Color;
11import java.awt.Cursor;
12import java.awt.Graphics2D;
13import java.awt.Point;
14import java.awt.event.ActionEvent;
15import java.awt.event.KeyEvent;
16import java.awt.event.MouseEvent;
17import java.util.ArrayList;
18import java.util.Collection;
19import java.util.Collections;
20import java.util.HashMap;
21import java.util.HashSet;
22import java.util.Iterator;
23import java.util.LinkedList;
24import java.util.List;
25import java.util.Map;
26import java.util.Set;
27
28import javax.swing.AbstractAction;
29import javax.swing.JCheckBoxMenuItem;
30import javax.swing.JMenuItem;
31import javax.swing.JOptionPane;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.actions.JosmAction;
35import org.openstreetmap.josm.command.AddCommand;
36import org.openstreetmap.josm.command.ChangeCommand;
37import org.openstreetmap.josm.command.Command;
38import org.openstreetmap.josm.command.SequenceCommand;
39import org.openstreetmap.josm.data.Bounds;
40import org.openstreetmap.josm.data.SelectionChangedListener;
41import org.openstreetmap.josm.data.coor.EastNorth;
42import org.openstreetmap.josm.data.osm.DataSet;
43import org.openstreetmap.josm.data.osm.Node;
44import org.openstreetmap.josm.data.osm.OsmPrimitive;
45import org.openstreetmap.josm.data.osm.Way;
46import org.openstreetmap.josm.data.osm.WaySegment;
47import org.openstreetmap.josm.data.osm.visitor.paint.ArrowPaintHelper;
48import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
49import org.openstreetmap.josm.data.preferences.AbstractToStringProperty;
50import org.openstreetmap.josm.data.preferences.BooleanProperty;
51import org.openstreetmap.josm.data.preferences.CachingProperty;
52import org.openstreetmap.josm.data.preferences.ColorProperty;
53import org.openstreetmap.josm.data.preferences.DoubleProperty;
54import org.openstreetmap.josm.data.preferences.StrokeProperty;
55import org.openstreetmap.josm.gui.MainMenu;
56import org.openstreetmap.josm.gui.MapView;
57import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
58import org.openstreetmap.josm.gui.NavigatableComponent;
59import org.openstreetmap.josm.gui.draw.MapPath2D;
60import org.openstreetmap.josm.gui.layer.Layer;
61import org.openstreetmap.josm.gui.layer.MapViewPaintable;
62import org.openstreetmap.josm.gui.layer.OsmDataLayer;
63import org.openstreetmap.josm.gui.util.KeyPressReleaseListener;
64import org.openstreetmap.josm.gui.util.ModifierListener;
65import org.openstreetmap.josm.tools.Geometry;
66import org.openstreetmap.josm.tools.ImageProvider;
67import org.openstreetmap.josm.tools.Pair;
68import org.openstreetmap.josm.tools.Shortcut;
69import org.openstreetmap.josm.tools.Utils;
70
71/**
72 * Mapmode to add nodes, create and extend ways.
73 */
74public class DrawAction extends MapMode implements MapViewPaintable, SelectionChangedListener, KeyPressReleaseListener, ModifierListener {
75
76    /**
77     * If this property is set, the draw action moves the viewport when adding new points.
78     * @since 12182
79     */
80    public static final CachingProperty<Boolean> VIEWPORT_FOLLOWING = new BooleanProperty("draw.viewport.following", false).cached();
81
82    private static final Color ORANGE_TRANSPARENT = new Color(Color.ORANGE.getRed(), Color.ORANGE.getGreen(), Color.ORANGE.getBlue(), 128);
83
84    private static final ArrowPaintHelper START_WAY_INDICATOR = new ArrowPaintHelper(Utils.toRadians(90), 8);
85
86    static final CachingProperty<Boolean> USE_REPEATED_SHORTCUT
87            = new BooleanProperty("draw.anglesnap.toggleOnRepeatedA", true).cached();
88    static final CachingProperty<BasicStroke> RUBBER_LINE_STROKE
89            = new StrokeProperty("draw.stroke.helper-line", "3").cached();
90
91    static final CachingProperty<BasicStroke> HIGHLIGHT_STROKE
92            = new StrokeProperty("draw.anglesnap.stroke.highlight", "10").cached();
93    static final CachingProperty<BasicStroke> HELPER_STROKE
94            = new StrokeProperty("draw.anglesnap.stroke.helper", "1 4").cached();
95
96    static final CachingProperty<Double> SNAP_ANGLE_TOLERANCE
97            = new DoubleProperty("draw.anglesnap.tolerance", 5.0).cached();
98    static final CachingProperty<Boolean> DRAW_CONSTRUCTION_GEOMETRY
99            = new BooleanProperty("draw.anglesnap.drawConstructionGeometry", true).cached();
100    static final CachingProperty<Boolean> SHOW_PROJECTED_POINT
101            = new BooleanProperty("draw.anglesnap.drawProjectedPoint", true).cached();
102    static final CachingProperty<Boolean> SNAP_TO_PROJECTIONS
103            = new BooleanProperty("draw.anglesnap.projectionsnap", true).cached();
104
105    static final CachingProperty<Boolean> SHOW_ANGLE
106            = new BooleanProperty("draw.anglesnap.showAngle", true).cached();
107
108    static final CachingProperty<Color> SNAP_HELPER_COLOR
109            = new ColorProperty(marktr("draw angle snap"), Color.ORANGE).cached();
110
111    static final CachingProperty<Color> HIGHLIGHT_COLOR
112            = new ColorProperty(marktr("draw angle snap highlight"), ORANGE_TRANSPARENT).cached();
113
114    static final AbstractToStringProperty<Color> RUBBER_LINE_COLOR
115            = PaintColors.SELECTED.getProperty().getChildColor(marktr("helper line"));
116
117    static final CachingProperty<Boolean> DRAW_HELPER_LINE
118            = new BooleanProperty("draw.helper-line", true).cached();
119    static final CachingProperty<Boolean> DRAW_TARGET_HIGHLIGHT
120            = new BooleanProperty("draw.target-highlight", true).cached();
121    static final CachingProperty<Double> SNAP_TO_INTERSECTION_THRESHOLD
122            = new DoubleProperty("edit.snap-intersection-threshold", 10).cached();
123
124    private final Cursor cursorJoinNode;
125    private final Cursor cursorJoinWay;
126
127    private transient Node lastUsedNode;
128    private double toleranceMultiplier;
129
130    private transient Node mouseOnExistingNode;
131    private transient Set<Way> mouseOnExistingWays = new HashSet<>();
132    // old highlights store which primitives are currently highlighted. This
133    // is true, even if target highlighting is disabled since the status bar
134    // derives its information from this list as well.
135    private transient Set<OsmPrimitive> oldHighlights = new HashSet<>();
136    // new highlights contains a list of primitives that should be highlighted
137    // but haven't been so far. The idea is to compare old and new and only
138    // repaint if there are changes.
139    private transient Set<OsmPrimitive> newHighlights = new HashSet<>();
140    private boolean wayIsFinished;
141    private Point mousePos;
142    private Point oldMousePos;
143
144    private transient Node currentBaseNode;
145    private transient Node previousNode;
146    private EastNorth currentMouseEastNorth;
147
148    private final transient DrawSnapHelper snapHelper = new DrawSnapHelper(this);
149
150    private final transient Shortcut backspaceShortcut;
151    private final BackSpaceAction backspaceAction;
152    private final transient Shortcut snappingShortcut;
153    private boolean ignoreNextKeyRelease;
154
155    private final SnapChangeAction snapChangeAction;
156    private final JCheckBoxMenuItem snapCheckboxMenuItem;
157    private static final BasicStroke BASIC_STROKE = new BasicStroke(1);
158
159    private Point rightClickPressPos;
160
161    /**
162     * Constructs a new {@code DrawAction}.
163     * @since 11713
164     */
165    public DrawAction() {
166        super(tr("Draw"), "node/autonode", tr("Draw nodes"),
167                Shortcut.registerShortcut("mapmode:draw", tr("Mode: {0}", tr("Draw")), KeyEvent.VK_A, Shortcut.DIRECT),
168                ImageProvider.getCursor("crosshair", null));
169
170        snappingShortcut = Shortcut.registerShortcut("mapmode:drawanglesnapping",
171                tr("Mode: Draw Angle snapping"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
172        snapChangeAction = new SnapChangeAction();
173        snapCheckboxMenuItem = addMenuItem();
174        snapHelper.setMenuCheckBox(snapCheckboxMenuItem);
175        backspaceShortcut = Shortcut.registerShortcut("mapmode:backspace",
176                tr("Backspace in Add mode"), KeyEvent.VK_BACK_SPACE, Shortcut.DIRECT);
177        backspaceAction = new BackSpaceAction();
178        cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode");
179        cursorJoinWay = ImageProvider.getCursor("crosshair", "joinway");
180
181        snapHelper.init();
182    }
183
184    private JCheckBoxMenuItem addMenuItem() {
185        int n = Main.main.menu.editMenu.getItemCount();
186        for (int i = n-1; i > 0; i--) {
187            JMenuItem item = Main.main.menu.editMenu.getItem(i);
188            if (item != null && item.getAction() != null && item.getAction() instanceof SnapChangeAction) {
189                Main.main.menu.editMenu.remove(i);
190            }
191        }
192        return MainMenu.addWithCheckbox(Main.main.menu.editMenu, snapChangeAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
193    }
194
195    /**
196     * Checks if a map redraw is required and does so if needed. Also updates the status bar.
197     * @return true if a repaint is needed
198     */
199    private boolean redrawIfRequired() {
200        updateStatusLine();
201        // repaint required if the helper line is active.
202        boolean needsRepaint = DRAW_HELPER_LINE.get() && !wayIsFinished;
203        if (DRAW_TARGET_HIGHLIGHT.get()) {
204            // move newHighlights to oldHighlights; only update changed primitives
205            for (OsmPrimitive x : newHighlights) {
206                if (oldHighlights.contains(x)) {
207                    continue;
208                }
209                x.setHighlighted(true);
210                needsRepaint = true;
211            }
212            oldHighlights.removeAll(newHighlights);
213            for (OsmPrimitive x : oldHighlights) {
214                x.setHighlighted(false);
215                needsRepaint = true;
216            }
217        }
218        // required in order to print correct help text
219        oldHighlights = newHighlights;
220
221        if (!needsRepaint && !DRAW_TARGET_HIGHLIGHT.get())
222            return false;
223
224        // update selection to reflect which way being modified
225        OsmDataLayer editLayer = getLayerManager().getEditLayer();
226        if (editLayer != null && getCurrentBaseNode() != null && !editLayer.data.selectionEmpty()) {
227            DataSet currentDataSet = editLayer.data;
228            Way continueFrom = getWayForNode(getCurrentBaseNode());
229            if (alt && continueFrom != null && (!getCurrentBaseNode().isSelected() || continueFrom.isSelected())) {
230                addRemoveSelection(currentDataSet, getCurrentBaseNode(), continueFrom);
231                needsRepaint = true;
232            } else if (!alt && continueFrom != null && !continueFrom.isSelected()) {
233                currentDataSet.addSelected(continueFrom);
234                needsRepaint = true;
235            }
236        }
237
238        if (needsRepaint && editLayer != null) {
239            editLayer.invalidate();
240        }
241        return needsRepaint;
242    }
243
244    private static void addRemoveSelection(DataSet ds, OsmPrimitive toAdd, OsmPrimitive toRemove) {
245        ds.beginUpdate(); // to prevent the selection listener to screw around with the state
246        try {
247            ds.addSelected(toAdd);
248            ds.clearSelection(toRemove);
249        } finally {
250            ds.endUpdate();
251        }
252    }
253
254    @Override
255    public void enterMode() {
256        if (!isEnabled())
257            return;
258        super.enterMode();
259        readPreferences();
260
261        // determine if selection is suitable to continue drawing. If it
262        // isn't, set wayIsFinished to true to avoid superfluous repaints.
263        determineCurrentBaseNodeAndPreviousNode(getLayerManager().getEditDataSet().getSelected());
264        wayIsFinished = getCurrentBaseNode() == null;
265
266        toleranceMultiplier = 0.01 * NavigatableComponent.PROP_SNAP_DISTANCE.get();
267
268        snapHelper.init();
269        snapCheckboxMenuItem.getAction().setEnabled(true);
270
271        Main.map.statusLine.getAnglePanel().addMouseListener(snapHelper.anglePopupListener);
272        Main.registerActionShortcut(backspaceAction, backspaceShortcut);
273
274        Main.map.mapView.addMouseListener(this);
275        Main.map.mapView.addMouseMotionListener(this);
276        Main.map.mapView.addTemporaryLayer(this);
277        DataSet.addSelectionListener(this);
278
279        Main.map.keyDetector.addKeyListener(this);
280        Main.map.keyDetector.addModifierListener(this);
281        ignoreNextKeyRelease = true;
282    }
283
284    @Override
285    public void exitMode() {
286        super.exitMode();
287        Main.map.mapView.removeMouseListener(this);
288        Main.map.mapView.removeMouseMotionListener(this);
289        Main.map.mapView.removeTemporaryLayer(this);
290        DataSet.removeSelectionListener(this);
291        Main.unregisterActionShortcut(backspaceAction, backspaceShortcut);
292        snapHelper.unsetFixedMode();
293        snapCheckboxMenuItem.getAction().setEnabled(false);
294
295        Main.map.statusLine.getAnglePanel().removeMouseListener(snapHelper.anglePopupListener);
296        Main.map.statusLine.activateAnglePanel(false);
297
298        removeHighlighting();
299        Main.map.keyDetector.removeKeyListener(this);
300        Main.map.keyDetector.removeModifierListener(this);
301    }
302
303    /**
304     * redraw to (possibly) get rid of helper line if selection changes.
305     */
306    @Override
307    public void modifiersChanged(int modifiers) {
308        if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable())
309            return;
310        updateKeyModifiers(modifiers);
311        computeHelperLine();
312        addHighlighting();
313    }
314
315    @Override
316    public void doKeyPressed(KeyEvent e) {
317        if (!snappingShortcut.isEvent(e) && !(USE_REPEATED_SHORTCUT.get() && getShortcut().isEvent(e)))
318            return;
319        snapHelper.setFixedMode();
320        computeHelperLine();
321        redrawIfRequired();
322    }
323
324    @Override
325    public void doKeyReleased(KeyEvent e) {
326        if (!snappingShortcut.isEvent(e) && !(USE_REPEATED_SHORTCUT.get() && getShortcut().isEvent(e)))
327            return;
328        if (ignoreNextKeyRelease) {
329            ignoreNextKeyRelease = false;
330            return;
331        }
332        snapHelper.unFixOrTurnOff();
333        computeHelperLine();
334        redrawIfRequired();
335    }
336
337    /**
338     * redraw to (possibly) get rid of helper line if selection changes.
339     */
340    @Override
341    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
342        if (!Main.map.mapView.isActiveLayerDrawable())
343            return;
344        computeHelperLine();
345        addHighlighting();
346    }
347
348    private void tryAgain(MouseEvent e) {
349        getLayerManager().getEditDataSet().setSelected();
350        mouseReleased(e);
351    }
352
353    /**
354     * This function should be called when the user wishes to finish his current draw action.
355     * If Potlatch Style is enabled, it will switch to select tool, otherwise simply disable
356     * the helper line until the user chooses to draw something else.
357     */
358    private void finishDrawing() {
359        lastUsedNode = null;
360        wayIsFinished = true;
361        Main.map.selectSelectTool(true);
362        snapHelper.noSnapNow();
363
364        // Redraw to remove the helper line stub
365        computeHelperLine();
366        removeHighlighting();
367    }
368
369    @Override
370    public void mousePressed(MouseEvent e) {
371        if (e.getButton() == MouseEvent.BUTTON3) {
372            rightClickPressPos = e.getPoint();
373        }
374    }
375
376    /**
377     * If user clicked with the left button, add a node at the current mouse
378     * position.
379     *
380     * If in nodeway mode, insert the node into the way.
381     */
382    @Override
383    public void mouseReleased(MouseEvent e) {
384        if (e.getButton() == MouseEvent.BUTTON3) {
385            Point curMousePos = e.getPoint();
386            if (curMousePos.equals(rightClickPressPos)) {
387                tryToSetBaseSegmentForAngleSnap();
388            }
389            return;
390        }
391        if (e.getButton() != MouseEvent.BUTTON1)
392            return;
393        if (!Main.map.mapView.isActiveLayerDrawable())
394            return;
395        // request focus in order to enable the expected keyboard shortcuts
396        //
397        Main.map.mapView.requestFocus();
398
399        if (e.getClickCount() > 1 && mousePos != null && mousePos.equals(oldMousePos)) {
400            // A double click equals "user clicked last node again, finish way"
401            // Change draw tool only if mouse position is nearly the same, as
402            // otherwise fast clicks will count as a double click
403            finishDrawing();
404            return;
405        }
406        oldMousePos = mousePos;
407
408        // we copy ctrl/alt/shift from the event just in case our global
409        // keyDetector didn't make it through the security manager. Unclear
410        // if that can ever happen but better be safe.
411        updateKeyModifiers(e);
412        mousePos = e.getPoint();
413
414        DataSet ds = getLayerManager().getEditDataSet();
415        Collection<OsmPrimitive> selection = new ArrayList<>(ds.getSelected());
416
417        boolean newNode = false;
418        Node n = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive::isSelectable);
419        if (ctrl) {
420            Iterator<Way> it = ds.getSelectedWays().iterator();
421            if (it.hasNext()) {
422                // ctrl-click on node of selected way = reuse node despite of ctrl
423                if (!it.next().containsNode(n)) n = null;
424            } else {
425                n = null; // ctrl-click + no selected way = new node
426            }
427        }
428
429        if (n != null && !snapHelper.isActive()) {
430            // user clicked on node
431            if (selection.isEmpty() || wayIsFinished) {
432                // select the clicked node and do nothing else
433                // (this is just a convenience option so that people don't
434                // have to switch modes)
435
436                ds.setSelected(n);
437                // If we extend/continue an existing way, select it already now to make it obvious
438                Way continueFrom = getWayForNode(n);
439                if (continueFrom != null) {
440                    ds.addSelected(continueFrom);
441                }
442
443                // The user explicitly selected a node, so let him continue drawing
444                wayIsFinished = false;
445                return;
446            }
447        } else {
448            EastNorth newEN;
449            if (n != null) {
450                EastNorth foundPoint = n.getEastNorth();
451                // project found node to snapping line
452                newEN = snapHelper.getSnapPoint(foundPoint);
453                // do not add new node if there is some node within snapping distance
454                double tolerance = Main.map.mapView.getDist100Pixel() * toleranceMultiplier;
455                if (foundPoint.distance(newEN) > tolerance) {
456                    n = new Node(newEN); // point != projected, so we create new node
457                    newNode = true;
458                }
459            } else { // n==null, no node found in clicked area
460                EastNorth mouseEN = Main.map.mapView.getEastNorth(e.getX(), e.getY());
461                newEN = snapHelper.isSnapOn() ? snapHelper.getSnapPoint(mouseEN) : mouseEN;
462                n = new Node(newEN); //create node at clicked point
463                newNode = true;
464            }
465            snapHelper.unsetFixedMode();
466        }
467
468        Collection<Command> cmds = new LinkedList<>();
469        Collection<OsmPrimitive> newSelection = new LinkedList<>(ds.getSelected());
470        List<Way> reuseWays = new ArrayList<>();
471        List<Way> replacedWays = new ArrayList<>();
472
473        if (newNode) {
474            if (n.getCoor().isOutSideWorld()) {
475                JOptionPane.showMessageDialog(
476                        Main.parent,
477                        tr("Cannot add a node outside of the world."),
478                        tr("Warning"),
479                        JOptionPane.WARNING_MESSAGE
480                        );
481                return;
482            }
483            cmds.add(new AddCommand(n));
484
485            if (!ctrl) {
486                // Insert the node into all the nearby way segments
487                List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(
488                        Main.map.mapView.getPoint(n), OsmPrimitive::isSelectable);
489                if (snapHelper.isActive()) {
490                    tryToMoveNodeOnIntersection(wss, n);
491                }
492                insertNodeIntoAllNearbySegments(wss, n, newSelection, cmds, replacedWays, reuseWays);
493            }
494        }
495        // now "n" is newly created or reused node that shoud be added to some way
496
497        // This part decides whether or not a "segment" (i.e. a connection) is made to an existing node.
498
499        // For a connection to be made, the user must either have a node selected (connection
500        // is made to that node), or he must have a way selected *and* one of the endpoints
501        // of that way must be the last used node (connection is made to last used node), or
502        // he must have a way and a node selected (connection is made to the selected node).
503
504        // If the above does not apply, the selection is cleared and a new try is started
505
506        boolean extendedWay = false;
507        boolean wayIsFinishedTemp = wayIsFinished;
508        wayIsFinished = false;
509
510        // don't draw lines if shift is held
511        if (!selection.isEmpty() && !shift) {
512            Node selectedNode = null;
513            Way selectedWay = null;
514
515            for (OsmPrimitive p : selection) {
516                if (p instanceof Node) {
517                    if (selectedNode != null) {
518                        // Too many nodes selected to do something useful
519                        tryAgain(e);
520                        return;
521                    }
522                    selectedNode = (Node) p;
523                } else if (p instanceof Way) {
524                    if (selectedWay != null) {
525                        // Too many ways selected to do something useful
526                        tryAgain(e);
527                        return;
528                    }
529                    selectedWay = (Way) p;
530                }
531            }
532
533            // the node from which we make a connection
534            Node n0 = findNodeToContinueFrom(selectedNode, selectedWay);
535            // We have a selection but it isn't suitable. Try again.
536            if (n0 == null) {
537                tryAgain(e);
538                return;
539            }
540            if (!wayIsFinishedTemp) {
541                if (isSelfContainedWay(selectedWay, n0, n))
542                    return;
543
544                // User clicked last node again, finish way
545                if (n0 == n) {
546                    finishDrawing();
547                    return;
548                }
549
550                // Ok we know now that we'll insert a line segment, but will it connect to an
551                // existing way or make a new way of its own? The "alt" modifier means that the
552                // user wants a new way.
553                Way way = alt ? null : (selectedWay != null ? selectedWay : getWayForNode(n0));
554                Way wayToSelect;
555
556                // Don't allow creation of self-overlapping ways
557                if (way != null) {
558                    int nodeCount = 0;
559                    for (Node p : way.getNodes()) {
560                        if (p.equals(n0)) {
561                            nodeCount++;
562                        }
563                    }
564                    if (nodeCount > 1) {
565                        way = null;
566                    }
567                }
568
569                if (way == null) {
570                    way = new Way();
571                    way.addNode(n0);
572                    cmds.add(new AddCommand(way));
573                    wayToSelect = way;
574                } else {
575                    int i;
576                    if ((i = replacedWays.indexOf(way)) != -1) {
577                        way = reuseWays.get(i);
578                        wayToSelect = way;
579                    } else {
580                        wayToSelect = way;
581                        Way wnew = new Way(way);
582                        cmds.add(new ChangeCommand(way, wnew));
583                        way = wnew;
584                    }
585                }
586
587                // Connected to a node that's already in the way
588                if (way.containsNode(n)) {
589                    wayIsFinished = true;
590                    selection.clear();
591                }
592
593                // Add new node to way
594                if (way.getNode(way.getNodesCount() - 1) == n0) {
595                    way.addNode(n);
596                } else {
597                    way.addNode(0, n);
598                }
599
600                extendedWay = true;
601                newSelection.clear();
602                newSelection.add(wayToSelect);
603            }
604        }
605        if (!extendedWay && !newNode) {
606            return; // We didn't do anything.
607        }
608
609        String title = getTitle(newNode, n, newSelection, reuseWays, extendedWay);
610
611        Command c = new SequenceCommand(title, cmds);
612
613        Main.main.undoRedo.add(c);
614        if (!wayIsFinished) {
615            lastUsedNode = n;
616        }
617
618        ds.setSelected(newSelection);
619
620        // "viewport following" mode for tracing long features
621        // from aerial imagery or GPS tracks.
622        if (VIEWPORT_FOLLOWING.get()) {
623            Main.map.mapView.smoothScrollTo(n.getEastNorth());
624        }
625        computeHelperLine();
626        removeHighlighting();
627    }
628
629    private static String getTitle(boolean newNode, Node n, Collection<OsmPrimitive> newSelection, List<Way> reuseWays,
630            boolean extendedWay) {
631        String title;
632        if (!extendedWay) {
633            if (reuseWays.isEmpty()) {
634                title = tr("Add node");
635            } else {
636                title = tr("Add node into way");
637                for (Way w : reuseWays) {
638                    newSelection.remove(w);
639                }
640            }
641            newSelection.clear();
642            newSelection.add(n);
643        } else if (!newNode) {
644            title = tr("Connect existing way to node");
645        } else if (reuseWays.isEmpty()) {
646            title = tr("Add a new node to an existing way");
647        } else {
648            title = tr("Add node into way and connect");
649        }
650        return title;
651    }
652
653    private void insertNodeIntoAllNearbySegments(List<WaySegment> wss, Node n, Collection<OsmPrimitive> newSelection,
654            Collection<Command> cmds, List<Way> replacedWays, List<Way> reuseWays) {
655        Map<Way, List<Integer>> insertPoints = new HashMap<>();
656        for (WaySegment ws : wss) {
657            List<Integer> is;
658            if (insertPoints.containsKey(ws.way)) {
659                is = insertPoints.get(ws.way);
660            } else {
661                is = new ArrayList<>();
662                insertPoints.put(ws.way, is);
663            }
664
665            is.add(ws.lowerIndex);
666        }
667
668        Set<Pair<Node, Node>> segSet = new HashSet<>();
669
670        for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
671            Way w = insertPoint.getKey();
672            List<Integer> is = insertPoint.getValue();
673
674            Way wnew = new Way(w);
675
676            pruneSuccsAndReverse(is);
677            for (int i : is) {
678                segSet.add(Pair.sort(new Pair<>(w.getNode(i), w.getNode(i+1))));
679                wnew.addNode(i + 1, n);
680            }
681
682            // If ALT is pressed, a new way should be created and that new way should get
683            // selected. This works everytime unless the ways the nodes get inserted into
684            // are already selected. This is the case when creating a self-overlapping way
685            // but pressing ALT prevents this. Therefore we must de-select the way manually
686            // here so /only/ the new way will be selected after this method finishes.
687            if (alt) {
688                newSelection.add(insertPoint.getKey());
689            }
690
691            cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
692            replacedWays.add(insertPoint.getKey());
693            reuseWays.add(wnew);
694        }
695
696        adjustNode(segSet, n);
697    }
698
699    /**
700     * Prevent creation of ways that look like this: &lt;----&gt;
701     * This happens if users want to draw a no-exit-sideway from the main way like this:
702     * ^
703     * |&lt;----&gt;
704     * |
705     * The solution isn't ideal because the main way will end in the side way, which is bad for
706     * navigation software ("drive straight on") but at least easier to fix. Maybe users will fix
707     * it on their own, too. At least it's better than producing an error.
708     *
709     * @param selectedWay the way to check
710     * @param currentNode the current node (i.e. the one the connection will be made from)
711     * @param targetNode the target node (i.e. the one the connection will be made to)
712     * @return {@code true} if this would create a selfcontaining way, {@code false} otherwise.
713     */
714    private boolean isSelfContainedWay(Way selectedWay, Node currentNode, Node targetNode) {
715        if (selectedWay != null) {
716            int posn0 = selectedWay.getNodes().indexOf(currentNode);
717            // CHECKSTYLE.OFF: SingleSpaceSeparator
718            if ((posn0 != -1 && // n0 is part of way
719                (posn0 >= 1                            && targetNode.equals(selectedWay.getNode(posn0-1)))) || // previous node
720                (posn0 < selectedWay.getNodesCount()-1 && targetNode.equals(selectedWay.getNode(posn0+1)))) {  // next node
721                getLayerManager().getEditDataSet().setSelected(targetNode);
722                lastUsedNode = targetNode;
723                return true;
724            }
725            // CHECKSTYLE.ON: SingleSpaceSeparator
726        }
727
728        return false;
729    }
730
731    /**
732     * Finds a node to continue drawing from. Decision is based upon given node and way.
733     * @param selectedNode Currently selected node, may be null
734     * @param selectedWay Currently selected way, may be null
735     * @return Node if a suitable node is found, null otherwise
736     */
737    private Node findNodeToContinueFrom(Node selectedNode, Way selectedWay) {
738        // No nodes or ways have been selected, this occurs when a relation
739        // has been selected or the selection is empty
740        if (selectedNode == null && selectedWay == null)
741            return null;
742
743        if (selectedNode == null) {
744            if (selectedWay.isFirstLastNode(lastUsedNode))
745                return lastUsedNode;
746
747            // We have a way selected, but no suitable node to continue from. Start anew.
748            return null;
749        }
750
751        if (selectedWay == null)
752            return selectedNode;
753
754        if (selectedWay.isFirstLastNode(selectedNode))
755            return selectedNode;
756
757        // We have a way and node selected, but it's not at the start/end of the way. Start anew.
758        return null;
759    }
760
761    @Override
762    public void mouseDragged(MouseEvent e) {
763        mouseMoved(e);
764    }
765
766    @Override
767    public void mouseMoved(MouseEvent e) {
768        if (!Main.map.mapView.isActiveLayerDrawable())
769            return;
770
771        // we copy ctrl/alt/shift from the event just in case our global
772        // keyDetector didn't make it through the security manager. Unclear
773        // if that can ever happen but better be safe.
774        updateKeyModifiers(e);
775        mousePos = e.getPoint();
776        if (snapHelper.isSnapOn() && ctrl)
777            tryToSetBaseSegmentForAngleSnap();
778
779        computeHelperLine();
780        addHighlighting();
781    }
782
783    /**
784     * This method is used to detect segment under mouse and use it as reference for angle snapping
785     */
786    private void tryToSetBaseSegmentForAngleSnap() {
787        WaySegment seg = Main.map.mapView.getNearestWaySegment(mousePos, OsmPrimitive::isSelectable);
788        if (seg != null) {
789            snapHelper.setBaseSegment(seg);
790        }
791    }
792
793    /**
794     * This method prepares data required for painting the "helper line" from
795     * the last used position to the mouse cursor. It duplicates some code from
796     * mouseReleased() (FIXME).
797     */
798    private void computeHelperLine() {
799        if (mousePos == null) {
800            // Don't draw the line.
801            currentMouseEastNorth = null;
802            currentBaseNode = null;
803            return;
804        }
805
806        Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
807
808        MapView mv = Main.map.mapView;
809        Node currentMouseNode = null;
810        mouseOnExistingNode = null;
811        mouseOnExistingWays = new HashSet<>();
812
813        showStatusInfo(-1, -1, -1, snapHelper.isSnapOn());
814
815        if (!ctrl && mousePos != null) {
816            currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive::isSelectable);
817        }
818
819        // We need this for highlighting and we'll only do so if we actually want to re-use
820        // *and* there is no node nearby (because nodes beat ways when re-using)
821        if (!ctrl && currentMouseNode == null) {
822            List<WaySegment> wss = mv.getNearestWaySegments(mousePos, OsmPrimitive::isSelectable);
823            for (WaySegment ws : wss) {
824                mouseOnExistingWays.add(ws.way);
825            }
826        }
827
828        if (currentMouseNode != null) {
829            // user clicked on node
830            if (selection.isEmpty()) return;
831            currentMouseEastNorth = currentMouseNode.getEastNorth();
832            mouseOnExistingNode = currentMouseNode;
833        } else {
834            // no node found in clicked area
835            currentMouseEastNorth = mv.getEastNorth(mousePos.x, mousePos.y);
836        }
837
838        determineCurrentBaseNodeAndPreviousNode(selection);
839        if (previousNode == null) {
840            snapHelper.noSnapNow();
841        }
842
843        if (getCurrentBaseNode() == null || getCurrentBaseNode() == currentMouseNode)
844            return; // Don't create zero length way segments.
845
846
847        double curHdg = Utils.toDegrees(getCurrentBaseNode().getEastNorth()
848                .heading(currentMouseEastNorth));
849        double baseHdg = -1;
850        if (previousNode != null) {
851            EastNorth en = previousNode.getEastNorth();
852            if (en != null) {
853                baseHdg = Utils.toDegrees(en.heading(getCurrentBaseNode().getEastNorth()));
854            }
855        }
856
857        snapHelper.checkAngleSnapping(currentMouseEastNorth, baseHdg, curHdg);
858
859        // status bar was filled by snapHelper
860    }
861
862    static void showStatusInfo(double angle, double hdg, double distance, boolean activeFlag) {
863        Main.map.statusLine.setAngle(angle);
864        Main.map.statusLine.activateAnglePanel(activeFlag);
865        Main.map.statusLine.setHeading(hdg);
866        Main.map.statusLine.setDist(distance);
867    }
868
869    /**
870     * Helper function that sets fields currentBaseNode and previousNode
871     * @param selection
872     * uses also lastUsedNode field
873     */
874    private void determineCurrentBaseNodeAndPreviousNode(Collection<OsmPrimitive> selection) {
875        Node selectedNode = null;
876        Way selectedWay = null;
877        for (OsmPrimitive p : selection) {
878            if (p instanceof Node) {
879                if (selectedNode != null)
880                    return;
881                selectedNode = (Node) p;
882            } else if (p instanceof Way) {
883                if (selectedWay != null)
884                    return;
885                selectedWay = (Way) p;
886            }
887        }
888        // we are here, if not more than 1 way or node is selected,
889
890        // the node from which we make a connection
891        currentBaseNode = null;
892        previousNode = null;
893
894        // Try to find an open way to measure angle from it. The way is not to be continued!
895        // warning: may result in changes of currentBaseNode and previousNode
896        // please remove if bugs arise
897        if (selectedWay == null && selectedNode != null) {
898            for (OsmPrimitive p: selectedNode.getReferrers()) {
899                if (p.isUsable() && p instanceof Way && ((Way) p).isFirstLastNode(selectedNode)) {
900                    if (selectedWay != null) { // two uncontinued ways, nothing to take as reference
901                        selectedWay = null;
902                        break;
903                    } else {
904                        // set us ~continue this way (measure angle from it)
905                        selectedWay = (Way) p;
906                    }
907                }
908            }
909        }
910
911        if (selectedNode == null) {
912            if (selectedWay == null)
913                return;
914            continueWayFromNode(selectedWay, lastUsedNode);
915        } else if (selectedWay == null) {
916            currentBaseNode = selectedNode;
917        } else if (!selectedWay.isDeleted()) { // fix #7118
918            continueWayFromNode(selectedWay, selectedNode);
919        }
920    }
921
922    /**
923     * if one of the ends of {@code way} is given {@code node},
924     * then set currentBaseNode = node and previousNode = adjacent node of way
925     * @param way way to continue
926     * @param node starting node
927     */
928    private void continueWayFromNode(Way way, Node node) {
929        int n = way.getNodesCount();
930        if (node == way.firstNode()) {
931            currentBaseNode = node;
932            if (n > 1) previousNode = way.getNode(1);
933        } else if (node == way.lastNode()) {
934            currentBaseNode = node;
935            if (n > 1) previousNode = way.getNode(n-2);
936        }
937    }
938
939    /**
940     * Repaint on mouse exit so that the helper line goes away.
941     */
942    @Override
943    public void mouseExited(MouseEvent e) {
944        OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
945        if (editLayer == null)
946            return;
947        mousePos = e.getPoint();
948        snapHelper.noSnapNow();
949        boolean repaintIssued = removeHighlighting();
950        // force repaint in case snapHelper needs one. If removeHighlighting
951        // caused one already, don't do it again.
952        if (!repaintIssued) {
953            editLayer.invalidate();
954        }
955    }
956
957    /**
958     * @param n node
959     * @return If the node is the end of exactly one way, return this.
960     *  <code>null</code> otherwise.
961     */
962    public static Way getWayForNode(Node n) {
963        Way way = null;
964        for (Way w : Utils.filteredCollection(n.getReferrers(), Way.class)) {
965            if (!w.isUsable() || w.getNodesCount() < 1) {
966                continue;
967            }
968            Node firstNode = w.getNode(0);
969            Node lastNode = w.getNode(w.getNodesCount() - 1);
970            if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) {
971                if (way != null)
972                    return null;
973                way = w;
974            }
975        }
976        return way;
977    }
978
979    /**
980     * Replies the current base node, after having checked it is still usable (see #11105).
981     * @return the current base node (can be null). If not-null, it's guaranteed the node is usable
982     */
983    public Node getCurrentBaseNode() {
984        if (currentBaseNode != null && (currentBaseNode.getDataSet() == null || !currentBaseNode.isUsable())) {
985            currentBaseNode = null;
986        }
987        return currentBaseNode;
988    }
989
990    private static void pruneSuccsAndReverse(List<Integer> is) {
991        Set<Integer> is2 = new HashSet<>();
992        for (int i : is) {
993            if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
994                is2.add(i);
995            }
996        }
997        is.clear();
998        is.addAll(is2);
999        Collections.sort(is);
1000        Collections.reverse(is);
1001    }
1002
1003    /**
1004     * Adjusts the position of a node to lie on a segment (or a segment intersection).
1005     *
1006     * If one or more than two segments are passed, the node is adjusted
1007     * to lie on the first segment that is passed.
1008     *
1009     * If two segments are passed, the node is adjusted to be at their intersection.
1010     *
1011     * No action is taken if no segments are passed.
1012     *
1013     * @param segs the segments to use as a reference when adjusting
1014     * @param n the node to adjust
1015     */
1016    private static void adjustNode(Collection<Pair<Node, Node>> segs, Node n) {
1017        switch (segs.size()) {
1018        case 0:
1019            return;
1020        case 2:
1021            adjustNodeTwoSegments(segs, n);
1022            break;
1023        default:
1024            adjustNodeDefault(segs, n);
1025        }
1026    }
1027
1028    private static void adjustNodeTwoSegments(Collection<Pair<Node, Node>> segs, Node n) {
1029        // This computes the intersection between the two segments and adjusts the node position.
1030        Iterator<Pair<Node, Node>> i = segs.iterator();
1031        Pair<Node, Node> seg = i.next();
1032        EastNorth pA = seg.a.getEastNorth();
1033        EastNorth pB = seg.b.getEastNorth();
1034        seg = i.next();
1035        EastNorth pC = seg.a.getEastNorth();
1036        EastNorth pD = seg.b.getEastNorth();
1037
1038        double u = det(pB.east() - pA.east(), pB.north() - pA.north(), pC.east() - pD.east(), pC.north() - pD.north());
1039
1040        // Check for parallel segments and do nothing if they are
1041        // In practice this will probably only happen when a way has been duplicated
1042
1043        if (u == 0)
1044            return;
1045
1046        // q is a number between 0 and 1
1047        // It is the point in the segment where the intersection occurs
1048        // if the segment is scaled to length 1
1049
1050        double q = det(pB.north() - pC.north(), pB.east() - pC.east(), pD.north() - pC.north(), pD.east() - pC.east()) / u;
1051        EastNorth intersection = new EastNorth(
1052                pB.east() + q * (pA.east() - pB.east()),
1053                pB.north() + q * (pA.north() - pB.north()));
1054
1055
1056        // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise
1057        // fall through to default action.
1058        // (for semi-parallel lines, intersection might be miles away!)
1059        if (Main.map.mapView.getPoint2D(n).distance(Main.map.mapView.getPoint2D(intersection)) < SNAP_TO_INTERSECTION_THRESHOLD.get()) {
1060            n.setEastNorth(intersection);
1061            return;
1062        }
1063
1064        adjustNodeDefault(segs, n);
1065    }
1066
1067    private static void adjustNodeDefault(Collection<Pair<Node, Node>> segs, Node n) {
1068        EastNorth p = n.getEastNorth();
1069        Pair<Node, Node> seg = segs.iterator().next();
1070        EastNorth pA = seg.a.getEastNorth();
1071        EastNorth pB = seg.b.getEastNorth();
1072        double a = p.distanceSq(pB);
1073        double b = p.distanceSq(pA);
1074        double c = pA.distanceSq(pB);
1075        double q = (a - b + c) / (2*c);
1076        n.setEastNorth(new EastNorth(pB.east() + q * (pA.east() - pB.east()), pB.north() + q * (pA.north() - pB.north())));
1077    }
1078
1079    // helper for adjustNode
1080    static double det(double a, double b, double c, double d) {
1081        return a * d - b * c;
1082    }
1083
1084    private void tryToMoveNodeOnIntersection(List<WaySegment> wss, Node n) {
1085        if (wss.isEmpty())
1086            return;
1087        WaySegment ws = wss.get(0);
1088        EastNorth p1 = ws.getFirstNode().getEastNorth();
1089        EastNorth p2 = ws.getSecondNode().getEastNorth();
1090        if (snapHelper.dir2 != null && getCurrentBaseNode() != null) {
1091            EastNorth xPoint = Geometry.getSegmentSegmentIntersection(p1, p2, snapHelper.dir2,
1092                    getCurrentBaseNode().getEastNorth());
1093            if (xPoint != null) {
1094                n.setEastNorth(xPoint);
1095            }
1096        }
1097    }
1098
1099    /**
1100     * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
1101     * (if feature enabled). Also sets the target cursor if appropriate. It adds the to-be-
1102     * highlighted primitives to newHighlights but does not actually highlight them. This work is
1103     * done in redrawIfRequired. This means, calling addHighlighting() without redrawIfRequired()
1104     * will leave the data in an inconsistent state.
1105     *
1106     * The status bar derives its information from oldHighlights, so in order to update the status
1107     * bar both addHighlighting() and repaintIfRequired() are needed, since former fills newHighlights
1108     * and latter processes them into oldHighlights.
1109     */
1110    private void addHighlighting() {
1111        newHighlights = new HashSet<>();
1112
1113        // if ctrl key is held ("no join"), don't highlight anything
1114        if (ctrl) {
1115            Main.map.mapView.setNewCursor(cursor, this);
1116            redrawIfRequired();
1117            return;
1118        }
1119
1120        // This happens when nothing is selected, but we still want to highlight the "target node"
1121        if (mouseOnExistingNode == null && mousePos != null && getLayerManager().getEditDataSet().selectionEmpty()) {
1122            mouseOnExistingNode = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive::isSelectable);
1123        }
1124
1125        if (mouseOnExistingNode != null) {
1126            Main.map.mapView.setNewCursor(cursorJoinNode, this);
1127            newHighlights.add(mouseOnExistingNode);
1128            redrawIfRequired();
1129            return;
1130        }
1131
1132        // Insert the node into all the nearby way segments
1133        if (mouseOnExistingWays.isEmpty()) {
1134            Main.map.mapView.setNewCursor(cursor, this);
1135            redrawIfRequired();
1136            return;
1137        }
1138
1139        Main.map.mapView.setNewCursor(cursorJoinWay, this);
1140        newHighlights.addAll(mouseOnExistingWays);
1141        redrawIfRequired();
1142    }
1143
1144    /**
1145     * Removes target highlighting from primitives. Issues repaint if required.
1146     * @return true if a repaint has been issued.
1147     */
1148    private boolean removeHighlighting() {
1149        newHighlights = new HashSet<>();
1150        return redrawIfRequired();
1151    }
1152
1153    @Override
1154    public void paint(Graphics2D g, MapView mv, Bounds box) {
1155        // sanity checks
1156        if (Main.map.mapView == null || mousePos == null
1157                // don't draw line if we don't know where from or where to
1158                || currentMouseEastNorth == null || getCurrentBaseNode() == null
1159                // don't draw line if mouse is outside window
1160                || !Main.map.mapView.getState().getForView(mousePos.getX(), mousePos.getY()).isInView())
1161            return;
1162
1163        Graphics2D g2 = g;
1164        snapHelper.drawIfNeeded(g2, mv.getState());
1165        if (!DRAW_HELPER_LINE.get() || wayIsFinished || shift)
1166            return;
1167
1168        if (!snapHelper.isActive()) {
1169            g2.setColor(RUBBER_LINE_COLOR.get());
1170            g2.setStroke(RUBBER_LINE_STROKE.get());
1171            paintConstructionGeometry(mv, g2);
1172        } else if (DRAW_CONSTRUCTION_GEOMETRY.get()) {
1173            // else use color and stoke from  snapHelper.draw
1174            paintConstructionGeometry(mv, g2);
1175        }
1176    }
1177
1178    private void paintConstructionGeometry(MapView mv, Graphics2D g2) {
1179        MapPath2D b = new MapPath2D();
1180        MapViewPoint p1 = mv.getState().getPointFor(getCurrentBaseNode());
1181        MapViewPoint p2 = mv.getState().getPointFor(currentMouseEastNorth);
1182
1183        b.moveTo(p1);
1184        b.lineTo(p2);
1185
1186        // if alt key is held ("start new way"), draw a little perpendicular line
1187        if (alt) {
1188            START_WAY_INDICATOR.paintArrowAt(b, p1, p2);
1189        }
1190
1191        g2.draw(b);
1192        g2.setStroke(BASIC_STROKE);
1193    }
1194
1195    @Override
1196    public String getModeHelpText() {
1197        StringBuilder rv;
1198        /*
1199         *  No modifiers: all (Connect, Node Re-Use, Auto-Weld)
1200         *  CTRL: disables node re-use, auto-weld
1201         *  Shift: do not make connection
1202         *  ALT: make connection but start new way in doing so
1203         */
1204
1205        /*
1206         * Status line text generation is split into two parts to keep it maintainable.
1207         * First part looks at what will happen to the new node inserted on click and
1208         * the second part will look if a connection is made or not.
1209         *
1210         * Note that this help text is not absolutely accurate as it doesn't catch any special
1211         * cases (e.g. when preventing <---> ways). The only special that it catches is when
1212         * a way is about to be finished.
1213         *
1214         * First check what happens to the new node.
1215         */
1216
1217        // oldHighlights stores the current highlights. If this
1218        // list is empty we can assume that we won't do any joins
1219        if (ctrl || oldHighlights.isEmpty()) {
1220            rv = new StringBuilder(tr("Create new node."));
1221        } else {
1222            // oldHighlights may store a node or way, check if it's a node
1223            OsmPrimitive x = oldHighlights.iterator().next();
1224            if (x instanceof Node) {
1225                rv = new StringBuilder(tr("Select node under cursor."));
1226            } else {
1227                rv = new StringBuilder(trn("Insert new node into way.", "Insert new node into {0} ways.",
1228                        oldHighlights.size(), oldHighlights.size()));
1229            }
1230        }
1231
1232        /*
1233         * Check whether a connection will be made
1234         */
1235        if (!wayIsFinished && getCurrentBaseNode() != null) {
1236            if (alt) {
1237                rv.append(' ').append(tr("Start new way from last node."));
1238            } else {
1239                rv.append(' ').append(tr("Continue way from last node."));
1240            }
1241            if (snapHelper.isSnapOn()) {
1242                rv.append(' ').append(tr("Angle snapping active."));
1243            }
1244        }
1245
1246        Node n = mouseOnExistingNode;
1247        DataSet ds = getLayerManager().getEditDataSet();
1248        /*
1249         * Handle special case: Highlighted node == selected node => finish drawing
1250         */
1251        if (n != null && ds != null && ds.getSelectedNodes().contains(n)) {
1252            if (wayIsFinished) {
1253                rv = new StringBuilder(tr("Select node under cursor."));
1254            } else {
1255                rv = new StringBuilder(tr("Finish drawing."));
1256            }
1257        }
1258
1259        /*
1260         * Handle special case: Self-Overlapping or closing way
1261         */
1262        if (ds != null && !ds.getSelectedWays().isEmpty() && !wayIsFinished && !alt) {
1263            Way w = ds.getSelectedWays().iterator().next();
1264            for (Node m : w.getNodes()) {
1265                if (m.equals(mouseOnExistingNode) || mouseOnExistingWays.contains(w)) {
1266                    rv.append(' ').append(tr("Finish drawing."));
1267                    break;
1268                }
1269            }
1270        }
1271        return rv.toString();
1272    }
1273
1274    /**
1275     * Get selected primitives, while draw action is in progress.
1276     *
1277     * While drawing a way, technically the last node is selected.
1278     * This is inconvenient when the user tries to add/edit tags to the way.
1279     * For this case, this method returns the current way as selection,
1280     * to work around this issue.
1281     * Otherwise the normal selection of the current data layer is returned.
1282     * @return selected primitives, while draw action is in progress
1283     */
1284    public Collection<OsmPrimitive> getInProgressSelection() {
1285        DataSet ds = getLayerManager().getEditDataSet();
1286        if (ds == null) return null;
1287        if (getCurrentBaseNode() != null && !ds.selectionEmpty()) {
1288            Way continueFrom = getWayForNode(getCurrentBaseNode());
1289            if (continueFrom != null)
1290                return Collections.<OsmPrimitive>singleton(continueFrom);
1291        }
1292        return ds.getSelected();
1293    }
1294
1295    @Override
1296    public Collection<? extends OsmPrimitive> getPreservedPrimitives() {
1297        DataSet ds = getLayerManager().getEditDataSet();
1298        return ds != null ? ds.getSelected() : Collections.emptySet();
1299    }
1300
1301    @Override
1302    public boolean layerIsSupported(Layer l) {
1303        return l instanceof OsmDataLayer;
1304    }
1305
1306    @Override
1307    protected void updateEnabledState() {
1308        setEnabled(getLayerManager().getEditLayer() != null);
1309    }
1310
1311    @Override
1312    public void destroy() {
1313        super.destroy();
1314        snapChangeAction.destroy();
1315    }
1316
1317    /**
1318     * Undo the last command. Binded by default to backspace key.
1319     */
1320    public class BackSpaceAction extends AbstractAction {
1321
1322        @Override
1323        public void actionPerformed(ActionEvent e) {
1324            Main.main.undoRedo.undo();
1325            Command lastCmd = Main.main.undoRedo.getLastCommand();
1326            if (lastCmd == null) return;
1327            Node n = null;
1328            for (OsmPrimitive p: lastCmd.getParticipatingPrimitives()) {
1329                if (p instanceof Node) {
1330                    if (n == null) {
1331                        n = (Node) p; // found one node
1332                        wayIsFinished = false;
1333                    } else {
1334                        // if more than 1 node were affected by previous command,
1335                        // we have no way to continue, so we forget about found node
1336                        n = null;
1337                        break;
1338                    }
1339                }
1340            }
1341            // select last added node - maybe we will continue drawing from it
1342            if (n != null) {
1343                getLayerManager().getEditDataSet().addSelected(n);
1344            }
1345        }
1346    }
1347
1348    private class SnapChangeAction extends JosmAction {
1349        /**
1350         * Constructs a new {@code SnapChangeAction}.
1351         */
1352        SnapChangeAction() {
1353            super(tr("Angle snapping"), /* ICON() */ "anglesnap",
1354                    tr("Switch angle snapping mode while drawing"), null, false);
1355            putValue("help", ht("/Action/Draw/AngleSnap"));
1356        }
1357
1358        @Override
1359        public void actionPerformed(ActionEvent e) {
1360            if (snapHelper != null) {
1361                snapHelper.toggleSnapping();
1362            }
1363        }
1364
1365        @Override
1366        protected void updateEnabledState() {
1367            setEnabled(Main.map != null && Main.map.mapMode instanceof DrawAction);
1368        }
1369    }
1370}
Note: See TracBrowser for help on using the repository browser.