001// License: GPL. See LICENSE file for details.
002package org.openstreetmap.josm.actions.mapmode;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007import static org.openstreetmap.josm.tools.I18n.trn;
008
009import java.awt.AWTEvent;
010import java.awt.BasicStroke;
011import java.awt.Color;
012import java.awt.Component;
013import java.awt.Cursor;
014import java.awt.Graphics2D;
015import java.awt.KeyboardFocusManager;
016import java.awt.Point;
017import java.awt.Stroke;
018import java.awt.Toolkit;
019import java.awt.event.AWTEventListener;
020import java.awt.event.ActionEvent;
021import java.awt.event.ActionListener;
022import java.awt.event.InputEvent;
023import java.awt.event.KeyEvent;
024import java.awt.event.MouseEvent;
025import java.awt.event.MouseListener;
026import java.awt.geom.GeneralPath;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Iterator;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.TreeSet;
039
040import javax.swing.AbstractAction;
041import javax.swing.JCheckBoxMenuItem;
042import javax.swing.JFrame;
043import javax.swing.JMenuItem;
044import javax.swing.JOptionPane;
045import javax.swing.JPopupMenu;
046import javax.swing.SwingUtilities;
047import javax.swing.Timer;
048
049import org.openstreetmap.josm.Main;
050import org.openstreetmap.josm.actions.JosmAction;
051import org.openstreetmap.josm.command.AddCommand;
052import org.openstreetmap.josm.command.ChangeCommand;
053import org.openstreetmap.josm.command.Command;
054import org.openstreetmap.josm.command.SequenceCommand;
055import org.openstreetmap.josm.data.Bounds;
056import org.openstreetmap.josm.data.SelectionChangedListener;
057import org.openstreetmap.josm.data.coor.EastNorth;
058import org.openstreetmap.josm.data.coor.LatLon;
059import org.openstreetmap.josm.data.osm.DataSet;
060import org.openstreetmap.josm.data.osm.Node;
061import org.openstreetmap.josm.data.osm.OsmPrimitive;
062import org.openstreetmap.josm.data.osm.Way;
063import org.openstreetmap.josm.data.osm.WaySegment;
064import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
065import org.openstreetmap.josm.gui.MainMenu;
066import org.openstreetmap.josm.gui.MapFrame;
067import org.openstreetmap.josm.gui.MapView;
068import org.openstreetmap.josm.gui.NavigatableComponent;
069import org.openstreetmap.josm.gui.layer.Layer;
070import org.openstreetmap.josm.gui.layer.MapViewPaintable;
071import org.openstreetmap.josm.gui.layer.OsmDataLayer;
072import org.openstreetmap.josm.gui.util.GuiHelper;
073import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
074import org.openstreetmap.josm.tools.Geometry;
075import org.openstreetmap.josm.tools.ImageProvider;
076import org.openstreetmap.josm.tools.Pair;
077import org.openstreetmap.josm.tools.Shortcut;
078import org.openstreetmap.josm.tools.Utils;
079
080/**
081 * Mapmode to add nodes, create and extend ways.
082 */
083public class DrawAction extends MapMode implements MapViewPaintable, SelectionChangedListener, AWTEventListener {
084    final private Cursor cursorJoinNode;
085    final private Cursor cursorJoinWay;
086
087    private Node lastUsedNode = null;
088    private double PHI=Math.toRadians(90);
089    private double toleranceMultiplier;
090
091    private Node mouseOnExistingNode;
092    private Set<Way> mouseOnExistingWays = new HashSet<Way>();
093    // old highlights store which primitives are currently highlighted. This
094    // is true, even if target highlighting is disabled since the status bar
095    // derives its information from this list as well.
096    private Set<OsmPrimitive> oldHighlights = new HashSet<OsmPrimitive>();
097    // new highlights contains a list of primitives that should be highlighted
098    // but haven’t been so far. The idea is to compare old and new and only
099    // repaint if there are changes.
100    private Set<OsmPrimitive> newHighlights = new HashSet<OsmPrimitive>();
101    private boolean drawHelperLine;
102    private boolean wayIsFinished = false;
103    private boolean drawTargetHighlight;
104    private Point mousePos;
105    private Point oldMousePos;
106    private Color rubberLineColor;
107
108    private Node currentBaseNode;
109    private Node previousNode;
110    private EastNorth currentMouseEastNorth;
111
112    private final SnapHelper snapHelper = new SnapHelper();
113
114    private Shortcut backspaceShortcut;
115    private BackSpaceAction backspaceAction;
116    private final Shortcut snappingShortcut;
117
118    private final SnapChangeAction snapChangeAction;
119    private final JCheckBoxMenuItem snapCheckboxMenuItem;
120    private boolean useRepeatedShortcut;
121    private Stroke rubberLineStroke;
122    private static final BasicStroke BASIC_STROKE = new BasicStroke(1);
123
124    public DrawAction(MapFrame mapFrame) {
125        super(tr("Draw"), "node/autonode", tr("Draw nodes"),
126                Shortcut.registerShortcut("mapmode:draw", tr("Mode: {0}", tr("Draw")), KeyEvent.VK_A, Shortcut.DIRECT),
127                mapFrame, ImageProvider.getCursor("crosshair", null));
128
129        snappingShortcut = Shortcut.registerShortcut("mapmode:drawanglesnapping",
130                tr("Mode: Draw Angle snapping"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
131        snapChangeAction = new SnapChangeAction();
132        snapCheckboxMenuItem = addMenuItem();
133        snapHelper.setMenuCheckBox(snapCheckboxMenuItem);
134        backspaceShortcut = Shortcut.registerShortcut("mapmode:backspace",
135                tr("Backspace in Add mode"), KeyEvent.VK_BACK_SPACE, Shortcut.DIRECT);
136        backspaceAction = new BackSpaceAction();
137        cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode");
138        cursorJoinWay = ImageProvider.getCursor("crosshair", "joinway");
139    }
140
141    private JCheckBoxMenuItem addMenuItem() {
142        int n=Main.main.menu.editMenu.getItemCount();
143        for (int i=n-1;i>0;i--) {
144            JMenuItem item = Main.main.menu.editMenu.getItem(i);
145            if (item!=null && item.getAction() !=null && item.getAction() instanceof SnapChangeAction) {
146                Main.main.menu.editMenu.remove(i);
147            }
148        }
149        return MainMenu.addWithCheckbox(Main.main.menu.editMenu, snapChangeAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
150    }
151
152    /**
153     * Checks if a map redraw is required and does so if needed. Also updates the status bar
154     */
155    private boolean redrawIfRequired() {
156        updateStatusLine();
157        // repaint required if the helper line is active.
158        boolean needsRepaint = drawHelperLine && !wayIsFinished;
159        if(drawTargetHighlight) {
160            // move newHighlights to oldHighlights; only update changed primitives
161            for(OsmPrimitive x : newHighlights) {
162                if(oldHighlights.contains(x)) {
163                    continue;
164                }
165                x.setHighlighted(true);
166                needsRepaint = true;
167            }
168            oldHighlights.removeAll(newHighlights);
169            for(OsmPrimitive x : oldHighlights) {
170                x.setHighlighted(false);
171                needsRepaint = true;
172            }
173        }
174        // required in order to print correct help text
175        oldHighlights = newHighlights;
176
177        if (!needsRepaint && !drawTargetHighlight)
178            return false;
179
180        // update selection to reflect which way being modified
181        if (currentBaseNode != null && getCurrentDataSet() != null && !getCurrentDataSet().getSelected().isEmpty()) {
182            Way continueFrom = getWayForNode(currentBaseNode);
183            if (alt && continueFrom != null && (!currentBaseNode.isSelected() || continueFrom.isSelected())) {
184                getCurrentDataSet().beginUpdate(); // to prevent the selection listener to screw around with the state
185                getCurrentDataSet().addSelected(currentBaseNode);
186                getCurrentDataSet().clearSelection(continueFrom);
187                getCurrentDataSet().endUpdate();
188                needsRepaint = true;
189            } else if (!alt && continueFrom != null && !continueFrom.isSelected()) {
190                getCurrentDataSet().addSelected(continueFrom);
191                needsRepaint = true;
192            }
193        }
194
195        if(needsRepaint) {
196            Main.map.mapView.repaint();
197        }
198        return needsRepaint;
199    }
200
201    @Override
202    public void enterMode() {
203        if (!isEnabled())
204            return;
205        super.enterMode();
206
207        rubberLineColor = Main.pref.getColor(marktr("helper line"), null);
208        if (rubberLineColor == null) rubberLineColor = PaintColors.SELECTED.get();
209
210        rubberLineStroke = GuiHelper.getCustomizedStroke(Main.pref.get("draw.stroke.helper-line","3"));
211        drawHelperLine = Main.pref.getBoolean("draw.helper-line", true);
212        drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true);
213
214        // determine if selection is suitable to continue drawing. If it
215        // isn't, set wayIsFinished to true to avoid superfluous repaints.
216        determineCurrentBaseNodeAndPreviousNode(getCurrentDataSet().getSelected());
217        wayIsFinished = currentBaseNode == null;
218
219        toleranceMultiplier = 0.01 * NavigatableComponent.PROP_SNAP_DISTANCE.get();
220
221        snapHelper.init();
222        snapCheckboxMenuItem.getAction().setEnabled(true);
223
224        timer = new Timer(0, new ActionListener() {
225            @Override
226            public void actionPerformed(ActionEvent ae) {
227                timer.stop();
228                if (set.remove(releaseEvent.getKeyCode())) {
229                    doKeyReleaseEvent(releaseEvent);
230                }
231            }
232
233        });
234        Main.map.statusLine.getAnglePanel().addMouseListener(snapHelper.anglePopupListener);
235        Main.registerActionShortcut(backspaceAction, backspaceShortcut);
236
237        Main.map.mapView.addMouseListener(this);
238        Main.map.mapView.addMouseMotionListener(this);
239        Main.map.mapView.addTemporaryLayer(this);
240        DataSet.addSelectionListener(this);
241
242        try {
243            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
244        } catch (SecurityException ex) {
245            Main.warn(ex);
246        }
247        // would like to but haven't got mouse position yet:
248        // computeHelperLine(false, false, false);
249    }
250
251    @Override
252    public void exitMode() {
253        super.exitMode();
254        Main.map.mapView.removeMouseListener(this);
255        Main.map.mapView.removeMouseMotionListener(this);
256        Main.map.mapView.removeTemporaryLayer(this);
257        DataSet.removeSelectionListener(this);
258        Main.unregisterActionShortcut(backspaceAction, backspaceShortcut);
259        snapHelper.unsetFixedMode();
260        snapCheckboxMenuItem.getAction().setEnabled(false);
261
262        Main.map.statusLine.getAnglePanel().removeMouseListener(snapHelper.anglePopupListener);
263        Main.map.statusLine.activateAnglePanel(false);
264
265        removeHighlighting();
266        try {
267            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
268        } catch (SecurityException ex) {
269        }
270
271        // when exiting we let everybody know about the currently selected
272        // primitives
273        //
274        DataSet ds = getCurrentDataSet();
275        if(ds != null) {
276            ds.fireSelectionChanged();
277        }
278    }
279
280    /**
281     * redraw to (possibly) get rid of helper line if selection changes.
282     */
283    @Override
284    public void eventDispatched(AWTEvent event) {
285        if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable())
286            return;
287        if (event instanceof KeyEvent) {
288            KeyEvent e = (KeyEvent) event;
289            if (snappingShortcut.isEvent(e) || (useRepeatedShortcut && getShortcut().isEvent(e))) {
290                Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
291                if (SwingUtilities.getWindowAncestor(focused) instanceof JFrame) {
292                    processKeyEvent(e);
293                }
294            }
295        } //  toggle angle snapping
296        updateKeyModifiers((InputEvent) event);
297        computeHelperLine();
298        addHighlighting();
299    }
300
301    // events for crossplatform key holding processing
302    // thanks to http://www.arco.in-berlin.de/keyevent.html
303    private final Set<Integer> set = new TreeSet<Integer>();
304    private KeyEvent releaseEvent;
305    private Timer timer;
306    void processKeyEvent(KeyEvent e) {
307        if (!snappingShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e)))
308            return;
309
310        if (e.getID() == KeyEvent.KEY_PRESSED) {
311            if (timer.isRunning()) {
312                timer.stop();
313            } else if (set.add((e.getKeyCode()))) {
314                doKeyPressEvent(e);
315            }
316        } else if (e.getID() == KeyEvent.KEY_RELEASED) {
317            if (timer.isRunning()) {
318                timer.stop();
319                if (set.remove(e.getKeyCode())) {
320                    doKeyReleaseEvent(e);
321                }
322            } else {
323                releaseEvent = e;
324                timer.restart();
325            }
326        }
327    }
328
329    private void doKeyPressEvent(KeyEvent e) {
330        snapHelper.setFixedMode();
331        computeHelperLine();
332        redrawIfRequired();
333    }
334    private void doKeyReleaseEvent(KeyEvent e) {
335        snapHelper.unFixOrTurnOff();
336        computeHelperLine();
337        redrawIfRequired();
338    }
339
340    /**
341     * redraw to (possibly) get rid of helper line if selection changes.
342     */
343    @Override
344    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
345        if(!Main.map.mapView.isActiveLayerDrawable())
346            return;
347        computeHelperLine();
348        addHighlighting();
349    }
350
351    private void tryAgain(MouseEvent e) {
352        getCurrentDataSet().setSelected();
353        mouseReleased(e);
354    }
355
356    /**
357     * This function should be called when the user wishes to finish his current draw action.
358     * If Potlatch Style is enabled, it will switch to select tool, otherwise simply disable
359     * the helper line until the user chooses to draw something else.
360     */
361    private void finishDrawing() {
362        // let everybody else know about the current selection
363        //
364        Main.main.getCurrentDataSet().fireSelectionChanged();
365        lastUsedNode = null;
366        wayIsFinished = true;
367        Main.map.selectSelectTool(true);
368        snapHelper.noSnapNow();
369
370        // Redraw to remove the helper line stub
371        computeHelperLine();
372        removeHighlighting();
373    }
374
375    private Point rightClickPressPos;
376
377    @Override
378    public void mousePressed(MouseEvent e) {
379        if (e.getButton() == MouseEvent.BUTTON3) {
380            rightClickPressPos = e.getPoint();
381        }
382    }
383
384    /**
385     * If user clicked with the left button, add a node at the current mouse
386     * position.
387     *
388     * If in nodeway mode, insert the node into the way.
389     */
390    @Override public void mouseReleased(MouseEvent e) {
391        if (e.getButton() == MouseEvent.BUTTON3) {
392            Point curMousePos = e.getPoint();
393            if (curMousePos.equals(rightClickPressPos)) {
394                tryToSetBaseSegmentForAngleSnap();
395            }
396            return;
397        }
398        if (e.getButton() != MouseEvent.BUTTON1)
399            return;
400        if(!Main.map.mapView.isActiveLayerDrawable())
401            return;
402        // request focus in order to enable the expected keyboard shortcuts
403        //
404        Main.map.mapView.requestFocus();
405
406        if(e.getClickCount() > 1 && mousePos != null && mousePos.equals(oldMousePos)) {
407            // A double click equals "user clicked last node again, finish way"
408            // Change draw tool only if mouse position is nearly the same, as
409            // otherwise fast clicks will count as a double click
410            finishDrawing();
411            return;
412        }
413        oldMousePos = mousePos;
414
415        // we copy ctrl/alt/shift from the event just in case our global
416        // AWTEvent didn't make it through the security manager. Unclear
417        // if that can ever happen but better be safe.
418        updateKeyModifiers(e);
419        mousePos = e.getPoint();
420
421        DataSet ds = getCurrentDataSet();
422        Collection<OsmPrimitive> selection = new ArrayList<OsmPrimitive>(ds.getSelected());
423        Collection<Command> cmds = new LinkedList<Command>();
424        Collection<OsmPrimitive> newSelection = new LinkedList<OsmPrimitive>(ds.getSelected());
425
426        List<Way> reuseWays = new ArrayList<Way>(),
427                replacedWays = new ArrayList<Way>();
428        boolean newNode = false;
429        Node n = null;
430
431        n = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
432        if (ctrl) {
433            Iterator<Way> it = getCurrentDataSet().getSelectedWays().iterator();
434            if (it.hasNext()) {
435                // ctrl-click on node of selected way = reuse node despite of ctrl
436                if (!it.next().containsNode(n)) n = null;
437            } else {
438                n=null; // ctrl-click + no selected way = new node
439            }
440        }
441
442        if (n != null && !snapHelper.isActive()) {
443            // user clicked on node
444            if (selection.isEmpty() || wayIsFinished) {
445                // select the clicked node and do nothing else
446                // (this is just a convenience option so that people don't
447                // have to switch modes)
448
449                getCurrentDataSet().setSelected(n);
450                // If we extend/continue an existing way, select it already now to make it obvious
451                Way continueFrom = getWayForNode(n);
452                if (continueFrom != null) {
453                    getCurrentDataSet().addSelected(continueFrom);
454                }
455
456                // The user explicitly selected a node, so let him continue drawing
457                wayIsFinished = false;
458                return;
459            }
460        } else {
461            EastNorth newEN;
462            if (n!=null) {
463                EastNorth foundPoint = n.getEastNorth();
464                // project found node to snapping line
465                newEN = snapHelper.getSnapPoint(foundPoint);
466                // do not add new node if there is some node within snapping distance
467                double tolerance = Main.map.mapView.getDist100Pixel() * toleranceMultiplier;
468                if (foundPoint.distance(newEN) > tolerance) {
469                    n = new Node(newEN); // point != projected, so we create new node
470                    newNode = true;
471                }
472            } else { // n==null, no node found in clicked area
473                EastNorth mouseEN = Main.map.mapView.getEastNorth(e.getX(), e.getY());
474                newEN = snapHelper.isSnapOn() ? snapHelper.getSnapPoint(mouseEN) : mouseEN;
475                n = new Node(newEN); //create node at clicked point
476                newNode = true;
477            }
478            snapHelper.unsetFixedMode();
479        }
480
481        if (newNode) {
482            if (n.getCoor().isOutSideWorld()) {
483                JOptionPane.showMessageDialog(
484                        Main.parent,
485                        tr("Cannot add a node outside of the world."),
486                        tr("Warning"),
487                        JOptionPane.WARNING_MESSAGE
488                        );
489                return;
490            }
491            cmds.add(new AddCommand(n));
492
493            if (!ctrl) {
494                // Insert the node into all the nearby way segments
495                List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(
496                        Main.map.mapView.getPoint(n), OsmPrimitive.isSelectablePredicate);
497                if (snapHelper.isActive()) {
498                    tryToMoveNodeOnIntersection(wss,n);
499                }
500                insertNodeIntoAllNearbySegments(wss, n, newSelection, cmds, replacedWays, reuseWays);
501            }
502        }
503        // now "n" is newly created or reused node that shoud be added to some way
504
505        // This part decides whether or not a "segment" (i.e. a connection) is made to an
506        // existing node.
507
508        // For a connection to be made, the user must either have a node selected (connection
509        // is made to that node), or he must have a way selected *and* one of the endpoints
510        // of that way must be the last used node (connection is made to last used node), or
511        // he must have a way and a node selected (connection is made to the selected node).
512
513        // If the above does not apply, the selection is cleared and a new try is started
514
515        boolean extendedWay = false;
516        boolean wayIsFinishedTemp = wayIsFinished;
517        wayIsFinished = false;
518
519        // don't draw lines if shift is held
520        if (!selection.isEmpty() && !shift) {
521            Node selectedNode = null;
522            Way selectedWay = null;
523
524            for (OsmPrimitive p : selection) {
525                if (p instanceof Node) {
526                    if (selectedNode != null) {
527                        // Too many nodes selected to do something useful
528                        tryAgain(e);
529                        return;
530                    }
531                    selectedNode = (Node) p;
532                } else if (p instanceof Way) {
533                    if (selectedWay != null) {
534                        // Too many ways selected to do something useful
535                        tryAgain(e);
536                        return;
537                    }
538                    selectedWay = (Way) p;
539                }
540            }
541
542            // the node from which we make a connection
543            Node n0 = findNodeToContinueFrom(selectedNode, selectedWay);
544            // We have a selection but it isn't suitable. Try again.
545            if(n0 == null) {
546                tryAgain(e);
547                return;
548            }
549            if(!wayIsFinishedTemp){
550                if(isSelfContainedWay(selectedWay, n0, n))
551                    return;
552
553                // User clicked last node again, finish way
554                if(n0 == n) {
555                    finishDrawing();
556                    return;
557                }
558
559                // Ok we know now that we'll insert a line segment, but will it connect to an
560                // existing way or make a new way of its own? The "alt" modifier means that the
561                // user wants a new way.
562                Way way = alt ? null : (selectedWay != null) ? selectedWay : getWayForNode(n0);
563                Way wayToSelect;
564
565                // Don't allow creation of self-overlapping ways
566                if(way != null) {
567                    int nodeCount=0;
568                    for (Node p : way.getNodes())
569                        if(p.equals(n0)) {
570                            nodeCount++;
571                        }
572                    if(nodeCount > 1) {
573                        way = null;
574                    }
575                }
576
577                if (way == null) {
578                    way = new Way();
579                    way.addNode(n0);
580                    cmds.add(new AddCommand(way));
581                    wayToSelect = way;
582                } else {
583                    int i;
584                    if ((i = replacedWays.indexOf(way)) != -1) {
585                        way = reuseWays.get(i);
586                        wayToSelect = way;
587                    } else {
588                        wayToSelect = way;
589                        Way wnew = new Way(way);
590                        cmds.add(new ChangeCommand(way, wnew));
591                        way = wnew;
592                    }
593                }
594
595                // Connected to a node that's already in the way
596                if(way.containsNode(n)) {
597                    wayIsFinished = true;
598                    selection.clear();
599                }
600
601                // Add new node to way
602                if (way.getNode(way.getNodesCount() - 1) == n0) {
603                    way.addNode(n);
604                } else {
605                    way.addNode(0, n);
606                }
607
608                extendedWay = true;
609                newSelection.clear();
610                newSelection.add(wayToSelect);
611            }
612        }
613
614        String title;
615        if (!extendedWay) {
616            if (!newNode)
617                return; // We didn't do anything.
618            else if (reuseWays.isEmpty()) {
619                title = tr("Add node");
620            } else {
621                title = tr("Add node into way");
622                for (Way w : reuseWays) {
623                    newSelection.remove(w);
624                }
625            }
626            newSelection.clear();
627            newSelection.add(n);
628        } else if (!newNode) {
629            title = tr("Connect existing way to node");
630        } else if (reuseWays.isEmpty()) {
631            title = tr("Add a new node to an existing way");
632        } else {
633            title = tr("Add node into way and connect");
634        }
635
636        Command c = new SequenceCommand(title, cmds);
637
638        Main.main.undoRedo.add(c);
639        if(!wayIsFinished) {
640            lastUsedNode = n;
641        }
642
643        getCurrentDataSet().setSelected(newSelection);
644
645        // "viewport following" mode for tracing long features
646        // from aerial imagery or GPS tracks.
647        if (n != null && Main.map.mapView.viewportFollowing) {
648            Main.map.mapView.smoothScrollTo(n.getEastNorth());
649        }
650        computeHelperLine();
651        removeHighlighting();
652    }
653
654    private void insertNodeIntoAllNearbySegments(List<WaySegment> wss, Node n, Collection<OsmPrimitive> newSelection, Collection<Command> cmds, List<Way> replacedWays, List<Way> reuseWays) {
655        Map<Way, List<Integer>> insertPoints = new HashMap<Way, List<Integer>>();
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<Integer>();
662                insertPoints.put(ws.way, is);
663            }
664
665            is.add(ws.lowerIndex);
666        }
667
668        Set<Pair<Node,Node>> segSet = new HashSet<Pair<Node,Node>>();
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<Node,Node>(w.getNode(i), w.getNode(i+1))));
679            }
680            for (int i : is) {
681                wnew.addNode(i + 1, n);
682            }
683
684            // If ALT is pressed, a new way should be created and that new way should get
685            // selected. This works everytime unless the ways the nodes get inserted into
686            // are already selected. This is the case when creating a self-overlapping way
687            // but pressing ALT prevents this. Therefore we must de-select the way manually
688            // here so /only/ the new way will be selected after this method finishes.
689            if(alt) {
690                newSelection.add(insertPoint.getKey());
691            }
692
693            cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
694            replacedWays.add(insertPoint.getKey());
695            reuseWays.add(wnew);
696        }
697
698        adjustNode(segSet, n);
699    }
700
701    /**
702     * Prevent creation of ways that look like this: &lt;----&gt;
703     * This happens if users want to draw a no-exit-sideway from the main way like this:
704     * ^
705     * |&lt;----&gt;
706     * |
707     * The solution isn't ideal because the main way will end in the side way, which is bad for
708     * navigation software ("drive straight on") but at least easier to fix. Maybe users will fix
709     * it on their own, too. At least it's better than producing an error.
710     *
711     * @param selectedWay the way to check
712     * @param currentNode the current node (i.e. the one the connection will be made from)
713     * @param targetNode the target node (i.e. the one the connection will be made to)
714     * @return {@code true} if this would create a selfcontaining way, {@code false} otherwise.
715     */
716    private boolean isSelfContainedWay(Way selectedWay, Node currentNode, Node targetNode) {
717        if(selectedWay != null) {
718            int posn0 = selectedWay.getNodes().indexOf(currentNode);
719            if( posn0 != -1 && // n0 is part of way
720                    (posn0 >= 1                             && targetNode.equals(selectedWay.getNode(posn0-1))) || // previous node
721                    (posn0 < selectedWay.getNodesCount()-1) && targetNode.equals(selectedWay.getNode(posn0+1))) {  // next node
722                getCurrentDataSet().setSelected(targetNode);
723                lastUsedNode = targetNode;
724                return true;
725            }
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        // AWTEvent 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.isSelectablePredicate);
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        MapView mv = Main.map.mapView;
800        if (mousePos == null) {
801            // Don't draw the line.
802            currentMouseEastNorth = null;
803            currentBaseNode = null;
804            return;
805        }
806
807        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
808
809        Node currentMouseNode = null;
810        mouseOnExistingNode = null;
811        mouseOnExistingWays = new HashSet<Way>();
812
813        showStatusInfo(-1, -1, -1, snapHelper.isSnapOn());
814
815        if (!ctrl && mousePos != null) {
816            currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
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.isSelectablePredicate);
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 (currentBaseNode == null || currentBaseNode == currentMouseNode)
844            return; // Don't create zero length way segments.
845
846
847        double curHdg = Math.toDegrees(currentBaseNode.getEastNorth()
848                .heading(currentMouseEastNorth));
849        double baseHdg=-1;
850        if (previousNode != null) {
851            baseHdg =  Math.toDegrees(previousNode.getEastNorth()
852                    .heading(currentBaseNode.getEastNorth()));
853        }
854
855        snapHelper.checkAngleSnapping(currentMouseEastNorth,baseHdg, curHdg);
856
857        // status bar was filled by snapHelper
858    }
859
860    private void showStatusInfo(double angle, double hdg, double distance, boolean activeFlag) {
861        Main.map.statusLine.setAngle(angle);
862        Main.map.statusLine.activateAnglePanel(activeFlag);
863        Main.map.statusLine.setHeading(hdg);
864        Main.map.statusLine.setDist(distance);
865    }
866
867    /**
868     * Helper function that sets fields currentBaseNode and previousNode
869     * @param selection
870     * uses also lastUsedNode field
871     */
872    private void determineCurrentBaseNodeAndPreviousNode(Collection<OsmPrimitive>  selection) {
873        Node selectedNode = null;
874        Way selectedWay = null;
875        for (OsmPrimitive p : selection) {
876            if (p instanceof Node) {
877                if (selectedNode != null)
878                    return;
879                selectedNode = (Node) p;
880            } else if (p instanceof Way) {
881                if (selectedWay != null)
882                    return;
883                selectedWay = (Way) p;
884            }
885        }
886        // we are here, if not more than 1 way or node is selected,
887
888        // the node from which we make a connection
889        currentBaseNode = null;
890        previousNode = null;
891
892        // Try to find an open way to measure angle from it. The way is not to be continued!
893        // warning: may result in changes of currentBaseNode and previousNode
894        // please remove if bugs arise
895        if (selectedWay == null && selectedNode != null) {
896            for (OsmPrimitive p: selectedNode.getReferrers()) {
897                if (p.isUsable() && p instanceof Way && ((Way) p).isFirstLastNode(selectedNode)) {
898                    if (selectedWay!=null) { // two uncontinued ways, nothing to take as reference
899                        selectedWay=null;
900                        break;
901                    } else {
902                        // set us ~continue this way (measure angle from it)
903                        selectedWay = (Way) p;
904                    }
905                }
906            }
907        }
908
909        if (selectedNode == null) {
910            if (selectedWay == null)
911                return;
912            continueWayFromNode(selectedWay, lastUsedNode);
913        } else if (selectedWay == null) {
914            currentBaseNode = selectedNode;
915        } else if (!selectedWay.isDeleted()) { // fix #7118
916            continueWayFromNode(selectedWay, selectedNode);
917        }
918    }
919
920    /**
921     * if one of the ends of @param way is given @param node ,
922     * then set  currentBaseNode = node and previousNode = adjacent node of way
923     */
924    private void continueWayFromNode(Way way, Node node) {
925        int n = way.getNodesCount();
926        if (node == way.firstNode()){
927            currentBaseNode = node;
928            if (n>1) previousNode = way.getNode(1);
929        } else if (node == way.lastNode()) {
930            currentBaseNode = node;
931            if (n>1) previousNode = way.getNode(n-2);
932        }
933    }
934
935    /**
936     * Repaint on mouse exit so that the helper line goes away.
937     */
938    @Override public void mouseExited(MouseEvent e) {
939        if(!Main.map.mapView.isActiveLayerDrawable())
940            return;
941        mousePos = e.getPoint();
942        snapHelper.noSnapNow();
943        boolean repaintIssued = removeHighlighting();
944        // force repaint in case snapHelper needs one. If removeHighlighting
945        // caused one already, don’t do it again.
946        if(!repaintIssued) {
947            Main.map.mapView.repaint();
948        }
949    }
950
951    /**
952     * @return If the node is the end of exactly one way, return this.
953     *  <code>null</code> otherwise.
954     */
955    public static Way getWayForNode(Node n) {
956        Way way = null;
957        for (Way w : Utils.filteredCollection(n.getReferrers(), Way.class)) {
958            if (!w.isUsable() || w.getNodesCount() < 1) {
959                continue;
960            }
961            Node firstNode = w.getNode(0);
962            Node lastNode = w.getNode(w.getNodesCount() - 1);
963            if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) {
964                if (way != null)
965                    return null;
966                way = w;
967            }
968        }
969        return way;
970    }
971
972    public Node getCurrentBaseNode() {
973        return currentBaseNode;
974    }
975
976    private static void pruneSuccsAndReverse(List<Integer> is) {
977        HashSet<Integer> is2 = new HashSet<Integer>();
978        for (int i : is) {
979            if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
980                is2.add(i);
981            }
982        }
983        is.clear();
984        is.addAll(is2);
985        Collections.sort(is);
986        Collections.reverse(is);
987    }
988
989    /**
990     * Adjusts the position of a node to lie on a segment (or a segment
991     * intersection).
992     *
993     * If one or more than two segments are passed, the node is adjusted
994     * to lie on the first segment that is passed.
995     *
996     * If two segments are passed, the node is adjusted to be at their
997     * intersection.
998     *
999     * No action is taken if no segments are passed.
1000     *
1001     * @param segs the segments to use as a reference when adjusting
1002     * @param n the node to adjust
1003     */
1004    private static void adjustNode(Collection<Pair<Node,Node>> segs, Node n) {
1005
1006        switch (segs.size()) {
1007        case 0:
1008            return;
1009        case 2:
1010            // This computes the intersection between
1011            // the two segments and adjusts the node position.
1012            Iterator<Pair<Node,Node>> i = segs.iterator();
1013            Pair<Node,Node> seg = i.next();
1014            EastNorth A = seg.a.getEastNorth();
1015            EastNorth B = seg.b.getEastNorth();
1016            seg = i.next();
1017            EastNorth C = seg.a.getEastNorth();
1018            EastNorth D = seg.b.getEastNorth();
1019
1020            double u=det(B.east() - A.east(), B.north() - A.north(), C.east() - D.east(), C.north() - D.north());
1021
1022            // Check for parallel segments and do nothing if they are
1023            // In practice this will probably only happen when a way has been duplicated
1024
1025            if (u == 0)
1026                return;
1027
1028            // q is a number between 0 and 1
1029            // It is the point in the segment where the intersection occurs
1030            // if the segment is scaled to lenght 1
1031
1032            double q = det(B.north() - C.north(), B.east() - C.east(), D.north() - C.north(), D.east() - C.east()) / u;
1033            EastNorth intersection = new EastNorth(
1034                    B.east() + q * (A.east() - B.east()),
1035                    B.north() + q * (A.north() - B.north()));
1036
1037            int snapToIntersectionThreshold
1038            = Main.pref.getInteger("edit.snap-intersection-threshold",10);
1039
1040            // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise
1041            // fall through to default action.
1042            // (for semi-parallel lines, intersection might be miles away!)
1043            if (Main.map.mapView.getPoint2D(n).distance(Main.map.mapView.getPoint2D(intersection)) < snapToIntersectionThreshold) {
1044                n.setEastNorth(intersection);
1045                return;
1046            }
1047        default:
1048            EastNorth P = n.getEastNorth();
1049            seg = segs.iterator().next();
1050            A = seg.a.getEastNorth();
1051            B = seg.b.getEastNorth();
1052            double a = P.distanceSq(B);
1053            double b = P.distanceSq(A);
1054            double c = A.distanceSq(B);
1055            q = (a - b + c) / (2*c);
1056            n.setEastNorth(new EastNorth(B.east() + q * (A.east() - B.east()), B.north() + q * (A.north() - B.north())));
1057        }
1058    }
1059
1060    // helper for adjustNode
1061    static double det(double a, double b, double c, double d) {
1062        return a * d - b * c;
1063    }
1064
1065    private void tryToMoveNodeOnIntersection(List<WaySegment> wss, Node n) {
1066        if (wss.isEmpty())
1067            return;
1068        WaySegment ws = wss.get(0);
1069        EastNorth p1=ws.getFirstNode().getEastNorth();
1070        EastNorth p2=ws.getSecondNode().getEastNorth();
1071        if (snapHelper.dir2!=null && currentBaseNode!=null) {
1072            EastNorth xPoint = Geometry.getSegmentSegmentIntersection(p1, p2, snapHelper.dir2, currentBaseNode.getEastNorth());
1073            if (xPoint!=null) {
1074                n.setEastNorth(xPoint);
1075            }
1076        }
1077    }
1078    /**
1079     * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
1080     * (if feature enabled). Also sets the target cursor if appropriate. It adds the to-be-
1081     * highlighted primitives to newHighlights but does not actually highlight them. This work is
1082     * done in redrawIfRequired. This means, calling addHighlighting() without redrawIfRequired()
1083     * will leave the data in an inconsistent state.
1084     *
1085     * The status bar derives its information from oldHighlights, so in order to update the status
1086     * bar both addHighlighting() and repaintIfRequired() are needed, since former fills newHighlights
1087     * and latter processes them into oldHighlights.
1088     */
1089    private void addHighlighting() {
1090        newHighlights = new HashSet<OsmPrimitive>();
1091
1092        // if ctrl key is held ("no join"), don't highlight anything
1093        if (ctrl) {
1094            Main.map.mapView.setNewCursor(cursor, this);
1095            redrawIfRequired();
1096            return;
1097        }
1098
1099        // This happens when nothing is selected, but we still want to highlight the "target node"
1100        if (mouseOnExistingNode == null && getCurrentDataSet().getSelected().isEmpty()
1101                && mousePos != null) {
1102            mouseOnExistingNode = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
1103        }
1104
1105        if (mouseOnExistingNode != null) {
1106            Main.map.mapView.setNewCursor(cursorJoinNode, this);
1107            newHighlights.add(mouseOnExistingNode);
1108            redrawIfRequired();
1109            return;
1110        }
1111
1112        // Insert the node into all the nearby way segments
1113        if (mouseOnExistingWays.isEmpty()) {
1114            Main.map.mapView.setNewCursor(cursor, this);
1115            redrawIfRequired();
1116            return;
1117        }
1118
1119        Main.map.mapView.setNewCursor(cursorJoinWay, this);
1120        newHighlights.addAll(mouseOnExistingWays);
1121        redrawIfRequired();
1122    }
1123
1124    /**
1125     * Removes target highlighting from primitives. Issues repaint if required.
1126     * Returns true if a repaint has been issued.
1127     */
1128    private boolean removeHighlighting() {
1129        newHighlights = new HashSet<OsmPrimitive>();
1130        return redrawIfRequired();
1131    }
1132
1133    @Override
1134    public void paint(Graphics2D g, MapView mv, Bounds box) {
1135        // sanity checks
1136        if (Main.map.mapView == null || mousePos == null
1137                // don't draw line if we don't know where from or where to
1138                || currentBaseNode == null || currentMouseEastNorth == null
1139                // don't draw line if mouse is outside window
1140                || !Main.map.mapView.getBounds().contains(mousePos))
1141            return;
1142
1143        Graphics2D g2 = g;
1144        snapHelper.drawIfNeeded(g2,mv);
1145        if (!drawHelperLine || wayIsFinished || shift)
1146            return;
1147
1148        if (!snapHelper.isActive()) { // else use color and stoke from  snapHelper.draw
1149            g2.setColor(rubberLineColor);
1150            g2.setStroke(rubberLineStroke);
1151        } else if (!snapHelper.drawConstructionGeometry)
1152            return;
1153        GeneralPath b = new GeneralPath();
1154        Point p1=mv.getPoint(currentBaseNode);
1155        Point p2=mv.getPoint(currentMouseEastNorth);
1156
1157        double t = Math.atan2(p2.y-p1.y, p2.x-p1.x) + Math.PI;
1158
1159        b.moveTo(p1.x,p1.y); b.lineTo(p2.x, p2.y);
1160
1161        // if alt key is held ("start new way"), draw a little perpendicular line
1162        if (alt) {
1163            b.moveTo((int)(p1.x + 8*Math.cos(t+PHI)), (int)(p1.y + 8*Math.sin(t+PHI)));
1164            b.lineTo((int)(p1.x + 8*Math.cos(t-PHI)), (int)(p1.y + 8*Math.sin(t-PHI)));
1165        }
1166
1167        g2.draw(b);
1168        g2.setStroke(BASIC_STROKE);
1169    }
1170
1171    @Override
1172    public String getModeHelpText() {
1173        StringBuilder rv;
1174        /*
1175         *  No modifiers: all (Connect, Node Re-Use, Auto-Weld)
1176         *  CTRL: disables node re-use, auto-weld
1177         *  Shift: do not make connection
1178         *  ALT: make connection but start new way in doing so
1179         */
1180
1181        /*
1182         * Status line text generation is split into two parts to keep it maintainable.
1183         * First part looks at what will happen to the new node inserted on click and
1184         * the second part will look if a connection is made or not.
1185         *
1186         * Note that this help text is not absolutely accurate as it doesn't catch any special
1187         * cases (e.g. when preventing <---> ways). The only special that it catches is when
1188         * a way is about to be finished.
1189         *
1190         * First check what happens to the new node.
1191         */
1192
1193        // oldHighlights stores the current highlights. If this
1194        // list is empty we can assume that we won't do any joins
1195        if (ctrl || oldHighlights.isEmpty()) {
1196            rv = new StringBuilder(tr("Create new node."));
1197        } else {
1198            // oldHighlights may store a node or way, check if it's a node
1199            OsmPrimitive x = oldHighlights.iterator().next();
1200            if (x instanceof Node) {
1201                rv = new StringBuilder(tr("Select node under cursor."));
1202            } else {
1203                rv = new StringBuilder(trn("Insert new node into way.", "Insert new node into {0} ways.",
1204                        oldHighlights.size(), oldHighlights.size()));
1205            }
1206        }
1207
1208        /*
1209         * Check whether a connection will be made
1210         */
1211        if (currentBaseNode != null && !wayIsFinished) {
1212            if (alt) {
1213                rv.append(" ").append(tr("Start new way from last node."));
1214            } else {
1215                rv.append(" ").append(tr("Continue way from last node."));
1216            }
1217            if (snapHelper.isSnapOn()) {
1218                rv.append(" ").append(tr("Angle snapping active."));
1219            }
1220        }
1221
1222        Node n = mouseOnExistingNode;
1223        /*
1224         * Handle special case: Highlighted node == selected node => finish drawing
1225         */
1226        if (n != null && getCurrentDataSet() != null && getCurrentDataSet().getSelectedNodes().contains(n)) {
1227            if (wayIsFinished) {
1228                rv = new StringBuilder(tr("Select node under cursor."));
1229            } else {
1230                rv = new StringBuilder(tr("Finish drawing."));
1231            }
1232        }
1233
1234        /*
1235         * Handle special case: Self-Overlapping or closing way
1236         */
1237        if (getCurrentDataSet() != null && !getCurrentDataSet().getSelectedWays().isEmpty() && !wayIsFinished && !alt) {
1238            Way w = getCurrentDataSet().getSelectedWays().iterator().next();
1239            for (Node m : w.getNodes()) {
1240                if (m.equals(mouseOnExistingNode) || mouseOnExistingWays.contains(w)) {
1241                    rv.append(" ").append(tr("Finish drawing."));
1242                    break;
1243                }
1244            }
1245        }
1246        return rv.toString();
1247    }
1248
1249    /**
1250     * Get selected primitives, while draw action is in progress.
1251     *
1252     * While drawing a way, technically the last node is selected.
1253     * This is inconvenient when the user tries to add tags to the
1254     * way using a keyboard shortcut. In that case, this method returns
1255     * the current way as selection, to work around this issue.
1256     * Otherwise the normal selection of the current data layer is returned.
1257     */
1258    public Collection<OsmPrimitive> getInProgressSelection() {
1259        DataSet ds = getCurrentDataSet();
1260        if (ds == null) return null;
1261        if (currentBaseNode != null && !ds.getSelected().isEmpty()) {
1262            Way continueFrom = getWayForNode(currentBaseNode);
1263            if (alt && continueFrom != null)
1264                return Collections.<OsmPrimitive>singleton(continueFrom);
1265        }
1266        return ds.getSelected();
1267    }
1268
1269    @Override
1270    public boolean layerIsSupported(Layer l) {
1271        return l instanceof OsmDataLayer;
1272    }
1273
1274    @Override
1275    protected void updateEnabledState() {
1276        setEnabled(getEditLayer() != null);
1277    }
1278
1279    @Override
1280    public void destroy() {
1281        super.destroy();
1282        snapChangeAction.destroy();
1283    }
1284
1285    public class BackSpaceAction extends AbstractAction {
1286
1287        @Override
1288        public void actionPerformed(ActionEvent e) {
1289            Main.main.undoRedo.undo();
1290            Node n=null;
1291            Command lastCmd=Main.main.undoRedo.commands.peekLast();
1292            if (lastCmd==null) return;
1293            for (OsmPrimitive p: lastCmd.getParticipatingPrimitives()) {
1294                if (p instanceof Node) {
1295                    if (n==null) {
1296                        n=(Node) p; // found one node
1297                        wayIsFinished=false;
1298                    }  else {
1299                        // if more than 1 node were affected by previous command,
1300                        // we have no way to continue, so we forget about found node
1301                        n=null;
1302                        break;
1303                    }
1304                }
1305            }
1306            // select last added node - maybe we will continue drawing from it
1307            if (n!=null) {
1308                getCurrentDataSet().addSelected(n);
1309            }
1310        }
1311    }
1312
1313    private class SnapHelper {
1314        boolean snapOn; // snapping is turned on
1315
1316        private boolean active; // snapping is active for current mouse position
1317        private boolean fixed; // snap angle is fixed
1318        private boolean absoluteFix; // snap angle is absolute
1319
1320        private boolean drawConstructionGeometry;
1321        private boolean showProjectedPoint;
1322        private boolean showAngle;
1323
1324        private boolean snapToProjections;
1325
1326        EastNorth dir2;
1327        EastNorth projected;
1328        String labelText;
1329        double lastAngle;
1330
1331        double customBaseHeading=-1; // angle of base line, if not last segment)
1332        private EastNorth segmentPoint1; // remembered first point of base segment
1333        private EastNorth segmentPoint2; // remembered second point of base segment
1334        private EastNorth projectionSource; // point that we are projecting to the line
1335
1336        double[] snapAngles;
1337        double snapAngleTolerance;
1338
1339        double pe,pn; // (pe,pn) - direction of snapping line
1340        double e0,n0; // (e0,n0) - origin of snapping line
1341
1342        final String fixFmt="%d "+tr("FIX");
1343        Color snapHelperColor;
1344        private Color highlightColor;
1345
1346        private Stroke normalStroke;
1347        private Stroke helperStroke;
1348        private Stroke highlightStroke;
1349
1350        JCheckBoxMenuItem checkBox;
1351        public final Color ORANGE_TRANSPARENT = new Color(Color.ORANGE.getRed(),Color.ORANGE.getGreen(),Color.ORANGE.getBlue(),128);
1352
1353        public void init() {
1354            snapOn=false;
1355            checkBox.setState(snapOn);
1356            fixed=false; absoluteFix=false;
1357
1358            Collection<String> angles = Main.pref.getCollection("draw.anglesnap.angles",
1359                    Arrays.asList("0","30","45","60","90","120","135","150","180"));
1360
1361            snapAngles = new double[2*angles.size()];
1362            int i=0;
1363            for (String s: angles) {
1364                try {
1365                    snapAngles[i] = Double.parseDouble(s); i++;
1366                    snapAngles[i] = 360-Double.parseDouble(s); i++;
1367                } catch (NumberFormatException e) {
1368                    Main.warn("Incorrect number in draw.anglesnap.angles preferences: "+s);
1369                    snapAngles[i]=0;i++;
1370                    snapAngles[i]=0;i++;
1371                }
1372            }
1373            snapAngleTolerance = Main.pref.getDouble("draw.anglesnap.tolerance", 5.0);
1374            drawConstructionGeometry = Main.pref.getBoolean("draw.anglesnap.drawConstructionGeometry", true);
1375            showProjectedPoint = Main.pref.getBoolean("draw.anglesnap.drawProjectedPoint", true);
1376            snapToProjections = Main.pref.getBoolean("draw.anglesnap.projectionsnap", true);
1377
1378            showAngle = Main.pref.getBoolean("draw.anglesnap.showAngle", true);
1379            useRepeatedShortcut = Main.pref.getBoolean("draw.anglesnap.toggleOnRepeatedA", true);
1380
1381            normalStroke = rubberLineStroke;
1382            snapHelperColor = Main.pref.getColor(marktr("draw angle snap"), Color.ORANGE);
1383
1384            highlightColor = Main.pref.getColor(marktr("draw angle snap highlight"), ORANGE_TRANSPARENT);
1385            highlightStroke = GuiHelper.getCustomizedStroke(Main.pref.get("draw.anglesnap.stroke.highlight","10"));
1386            helperStroke = GuiHelper.getCustomizedStroke(Main.pref.get("draw.anglesnap.stroke.helper","1 4"));
1387        }
1388
1389        public void saveAngles(String ... angles) {
1390            Main.pref.putCollection("draw.anglesnap.angles", Arrays.asList(angles));
1391        }
1392
1393        public  void setMenuCheckBox(JCheckBoxMenuItem checkBox) {
1394            this.checkBox = checkBox;
1395        }
1396
1397        public  void drawIfNeeded(Graphics2D g2, MapView mv) {
1398            if (!snapOn || !active)
1399                return;
1400            Point p1=mv.getPoint(currentBaseNode);
1401            Point p2=mv.getPoint(dir2);
1402            Point p3=mv.getPoint(projected);
1403            GeneralPath b;
1404            if (drawConstructionGeometry) {
1405                g2.setColor(snapHelperColor);
1406                g2.setStroke(helperStroke);
1407
1408                b = new GeneralPath();
1409                if (absoluteFix) {
1410                    b.moveTo(p2.x,p2.y);
1411                    b.lineTo(2*p1.x-p2.x,2*p1.y-p2.y); // bi-directional line
1412                } else {
1413                    b.moveTo(p2.x,p2.y);
1414                    b.lineTo(p3.x,p3.y);
1415                }
1416                g2.draw(b);
1417            }
1418            if (projectionSource != null) {
1419                g2.setColor(snapHelperColor);
1420                g2.setStroke(helperStroke);
1421                b = new GeneralPath();
1422                b.moveTo(p3.x,p3.y);
1423                Point pp=mv.getPoint(projectionSource);
1424                b.lineTo(pp.x,pp.y);
1425                g2.draw(b);
1426            }
1427
1428            if (customBaseHeading >= 0) {
1429                g2.setColor(highlightColor);
1430                g2.setStroke(highlightStroke);
1431                b = new GeneralPath();
1432                Point pp1=mv.getPoint(segmentPoint1);
1433                Point pp2=mv.getPoint(segmentPoint2);
1434                b.moveTo(pp1.x,pp1.y);
1435                b.lineTo(pp2.x,pp2.y);
1436                g2.draw(b);
1437            }
1438
1439            g2.setColor(rubberLineColor);
1440            g2.setStroke(normalStroke);
1441            b = new GeneralPath();
1442            b.moveTo(p1.x,p1.y);
1443            b.lineTo(p3.x,p3.y);
1444            g2.draw(b);
1445
1446            g2.drawString(labelText, p3.x-5, p3.y+20);
1447            if (showProjectedPoint) {
1448                g2.setStroke(normalStroke);
1449                g2.drawOval(p3.x-5, p3.y-5, 10, 10); // projected point
1450            }
1451
1452            g2.setColor(snapHelperColor);
1453            g2.setStroke(helperStroke);
1454        }
1455
1456        /* If mouse position is close to line at 15-30-45-... angle, remembers this direction
1457         */
1458        public void checkAngleSnapping(EastNorth currentEN, double baseHeading, double curHeading) {
1459            EastNorth p0 = currentBaseNode.getEastNorth();
1460            EastNorth snapPoint = currentEN;
1461            double angle = -1;
1462
1463            double activeBaseHeading = (customBaseHeading>=0)? customBaseHeading : baseHeading;
1464
1465            if (snapOn && (activeBaseHeading>=0)) {
1466                angle = curHeading - activeBaseHeading;
1467                if (angle < 0) {
1468                    angle+=360;
1469                }
1470                if (angle > 360) {
1471                    angle=0;
1472                }
1473
1474                double nearestAngle;
1475                if (fixed) {
1476                    nearestAngle = lastAngle; // if direction is fixed use previous angle
1477                    active = true;
1478                } else {
1479                    nearestAngle = getNearestAngle(angle);
1480                    if (getAngleDelta(nearestAngle, angle) < snapAngleTolerance) {
1481                        active = (customBaseHeading>=0)? true : Math.abs(nearestAngle - 180) > 1e-3;
1482                        // if angle is to previous segment, exclude 180 degrees
1483                        lastAngle = nearestAngle;
1484                    } else {
1485                        active=false;
1486                    }
1487                }
1488
1489                if (active) {
1490                    double phi;
1491                    e0 = p0.east();
1492                    n0 = p0.north();
1493                    buildLabelText((nearestAngle<=180) ? nearestAngle : nearestAngle-360);
1494
1495                    phi = (nearestAngle + activeBaseHeading) * Math.PI / 180;
1496                    // (pe,pn) - direction of snapping line
1497                    pe = Math.sin(phi);
1498                    pn = Math.cos(phi);
1499                    double scale = 20 * Main.map.mapView.getDist100Pixel();
1500                    dir2 = new EastNorth(e0 + scale * pe, n0 + scale * pn);
1501                    snapPoint = getSnapPoint(currentEN);
1502                } else {
1503                    noSnapNow();
1504                }
1505            }
1506
1507            // find out the distance, in metres, between the base point and projected point
1508            LatLon mouseLatLon = Main.map.mapView.getProjection().eastNorth2latlon(snapPoint);
1509            double distance = currentBaseNode.getCoor().greatCircleDistance(mouseLatLon);
1510            double hdg = Math.toDegrees(p0.heading(snapPoint));
1511            // heading of segment from current to calculated point, not to mouse position
1512
1513            if (baseHeading >=0 ) { // there is previous line segment with some heading
1514                angle = hdg - baseHeading;
1515                if (angle < 0) {
1516                    angle+=360;
1517                }
1518                if (angle > 360) {
1519                    angle=0;
1520                }
1521            }
1522            showStatusInfo(angle, hdg, distance, isSnapOn());
1523        }
1524
1525        private void buildLabelText(double nearestAngle) {
1526            if (showAngle) {
1527                if (fixed) {
1528                    if (absoluteFix) {
1529                        labelText = "=";
1530                    } else {
1531                        labelText = String.format(fixFmt, (int) nearestAngle);
1532                    }
1533                } else {
1534                    labelText = String.format("%d", (int) nearestAngle);
1535                }
1536            } else {
1537                if (fixed) {
1538                    if (absoluteFix) {
1539                        labelText = "=";
1540                    } else {
1541                        labelText = String.format(tr("FIX"), 0);
1542                    }
1543                } else {
1544                    labelText = "";
1545                }
1546            }
1547        }
1548
1549        public  EastNorth getSnapPoint(EastNorth p) {
1550            if (!active)
1551                return p;
1552            double de=p.east()-e0;
1553            double dn=p.north()-n0;
1554            double l = de*pe+dn*pn;
1555            double delta = Main.map.mapView.getDist100Pixel()/20;
1556            if (!absoluteFix && l<delta) {
1557                active=false;
1558                return p;
1559            } //  do not go backward!
1560
1561            projectionSource=null;
1562            if (snapToProjections) {
1563                DataSet ds = getCurrentDataSet();
1564                Collection<Way> selectedWays = ds.getSelectedWays();
1565                if (selectedWays.size()==1) {
1566                    Way w = selectedWays.iterator().next();
1567                    Collection <EastNorth> pointsToProject = new ArrayList<EastNorth>();
1568                    if (w.getNodesCount()<1000) {
1569                        for (Node n: w.getNodes()) {
1570                            pointsToProject.add(n.getEastNorth());
1571                        }
1572                    }
1573                    if (customBaseHeading >=0 ) {
1574                        pointsToProject.add(segmentPoint1);
1575                        pointsToProject.add(segmentPoint2);
1576                    }
1577                    EastNorth enOpt=null;
1578                    double dOpt=1e5;
1579                    for (EastNorth en: pointsToProject) { // searching for besht projection
1580                        double l1 = (en.east()-e0)*pe+(en.north()-n0)*pn;
1581                        double d1 = Math.abs(l1-l);
1582                        if (d1 < delta && d1 < dOpt) {
1583                            l=l1;
1584                            enOpt = en;
1585                            dOpt = d1;
1586                        }
1587                    }
1588                    if (enOpt!=null) {
1589                        projectionSource =  enOpt;
1590                    }
1591                }
1592            }
1593            return projected = new EastNorth(e0+l*pe, n0+l*pn);
1594        }
1595
1596
1597        public void noSnapNow() {
1598            active=false;
1599            dir2=null; projected=null;
1600            labelText=null;
1601        }
1602
1603        public void setBaseSegment(WaySegment seg) {
1604            if (seg==null) return;
1605            segmentPoint1=seg.getFirstNode().getEastNorth();
1606            segmentPoint2=seg.getSecondNode().getEastNorth();
1607
1608            double hdg = segmentPoint1.heading(segmentPoint2);
1609            hdg=Math.toDegrees(hdg);
1610            if (hdg<0) {
1611                hdg+=360;
1612            }
1613            if (hdg>360) {
1614                hdg-=360;
1615            }
1616            customBaseHeading=hdg;
1617        }
1618
1619        private void nextSnapMode() {
1620            if (snapOn) {
1621                // turn off snapping if we are in fixed mode or no actile snapping line exist
1622                if (fixed || !active) { snapOn=false; unsetFixedMode(); } else {
1623                    setFixedMode();
1624                }
1625            } else {
1626                snapOn=true;
1627                unsetFixedMode();
1628            }
1629            checkBox.setState(snapOn);
1630            customBaseHeading=-1;
1631        }
1632
1633        private void enableSnapping() {
1634            snapOn = true;
1635            checkBox.setState(snapOn);
1636            customBaseHeading=-1;
1637            unsetFixedMode();
1638        }
1639
1640        private void toggleSnapping() {
1641            snapOn = !snapOn;
1642            checkBox.setState(snapOn);
1643            customBaseHeading=-1;
1644            unsetFixedMode();
1645        }
1646
1647        public void setFixedMode() {
1648            if (active) {
1649                fixed=true;
1650            }
1651        }
1652
1653
1654        public  void unsetFixedMode() {
1655            fixed=false;
1656            absoluteFix=false;
1657            lastAngle=0;
1658            active=false;
1659        }
1660
1661        public  boolean isActive() {
1662            return active;
1663        }
1664
1665        public  boolean isSnapOn() {
1666            return snapOn;
1667        }
1668
1669        private double getNearestAngle(double angle) {
1670            double delta,minDelta=1e5, bestAngle=0.0;
1671            for (double snapAngle : snapAngles) {
1672                delta = getAngleDelta(angle, snapAngle);
1673                if (delta < minDelta) {
1674                    minDelta = delta;
1675                    bestAngle = snapAngle;
1676                }
1677            }
1678            if (Math.abs(bestAngle-360) < 1e-3) {
1679                bestAngle=0;
1680            }
1681            return bestAngle;
1682        }
1683
1684        private double getAngleDelta(double a, double b) {
1685            double delta = Math.abs(a-b);
1686            if (delta>180)
1687                return 360-delta;
1688            else
1689                return delta;
1690        }
1691
1692        private void unFixOrTurnOff() {
1693            if (absoluteFix) {
1694                unsetFixedMode();
1695            } else {
1696                toggleSnapping();
1697            }
1698        }
1699
1700        MouseListener anglePopupListener = new PopupMenuLauncher( new JPopupMenu() {
1701            JCheckBoxMenuItem repeatedCb = new JCheckBoxMenuItem(new AbstractAction(tr("Toggle snapping by {0}", getShortcut().getKeyText())){
1702                @Override public void actionPerformed(ActionEvent e) {
1703                    boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1704                    Main.pref.put("draw.anglesnap.toggleOnRepeatedA", sel);
1705                    init();
1706                }
1707            });
1708            JCheckBoxMenuItem helperCb = new JCheckBoxMenuItem(new AbstractAction(tr("Show helper geometry")){
1709                @Override public void actionPerformed(ActionEvent e) {
1710                    boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1711                    Main.pref.put("draw.anglesnap.drawConstructionGeometry", sel);
1712                    Main.pref.put("draw.anglesnap.drawProjectedPoint", sel);
1713                    Main.pref.put("draw.anglesnap.showAngle", sel);
1714                    init();
1715                    enableSnapping();
1716                }
1717            });
1718            JCheckBoxMenuItem projectionCb = new JCheckBoxMenuItem(new AbstractAction(tr("Snap to node projections")){
1719                @Override public void actionPerformed(ActionEvent e) {
1720                    boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1721                    Main.pref.put("draw.anglesnap.projectionsnap", sel);
1722                    init();
1723                    enableSnapping();
1724                }
1725            });
1726            {
1727                helperCb.setState(Main.pref.getBoolean("draw.anglesnap.drawConstructionGeometry",true));
1728                projectionCb.setState(Main.pref.getBoolean("draw.anglesnap.projectionsnapgvff",true));
1729                repeatedCb.setState(Main.pref.getBoolean("draw.anglesnap.toggleOnRepeatedA",true));
1730                add(repeatedCb);
1731                add(helperCb);
1732                add(projectionCb);
1733                add(new AbstractAction(tr("Disable")) {
1734                    @Override public void actionPerformed(ActionEvent e) {
1735                        saveAngles("180");
1736                        init();
1737                        enableSnapping();
1738                    }
1739                });
1740                add(new AbstractAction(tr("0,90,...")) {
1741                    @Override public void actionPerformed(ActionEvent e) {
1742                        saveAngles("0","90","180");
1743                        init();
1744                        enableSnapping();
1745                    }
1746                });
1747                add(new AbstractAction(tr("0,45,90,...")) {
1748                    @Override public void actionPerformed(ActionEvent e) {
1749                        saveAngles("0","45","90","135","180");
1750                        init();
1751                        enableSnapping();
1752                    }
1753                });
1754                add(new AbstractAction(tr("0,30,45,60,90,...")) {
1755                    @Override public void actionPerformed(ActionEvent e) {
1756                        saveAngles("0","30","45","60","90","120","135","150","180");
1757                        init();
1758                        enableSnapping();
1759                    }
1760                });
1761            }
1762        }) {
1763            @Override
1764            public void mouseClicked(MouseEvent e) {
1765                super.mouseClicked(e);
1766                if (e.getButton() == MouseEvent.BUTTON1) {
1767                    toggleSnapping();
1768                    updateStatusLine();
1769                }
1770            }
1771        };
1772    }
1773
1774    private class SnapChangeAction extends JosmAction {
1775        public SnapChangeAction() {
1776            super(tr("Angle snapping"), "anglesnap",
1777                    tr("Switch angle snapping mode while drawing"), null, false);
1778            putValue("help", ht("/Action/Draw/AngleSnap"));
1779        }
1780
1781        @Override
1782        public void actionPerformed(ActionEvent e) {
1783            if (snapHelper!=null) {
1784                snapHelper.toggleSnapping();
1785            }
1786        }
1787    }
1788}