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