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                    tryToSetBaseSegmentForAngleSnap();
381                }
382                return;
383            }
384            if (e.getButton() != MouseEvent.BUTTON1)
385                return;
386            if(!Main.map.mapView.isActiveLayerDrawable())
387                return;
388            // request focus in order to enable the expected keyboard shortcuts
389            //
390            Main.map.mapView.requestFocus();
391    
392            if(e.getClickCount() > 1 && mousePos != null && mousePos.equals(oldMousePos)) {
393                // A double click equals "user clicked last node again, finish way"
394                // Change draw tool only if mouse position is nearly the same, as
395                // otherwise fast clicks will count as a double click
396                finishDrawing();
397                return;
398            }
399            oldMousePos = mousePos;
400    
401            // we copy ctrl/alt/shift from the event just in case our global
402            // AWTEvent didn't make it through the security manager. Unclear
403            // if that can ever happen but better be safe.
404            updateKeyModifiers(e);
405            mousePos = e.getPoint();
406    
407            DataSet ds = getCurrentDataSet();
408            Collection<OsmPrimitive> selection = new ArrayList<OsmPrimitive>(ds.getSelected());
409            Collection<Command> cmds = new LinkedList<Command>();
410            Collection<OsmPrimitive> newSelection = new LinkedList<OsmPrimitive>(ds.getSelected());
411    
412            ArrayList<Way> reuseWays = new ArrayList<Way>(),
413                    replacedWays = new ArrayList<Way>();
414            boolean newNode = false;
415            Node n = null;
416    
417            if (!ctrl) {
418                n = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
419            }
420    
421            if (n != null && !snapHelper.isActive()) {
422                // user clicked on node
423                if (selection.isEmpty() || wayIsFinished) {
424                    // select the clicked node and do nothing else
425                    // (this is just a convenience option so that people don't
426                    // have to switch modes)
427    
428                    getCurrentDataSet().setSelected(n);
429                    // If we extend/continue an existing way, select it already now to make it obvious
430                    Way continueFrom = getWayForNode(n);
431                    if (continueFrom != null) {
432                        getCurrentDataSet().addSelected(continueFrom);
433                    }
434    
435                    // The user explicitly selected a node, so let him continue drawing
436                    wayIsFinished = false;
437                    return;
438                }
439            } else {
440                EastNorth newEN;
441                if (n!=null) {
442                    EastNorth foundPoint = n.getEastNorth();
443                    // project found node to snapping line
444                    newEN = snapHelper.getSnapPoint(foundPoint);
445                    if (foundPoint.distance(newEN) > 1e-4) {
446                        n = new Node(newEN); // point != projected, so we create new node
447                        newNode = true;
448                    }
449                } else { // n==null, no node found in clicked area
450                    EastNorth mouseEN = Main.map.mapView.getEastNorth(e.getX(), e.getY());
451                    newEN = snapHelper.isSnapOn() ? snapHelper.getSnapPoint(mouseEN) : mouseEN;
452                    n = new Node(newEN); //create node at clicked point
453                    newNode = true;
454                }
455                snapHelper.unsetFixedMode();
456            }
457    
458            if (newNode) {
459                if (n.getCoor().isOutSideWorld()) {
460                    JOptionPane.showMessageDialog(
461                            Main.parent,
462                            tr("Cannot add a node outside of the world."),
463                            tr("Warning"),
464                            JOptionPane.WARNING_MESSAGE
465                            );
466                    return;
467                }
468                cmds.add(new AddCommand(n));
469    
470                if (!ctrl) {
471                    // Insert the node into all the nearby way segments
472                    List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(
473                            Main.map.mapView.getPoint(n), OsmPrimitive.isSelectablePredicate);
474                    if (snapHelper.isActive()) {
475                        tryToMoveNodeOnIntersection(wss,n);
476                    }
477                    insertNodeIntoAllNearbySegments(wss, n, newSelection, cmds, replacedWays, reuseWays);
478                }
479            }
480            // now "n" is newly created or reused node that shoud be added to some way
481    
482            // This part decides whether or not a "segment" (i.e. a connection) is made to an
483            // existing node.
484    
485            // For a connection to be made, the user must either have a node selected (connection
486            // is made to that node), or he must have a way selected *and* one of the endpoints
487            // of that way must be the last used node (connection is made to last used node), or
488            // he must have a way and a node selected (connection is made to the selected node).
489    
490            // If the above does not apply, the selection is cleared and a new try is started
491    
492            boolean extendedWay = false;
493            boolean wayIsFinishedTemp = wayIsFinished;
494            wayIsFinished = false;
495    
496            // don't draw lines if shift is held
497            if (selection.size() > 0 && !shift) {
498                Node selectedNode = null;
499                Way selectedWay = null;
500    
501                for (OsmPrimitive p : selection) {
502                    if (p instanceof Node) {
503                        if (selectedNode != null) {
504                            // Too many nodes selected to do something useful
505                            tryAgain(e);
506                            return;
507                        }
508                        selectedNode = (Node) p;
509                    } else if (p instanceof Way) {
510                        if (selectedWay != null) {
511                            // Too many ways selected to do something useful
512                            tryAgain(e);
513                            return;
514                        }
515                        selectedWay = (Way) p;
516                    }
517                }
518    
519                // the node from which we make a connection
520                Node n0 = findNodeToContinueFrom(selectedNode, selectedWay);
521                // We have a selection but it isn't suitable. Try again.
522                if(n0 == null) {
523                    tryAgain(e);
524                    return;
525                }
526                if(!wayIsFinishedTemp){
527                    if(isSelfContainedWay(selectedWay, n0, n))
528                        return;
529    
530                    // User clicked last node again, finish way
531                    if(n0 == n) {
532                        finishDrawing();
533                        return;
534                    }
535    
536                    // Ok we know now that we'll insert a line segment, but will it connect to an
537                    // existing way or make a new way of its own? The "alt" modifier means that the
538                    // user wants a new way.
539                    Way way = alt ? null : (selectedWay != null) ? selectedWay : getWayForNode(n0);
540                    Way wayToSelect;
541    
542                    // Don't allow creation of self-overlapping ways
543                    if(way != null) {
544                        int nodeCount=0;
545                        for (Node p : way.getNodes())
546                            if(p.equals(n0)) {
547                                nodeCount++;
548                            }
549                        if(nodeCount > 1) {
550                            way = null;
551                        }
552                    }
553    
554                    if (way == null) {
555                        way = new Way();
556                        way.addNode(n0);
557                        cmds.add(new AddCommand(way));
558                        wayToSelect = way;
559                    } else {
560                        int i;
561                        if ((i = replacedWays.indexOf(way)) != -1) {
562                            way = reuseWays.get(i);
563                            wayToSelect = way;
564                        } else {
565                            wayToSelect = way;
566                            Way wnew = new Way(way);
567                            cmds.add(new ChangeCommand(way, wnew));
568                            way = wnew;
569                        }
570                    }
571    
572                    // Connected to a node that's already in the way
573                    if(way.containsNode(n)) {
574                        wayIsFinished = true;
575                        selection.clear();
576                    }
577    
578                    // Add new node to way
579                    if (way.getNode(way.getNodesCount() - 1) == n0) {
580                        way.addNode(n);
581                    } else {
582                        way.addNode(0, n);
583                    }
584    
585                    extendedWay = true;
586                    newSelection.clear();
587                    newSelection.add(wayToSelect);
588                }
589            }
590    
591            String title;
592            if (!extendedWay) {
593                if (!newNode)
594                    return; // We didn't do anything.
595                else if (reuseWays.isEmpty()) {
596                    title = tr("Add node");
597                } else {
598                    title = tr("Add node into way");
599                    for (Way w : reuseWays) {
600                        newSelection.remove(w);
601                    }
602                }
603                newSelection.clear();
604                newSelection.add(n);
605            } else if (!newNode) {
606                title = tr("Connect existing way to node");
607            } else if (reuseWays.isEmpty()) {
608                title = tr("Add a new node to an existing way");
609            } else {
610                title = tr("Add node into way and connect");
611            }
612    
613            Command c = new SequenceCommand(title, cmds);
614    
615            Main.main.undoRedo.add(c);
616            if(!wayIsFinished) {
617                lastUsedNode = n;
618            }
619    
620            getCurrentDataSet().setSelected(newSelection);
621    
622            // "viewport following" mode for tracing long features
623            // from aerial imagery or GPS tracks.
624            if (n != null && Main.map.mapView.viewportFollowing) {
625                Main.map.mapView.smoothScrollTo(n.getEastNorth());
626            };
627            computeHelperLine();
628            removeHighlighting();
629        }
630    
631        private void insertNodeIntoAllNearbySegments(List<WaySegment> wss, Node n, Collection<OsmPrimitive> newSelection, Collection<Command> cmds, ArrayList<Way> replacedWays, ArrayList<Way> reuseWays) {
632            Map<Way, List<Integer>> insertPoints = new HashMap<Way, List<Integer>>();
633            for (WaySegment ws : wss) {
634                List<Integer> is;
635                if (insertPoints.containsKey(ws.way)) {
636                    is = insertPoints.get(ws.way);
637                } else {
638                    is = new ArrayList<Integer>();
639                    insertPoints.put(ws.way, is);
640                }
641    
642                is.add(ws.lowerIndex);
643            }
644    
645            Set<Pair<Node,Node>> segSet = new HashSet<Pair<Node,Node>>();
646    
647            for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
648                Way w = insertPoint.getKey();
649                List<Integer> is = insertPoint.getValue();
650    
651                Way wnew = new Way(w);
652    
653                pruneSuccsAndReverse(is);
654                for (int i : is) {
655                    segSet.add(Pair.sort(new Pair<Node,Node>(w.getNode(i), w.getNode(i+1))));
656                }
657                for (int i : is) {
658                    wnew.addNode(i + 1, n);
659                }
660    
661                // If ALT is pressed, a new way should be created and that new way should get
662                // selected. This works everytime unless the ways the nodes get inserted into
663                // are already selected. This is the case when creating a self-overlapping way
664                // but pressing ALT prevents this. Therefore we must de-select the way manually
665                // here so /only/ the new way will be selected after this method finishes.
666                if(alt) {
667                    newSelection.add(insertPoint.getKey());
668                }
669    
670                cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
671                replacedWays.add(insertPoint.getKey());
672                reuseWays.add(wnew);
673            }
674    
675            adjustNode(segSet, n);
676        }
677    
678        /**
679         * Prevent creation of ways that look like this: <---->
680         * This happens if users want to draw a no-exit-sideway from the main way like this:
681         * ^
682         * |<---->
683         * |
684         * The solution isn't ideal because the main way will end in the side way, which is bad for
685         * navigation software ("drive straight on") but at least easier to fix. Maybe users will fix
686         * it on their own, too. At least it's better than producing an error.
687         *
688         * @param Way the way to check
689         * @param Node the current node (i.e. the one the connection will be made from)
690         * @param Node the target node (i.e. the one the connection will be made to)
691         * @return Boolean True if this would create a selfcontaining way, false otherwise.
692         */
693        private boolean isSelfContainedWay(Way selectedWay, Node currentNode, Node targetNode) {
694            if(selectedWay != null) {
695                int posn0 = selectedWay.getNodes().indexOf(currentNode);
696                if( posn0 != -1 && // n0 is part of way
697                        (posn0 >= 1                             && targetNode.equals(selectedWay.getNode(posn0-1))) || // previous node
698                        (posn0 < selectedWay.getNodesCount()-1) && targetNode.equals(selectedWay.getNode(posn0+1))) {  // next node
699                    getCurrentDataSet().setSelected(targetNode);
700                    lastUsedNode = targetNode;
701                    return true;
702                }
703            }
704    
705            return false;
706        }
707    
708        /**
709         * Finds a node to continue drawing from. Decision is based upon given node and way.
710         * @param selectedNode Currently selected node, may be null
711         * @param selectedWay Currently selected way, may be null
712         * @return Node if a suitable node is found, null otherwise
713         */
714        private Node findNodeToContinueFrom(Node selectedNode, Way selectedWay) {
715            // No nodes or ways have been selected, this occurs when a relation
716            // has been selected or the selection is empty
717            if(selectedNode == null && selectedWay == null)
718                return null;
719    
720            if (selectedNode == null) {
721                if (selectedWay.isFirstLastNode(lastUsedNode))
722                    return lastUsedNode;
723    
724                // We have a way selected, but no suitable node to continue from. Start anew.
725                return null;
726            }
727    
728            if (selectedWay == null)
729                return selectedNode;
730    
731            if (selectedWay.isFirstLastNode(selectedNode))
732                return selectedNode;
733    
734            // We have a way and node selected, but it's not at the start/end of the way. Start anew.
735            return null;
736        }
737    
738        @Override
739        public void mouseDragged(MouseEvent e) {
740            mouseMoved(e);
741        }
742    
743        @Override
744        public void mouseMoved(MouseEvent e) {
745            if(!Main.map.mapView.isActiveLayerDrawable())
746                return;
747    
748            // we copy ctrl/alt/shift from the event just in case our global
749            // AWTEvent didn't make it through the security manager. Unclear
750            // if that can ever happen but better be safe.
751            updateKeyModifiers(e);
752            mousePos = e.getPoint();
753            if (snapHelper.isSnapOn() && ctrl) 
754                tryToSetBaseSegmentForAngleSnap();
755             
756            computeHelperLine();
757            addHighlighting();
758        }
759        
760        /**
761         * This method is used to detect segment under mouse and use it as reference for angle snapping
762         */
763        private void tryToSetBaseSegmentForAngleSnap() {
764            WaySegment seg = Main.map.mapView.getNearestWaySegment(mousePos, OsmPrimitive.isSelectablePredicate);
765            if (seg!=null) {
766                snapHelper.setBaseSegment(seg);
767            }
768        }
769    
770        /**
771         * This method prepares data required for painting the "helper line" from
772         * the last used position to the mouse cursor. It duplicates some code from
773         * mouseReleased() (FIXME).
774         */
775        private void computeHelperLine() {
776            MapView mv = Main.map.mapView;
777            if (mousePos == null) {
778                // Don't draw the line.
779                currentMouseEastNorth = null;
780                currentBaseNode = null;
781                return;
782            }
783    
784            Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
785    
786            Node currentMouseNode = null;
787            mouseOnExistingNode = null;
788            mouseOnExistingWays = new HashSet<Way>();
789    
790            showStatusInfo(-1, -1, -1, snapHelper.isSnapOn());
791    
792            if (!ctrl && mousePos != null) {
793                currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
794            }
795    
796            // We need this for highlighting and we'll only do so if we actually want to re-use
797            // *and* there is no node nearby (because nodes beat ways when re-using)
798            if(!ctrl && currentMouseNode == null) {
799                List<WaySegment> wss = mv.getNearestWaySegments(mousePos, OsmPrimitive.isSelectablePredicate);
800                for(WaySegment ws : wss) {
801                    mouseOnExistingWays.add(ws.way);
802                }
803            }
804    
805            if (currentMouseNode != null) {
806                // user clicked on node
807                if (selection.isEmpty()) return;
808                currentMouseEastNorth = currentMouseNode.getEastNorth();
809                mouseOnExistingNode = currentMouseNode;
810            } else {
811                // no node found in clicked area
812                currentMouseEastNorth = mv.getEastNorth(mousePos.x, mousePos.y);
813            }
814    
815            determineCurrentBaseNodeAndPreviousNode(selection);
816            if (previousNode == null) {
817                snapHelper.noSnapNow();
818            }
819    
820            if (currentBaseNode == null || currentBaseNode == currentMouseNode)
821                return; // Don't create zero length way segments.
822    
823    
824            double curHdg = Math.toDegrees(currentBaseNode.getEastNorth()
825                    .heading(currentMouseEastNorth));
826            double baseHdg=-1;
827            if (previousNode != null) {
828                baseHdg =  Math.toDegrees(previousNode.getEastNorth()
829                        .heading(currentBaseNode.getEastNorth()));
830            }
831    
832            snapHelper.checkAngleSnapping(currentMouseEastNorth,baseHdg, curHdg);
833    
834            // status bar was filled by snapHelper
835        }
836    
837        private void showStatusInfo(double angle, double hdg, double distance, boolean activeFlag) {
838            Main.map.statusLine.setAngle(angle);
839            Main.map.statusLine.activateAnglePanel(activeFlag);
840            Main.map.statusLine.setHeading(hdg);
841            Main.map.statusLine.setDist(distance);
842        }
843    
844        /**
845         * Helper function that sets fields currentBaseNode and previousNode
846         * @param selection
847         * uses also lastUsedNode field
848         */
849        private void determineCurrentBaseNodeAndPreviousNode(Collection<OsmPrimitive>  selection) {
850            Node selectedNode = null;
851            Way selectedWay = null;
852            for (OsmPrimitive p : selection) {
853                if (p instanceof Node) {
854                    if (selectedNode != null)
855                        return;
856                    selectedNode = (Node) p;
857                } else if (p instanceof Way) {
858                    if (selectedWay != null)
859                        return;
860                    selectedWay = (Way) p;
861                }
862            }
863            // we are here, if not more than 1 way or node is selected,
864    
865            // the node from which we make a connection
866            currentBaseNode = null;
867            previousNode = null;
868    
869            if (selectedNode == null) {
870                if (selectedWay == null)
871                    return;
872                if (selectedWay.isFirstLastNode(lastUsedNode)) {
873                    currentBaseNode = lastUsedNode;
874                    if (lastUsedNode == selectedWay.getNode(selectedWay.getNodesCount()-1) && selectedWay.getNodesCount() > 1) {
875                        previousNode = selectedWay.getNode(selectedWay.getNodesCount()-2);
876                    }
877                }
878            } else if (selectedWay == null) {
879                currentBaseNode = selectedNode;
880            } else if (!selectedWay.isDeleted()) { // fix #7118
881                if (selectedNode == selectedWay.getNode(0)){
882                    currentBaseNode = selectedNode;
883                    if (selectedWay.getNodesCount()>1) {
884                        previousNode = selectedWay.getNode(1);
885                    }
886                }
887                if (selectedNode == selectedWay.lastNode()) {
888                    currentBaseNode = selectedNode;
889                    if (selectedWay.getNodesCount()>1) {
890                        previousNode = selectedWay.getNode(selectedWay.getNodesCount()-2);
891                    }
892                }
893            }
894        }
895    
896    
897        /**
898         * Repaint on mouse exit so that the helper line goes away.
899         */
900        @Override public void mouseExited(MouseEvent e) {
901            if(!Main.map.mapView.isActiveLayerDrawable())
902                return;
903            mousePos = e.getPoint();
904            snapHelper.noSnapNow();
905            boolean repaintIssued = removeHighlighting();
906            // force repaint in case snapHelper needs one. If removeHighlighting
907            // caused one already, don???t do it again.
908            if(!repaintIssued) {
909                Main.map.mapView.repaint();
910            }
911        }
912    
913        /**
914         * @return If the node is the end of exactly one way, return this.
915         *  <code>null</code> otherwise.
916         */
917        public static Way getWayForNode(Node n) {
918            Way way = null;
919            for (Way w : Utils.filteredCollection(n.getReferrers(), Way.class)) {
920                if (!w.isUsable() || w.getNodesCount() < 1) {
921                    continue;
922                }
923                Node firstNode = w.getNode(0);
924                Node lastNode = w.getNode(w.getNodesCount() - 1);
925                if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) {
926                    if (way != null)
927                        return null;
928                    way = w;
929                }
930            }
931            return way;
932        }
933    
934        public Node getCurrentBaseNode() {
935            return currentBaseNode;
936        }
937    
938        private static void pruneSuccsAndReverse(List<Integer> is) {
939            HashSet<Integer> is2 = new HashSet<Integer>();
940            for (int i : is) {
941                if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
942                    is2.add(i);
943                }
944            }
945            is.clear();
946            is.addAll(is2);
947            Collections.sort(is);
948            Collections.reverse(is);
949        }
950    
951        /**
952         * Adjusts the position of a node to lie on a segment (or a segment
953         * intersection).
954         *
955         * If one or more than two segments are passed, the node is adjusted
956         * to lie on the first segment that is passed.
957         *
958         * If two segments are passed, the node is adjusted to be at their
959         * intersection.
960         *
961         * No action is taken if no segments are passed.
962         *
963         * @param segs the segments to use as a reference when adjusting
964         * @param n the node to adjust
965         */
966        private static void adjustNode(Collection<Pair<Node,Node>> segs, Node n) {
967    
968            switch (segs.size()) {
969            case 0:
970                return;
971            case 2:
972                // This computes the intersection between
973                // the two segments and adjusts the node position.
974                Iterator<Pair<Node,Node>> i = segs.iterator();
975                Pair<Node,Node> seg = i.next();
976                EastNorth A = seg.a.getEastNorth();
977                EastNorth B = seg.b.getEastNorth();
978                seg = i.next();
979                EastNorth C = seg.a.getEastNorth();
980                EastNorth D = seg.b.getEastNorth();
981    
982                double u=det(B.east() - A.east(), B.north() - A.north(), C.east() - D.east(), C.north() - D.north());
983    
984                // Check for parallel segments and do nothing if they are
985                // In practice this will probably only happen when a way has been duplicated
986    
987                if (u == 0)
988                    return;
989    
990                // q is a number between 0 and 1
991                // It is the point in the segment where the intersection occurs
992                // if the segment is scaled to lenght 1
993    
994                double q = det(B.north() - C.north(), B.east() - C.east(), D.north() - C.north(), D.east() - C.east()) / u;
995                EastNorth intersection = new EastNorth(
996                        B.east() + q * (A.east() - B.east()),
997                        B.north() + q * (A.north() - B.north()));
998    
999                int snapToIntersectionThreshold
1000                = Main.pref.getInteger("edit.snap-intersection-threshold",10);
1001    
1002                // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise
1003                // fall through to default action.
1004                // (for semi-parallel lines, intersection might be miles away!)
1005                if (Main.map.mapView.getPoint(n).distance(Main.map.mapView.getPoint(intersection)) < snapToIntersectionThreshold) {
1006                    n.setEastNorth(intersection);
1007                    return;
1008                }
1009            default:
1010                EastNorth P = n.getEastNorth();
1011                seg = segs.iterator().next();
1012                A = seg.a.getEastNorth();
1013                B = seg.b.getEastNorth();
1014                double a = P.distanceSq(B);
1015                double b = P.distanceSq(A);
1016                double c = A.distanceSq(B);
1017                q = (a - b + c) / (2*c);
1018                n.setEastNorth(new EastNorth(B.east() + q * (A.east() - B.east()), B.north() + q * (A.north() - B.north())));
1019            }
1020        }
1021    
1022        // helper for adjustNode
1023        static double det(double a, double b, double c, double d) {
1024            return a * d - b * c;
1025        }
1026    
1027        private void tryToMoveNodeOnIntersection(List<WaySegment> wss, Node n) {
1028            if (wss.isEmpty())
1029                return;
1030            WaySegment ws = wss.get(0);
1031            EastNorth p1=ws.getFirstNode().getEastNorth();
1032            EastNorth p2=ws.getSecondNode().getEastNorth();
1033            if (snapHelper.dir2!=null && currentBaseNode!=null) {
1034                EastNorth xPoint = Geometry.getSegmentSegmentIntersection(p1, p2, snapHelper.dir2, currentBaseNode.getEastNorth());
1035                if (xPoint!=null) {
1036                    n.setEastNorth(xPoint);
1037                }
1038            }
1039        }
1040        /**
1041         * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
1042         * (if feature enabled). Also sets the target cursor if appropriate. It adds the to-be-
1043         * highlighted primitives to newHighlights but does not actually highlight them. This work is
1044         * done in redrawIfRequired. This means, calling addHighlighting() without redrawIfRequired()
1045         * will leave the data in an inconsistent state.
1046         *
1047         * The status bar derives its information from oldHighlights, so in order to update the status
1048         * bar both addHighlighting() and repaintIfRequired() are needed, since former fills newHighlights
1049         * and latter processes them into oldHighlights.
1050         */
1051        private void addHighlighting() {
1052            newHighlights = new HashSet<OsmPrimitive>();
1053    
1054            // if ctrl key is held ("no join"), don't highlight anything
1055            if (ctrl) {
1056                Main.map.mapView.setNewCursor(cursor, this);
1057                redrawIfRequired();
1058                return;
1059            }
1060    
1061            // This happens when nothing is selected, but we still want to highlight the "target node"
1062            if (mouseOnExistingNode == null && getCurrentDataSet().getSelected().size() == 0
1063                    && mousePos != null) {
1064                mouseOnExistingNode = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
1065            }
1066    
1067            if (mouseOnExistingNode != null) {
1068                Main.map.mapView.setNewCursor(cursorJoinNode, this);
1069                newHighlights.add(mouseOnExistingNode);
1070                redrawIfRequired();
1071                return;
1072            }
1073    
1074            // Insert the node into all the nearby way segments
1075            if (mouseOnExistingWays.size() == 0) {
1076                Main.map.mapView.setNewCursor(cursor, this);
1077                redrawIfRequired();
1078                return;
1079            }
1080    
1081            Main.map.mapView.setNewCursor(cursorJoinWay, this);
1082            newHighlights.addAll(mouseOnExistingWays);
1083            redrawIfRequired();
1084        }
1085    
1086        /**
1087         * Removes target highlighting from primitives. Issues repaint if required.
1088         * Returns true if a repaint has been issued.
1089         */
1090        private boolean removeHighlighting() {
1091            newHighlights = new HashSet<OsmPrimitive>();
1092            return redrawIfRequired();
1093        }
1094    
1095        public void paint(Graphics2D g, MapView mv, Bounds box) {
1096            // sanity checks
1097            if (Main.map.mapView == null || mousePos == null
1098                    // don't draw line if we don't know where from or where to
1099                    || currentBaseNode == null || currentMouseEastNorth == null
1100                    // don't draw line if mouse is outside window
1101                    || !Main.map.mapView.getBounds().contains(mousePos))
1102                return;
1103    
1104            Graphics2D g2 = g;
1105            snapHelper.drawIfNeeded(g2,mv);
1106            if (!drawHelperLine || wayIsFinished || shift)
1107                return;
1108    
1109            if (!snapHelper.isActive()) { // else use color and stoke from  snapHelper.draw
1110                g2.setColor(selectedColor);
1111                g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
1112            } else if (!snapHelper.drawConstructionGeometry)
1113                return;
1114            GeneralPath b = new GeneralPath();
1115            Point p1=mv.getPoint(currentBaseNode);
1116            Point p2=mv.getPoint(currentMouseEastNorth);
1117    
1118            double t = Math.atan2(p2.y-p1.y, p2.x-p1.x) + Math.PI;
1119    
1120            b.moveTo(p1.x,p1.y); b.lineTo(p2.x, p2.y);
1121    
1122            // if alt key is held ("start new way"), draw a little perpendicular line
1123            if (alt) {
1124                b.moveTo((int)(p1.x + 8*Math.cos(t+PHI)), (int)(p1.y + 8*Math.sin(t+PHI)));
1125                b.lineTo((int)(p1.x + 8*Math.cos(t-PHI)), (int)(p1.y + 8*Math.sin(t-PHI)));
1126            }
1127    
1128            g2.draw(b);
1129            g2.setStroke(new BasicStroke(1));
1130        }
1131    
1132        @Override
1133        public String getModeHelpText() {
1134            String rv = "";
1135            /*
1136             *  No modifiers: all (Connect, Node Re-Use, Auto-Weld)
1137             *  CTRL: disables node re-use, auto-weld
1138             *  Shift: do not make connection
1139             *  ALT: make connection but start new way in doing so
1140             */
1141    
1142            /*
1143             * Status line text generation is split into two parts to keep it maintainable.
1144             * First part looks at what will happen to the new node inserted on click and
1145             * the second part will look if a connection is made or not.
1146             *
1147             * Note that this help text is not absolutely accurate as it doesn't catch any special
1148             * cases (e.g. when preventing <---> ways). The only special that it catches is when
1149             * a way is about to be finished.
1150             *
1151             * First check what happens to the new node.
1152             */
1153    
1154            // oldHighlights stores the current highlights. If this
1155            // list is empty we can assume that we won't do any joins
1156            if (ctrl || oldHighlights.isEmpty()) {
1157                rv = tr("Create new node.");
1158            } else {
1159                // oldHighlights may store a node or way, check if it's a node
1160                OsmPrimitive x = oldHighlights.iterator().next();
1161                if (x instanceof Node) {
1162                    rv = tr("Select node under cursor.");
1163                } else {
1164                    rv = trn("Insert new node into way.", "Insert new node into {0} ways.",
1165                            oldHighlights.size(), oldHighlights.size());
1166                }
1167            }
1168    
1169            /*
1170             * Check whether a connection will be made
1171             */
1172            if (currentBaseNode != null && !wayIsFinished) {
1173                if (alt) {
1174                    rv += " " + tr("Start new way from last node.");
1175                } else {
1176                    rv += " " + tr("Continue way from last node.");
1177                }
1178                if (snapHelper.isSnapOn()) {
1179                    rv += " "+ tr("Angle snapping active.");
1180                }
1181            }
1182    
1183            Node n = mouseOnExistingNode;
1184            /*
1185             * Handle special case: Highlighted node == selected node => finish drawing
1186             */
1187            if (n != null && getCurrentDataSet() != null && getCurrentDataSet().getSelectedNodes().contains(n)) {
1188                if (wayIsFinished) {
1189                    rv = tr("Select node under cursor.");
1190                } else {
1191                    rv = tr("Finish drawing.");
1192                }
1193            }
1194    
1195            /*
1196             * Handle special case: Self-Overlapping or closing way
1197             */
1198            if (getCurrentDataSet() != null && getCurrentDataSet().getSelectedWays().size() > 0 && !wayIsFinished && !alt) {
1199                Way w = getCurrentDataSet().getSelectedWays().iterator().next();
1200                for (Node m : w.getNodes()) {
1201                    if (m.equals(mouseOnExistingNode) || mouseOnExistingWays.contains(w)) {
1202                        rv += " " + tr("Finish drawing.");
1203                        break;
1204                    }
1205                }
1206            }
1207            return rv;
1208        }
1209    
1210        /**
1211         * Get selected primitives, while draw action is in progress.
1212         *
1213         * While drawing a way, technically the last node is selected.
1214         * This is inconvenient when the user tries to add tags to the
1215         * way using a keyboard shortcut. In that case, this method returns
1216         * the current way as selection, to work around this issue.
1217         * Otherwise the normal selection of the current data layer is returned.
1218         */
1219        public Collection<OsmPrimitive> getInProgressSelection() {
1220            DataSet ds = getCurrentDataSet();
1221            if (ds == null) return null;
1222            if (currentBaseNode != null && !ds.getSelected().isEmpty()) {
1223                Way continueFrom = getWayForNode(currentBaseNode);
1224                if (alt && continueFrom != null)
1225                    return Collections.<OsmPrimitive>singleton(continueFrom);
1226            }
1227            return ds.getSelected();
1228        }
1229    
1230        @Override
1231        public boolean layerIsSupported(Layer l) {
1232            return l instanceof OsmDataLayer;
1233        }
1234    
1235        @Override
1236        protected void updateEnabledState() {
1237            setEnabled(getEditLayer() != null);
1238        }
1239    
1240        @Override
1241        public void destroy() {
1242            super.destroy();
1243            snapChangeAction.destroy();
1244        }
1245    
1246        public class BackSpaceAction extends AbstractAction {
1247    
1248            @Override
1249            public void actionPerformed(ActionEvent e) {
1250                Main.main.undoRedo.undo();
1251                Node n=null;
1252                Command lastCmd=Main.main.undoRedo.commands.peekLast();
1253                if (lastCmd==null) return;
1254                for (OsmPrimitive p: lastCmd.getParticipatingPrimitives()) {
1255                    if (p instanceof Node) {
1256                        if (n==null) {
1257                            n=(Node) p; // found one node
1258                            wayIsFinished=false;
1259                        }  else {
1260                            // if more than 1 node were affected by previous command,
1261                            // we have no way to continue, so we forget about found node
1262                            n=null;
1263                            break;
1264                        }
1265                    }
1266                }
1267                // select last added node - maybe we will continue drawing from it
1268                if (n!=null) {
1269                    getCurrentDataSet().addSelected(n);
1270                }
1271            }
1272        }
1273    
1274        private class SnapHelper {
1275            boolean snapOn; // snapping is turned on
1276    
1277            private boolean active; // snapping is active for current mouse position
1278            private boolean fixed; // snap angle is fixed
1279            private boolean absoluteFix; // snap angle is absolute
1280    
1281            private boolean drawConstructionGeometry;
1282            private boolean showProjectedPoint;
1283            private boolean showAngle;
1284    
1285            private boolean snapToProjections;
1286    
1287            EastNorth dir2;
1288            EastNorth projected;
1289            String labelText;
1290            double lastAngle;
1291    
1292            double customBaseHeading=-1; // angle of base line, if not last segment)
1293            private EastNorth segmentPoint1; // remembered first point of base segment
1294            private EastNorth segmentPoint2; // remembered second point of base segment
1295            private EastNorth projectionSource; // point that we are projecting to the line
1296    
1297            double snapAngles[];
1298            double snapAngleTolerance;
1299    
1300            double pe,pn; // (pe,pn) - direction of snapping line
1301            double e0,n0; // (e0,n0) - origin of snapping line
1302    
1303            final String fixFmt="%d "+tr("FIX");
1304            Color snapHelperColor;
1305            private Color highlightColor;
1306    
1307            private Stroke normalStroke;
1308            private Stroke helperStroke;
1309            private Stroke highlightStroke;
1310    
1311            JCheckBoxMenuItem checkBox;
1312    
1313            public void init() {
1314                snapOn=false;
1315                checkBox.setState(snapOn);
1316                fixed=false; absoluteFix=false;
1317    
1318                Collection<String> angles = Main.pref.getCollection("draw.anglesnap.angles",
1319                        Arrays.asList("0","30","45","60","90","120","135","150","180"));
1320    
1321                snapAngles = new double[2*angles.size()];
1322                int i=0;
1323                for (String s: angles) {
1324                    try {
1325                        snapAngles[i] = Double.parseDouble(s); i++;
1326                        snapAngles[i] = 360-Double.parseDouble(s); i++;
1327                    } catch (NumberFormatException e) {
1328                        System.err.println("Warning: incorrect number in draw.anglesnap.angles preferences: "+s);
1329                        snapAngles[i]=0;i++;
1330                        snapAngles[i]=0;i++;
1331                    }
1332                }
1333                snapAngleTolerance = Main.pref.getDouble("draw.anglesnap.tolerance", 5.0);
1334                drawConstructionGeometry = Main.pref.getBoolean("draw.anglesnap.drawConstructionGeometry", true);
1335                showProjectedPoint = Main.pref.getBoolean("draw.anglesnap.drawProjectedPoint", true);
1336                snapToProjections = Main.pref.getBoolean("draw.anglesnap.projectionsnap", true);
1337    
1338                showAngle = Main.pref.getBoolean("draw.anglesnap.showAngle", true);
1339                useRepeatedShortcut = Main.pref.getBoolean("draw.anglesnap.toggleOnRepeatedA", true);
1340    
1341                normalStroke = new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
1342                snapHelperColor = Main.pref.getColor(marktr("draw angle snap"), Color.ORANGE);
1343    
1344                highlightColor = Main.pref.getColor(marktr("draw angle snap highlight"),
1345                        new Color(Color.ORANGE.getRed(),Color.ORANGE.getGreen(),Color.ORANGE.getBlue(),128));
1346                highlightStroke = new BasicStroke(10, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
1347    
1348                float dash1[] = { 4.0f };
1349                helperStroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
1350                        BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f);
1351            }
1352    
1353            public void saveAngles(String ... angles) {
1354                Main.pref.putCollection("draw.anglesnap.angles", Arrays.asList(angles));
1355            }
1356    
1357            public  void setMenuCheckBox(JCheckBoxMenuItem checkBox) {
1358                this.checkBox = checkBox;
1359            }
1360    
1361            public  void drawIfNeeded(Graphics2D g2, MapView mv) {
1362                if (!snapOn || !active)
1363                    return;
1364                Point p1=mv.getPoint(currentBaseNode);
1365                Point p2=mv.getPoint(dir2);
1366                Point p3=mv.getPoint(projected);
1367                GeneralPath b;
1368                if (drawConstructionGeometry) {
1369                    g2.setColor(snapHelperColor);
1370                    g2.setStroke(helperStroke);
1371    
1372                    b = new GeneralPath();
1373                    if (absoluteFix) {
1374                        b.moveTo(p2.x,p2.y);
1375                        b.lineTo(2*p1.x-p2.x,2*p1.y-p2.y); // bi-directional line
1376                    } else {
1377                        b.moveTo(p2.x,p2.y);
1378                        b.lineTo(p3.x,p3.y);
1379                    }
1380                    g2.draw(b);
1381                }
1382                if (projectionSource != null) {
1383                    g2.setColor(snapHelperColor);
1384                    g2.setStroke(helperStroke);
1385                    b = new GeneralPath();
1386                    b.moveTo(p3.x,p3.y);
1387                    Point pp=mv.getPoint(projectionSource);
1388                    b.lineTo(pp.x,pp.y);
1389                    g2.draw(b);
1390                }
1391    
1392                if (customBaseHeading >= 0) {
1393                    g2.setColor(highlightColor);
1394                    g2.setStroke(highlightStroke);
1395                    b = new GeneralPath();
1396                    Point pp1=mv.getPoint(segmentPoint1);
1397                    Point pp2=mv.getPoint(segmentPoint2);
1398                    b.moveTo(pp1.x,pp1.y);
1399                    b.lineTo(pp2.x,pp2.y);
1400                    g2.draw(b);
1401                }
1402    
1403                g2.setColor(selectedColor);
1404                g2.setStroke(normalStroke);
1405                b = new GeneralPath();
1406                b.moveTo(p1.x,p1.y);
1407                b.lineTo(p3.x,p3.y);
1408                g2.draw(b);
1409    
1410                g2.drawString(labelText, p3.x-5, p3.y+20);
1411                if (showProjectedPoint) {
1412                    g2.setStroke(normalStroke);
1413                    g2.drawOval(p3.x-5, p3.y-5, 10, 10); // projected point
1414                }
1415    
1416                g2.setColor(snapHelperColor);
1417                g2.setStroke(helperStroke);
1418            }
1419    
1420            /* If mouse position is close to line at 15-30-45-... angle, remembers this direction
1421             */
1422            public void checkAngleSnapping(EastNorth currentEN, double baseHeading, double curHeading) {
1423                EastNorth p0 = currentBaseNode.getEastNorth();
1424                EastNorth snapPoint = currentEN;
1425                double angle = -1;
1426    
1427                double activeBaseHeading = (customBaseHeading>=0)? customBaseHeading : baseHeading;
1428    
1429                if (snapOn && (activeBaseHeading>=0)) {
1430                    angle = curHeading - activeBaseHeading;
1431                    if (angle < 0) {
1432                        angle+=360;
1433                    }
1434                    if (angle > 360) {
1435                        angle=0;
1436                    }
1437    
1438                    double nearestAngle;
1439                    if (fixed) {
1440                        nearestAngle = lastAngle; // if direction is fixed use previous angle
1441                        active = true;
1442                    } else {
1443                        nearestAngle = getNearestAngle(angle);
1444                        if (getAngleDelta(nearestAngle, angle) < snapAngleTolerance) {
1445                            active = (customBaseHeading>=0)? true : Math.abs(nearestAngle - 180) > 1e-3;
1446                            // if angle is to previous segment, exclude 180 degrees
1447                            lastAngle = nearestAngle;
1448                        } else {
1449                            active=false;
1450                        }
1451                    }
1452    
1453                    if (active) {
1454                        double phi;
1455                        e0 = p0.east();
1456                        n0 = p0.north();
1457                        buildLabelText((nearestAngle<=180) ? nearestAngle : nearestAngle-360);
1458    
1459                        phi = (nearestAngle + activeBaseHeading) * Math.PI / 180;
1460                        // (pe,pn) - direction of snapping line
1461                        pe = Math.sin(phi);
1462                        pn = Math.cos(phi);
1463                        double scale = 20 * Main.map.mapView.getDist100Pixel();
1464                        dir2 = new EastNorth(e0 + scale * pe, n0 + scale * pn);
1465                        snapPoint = getSnapPoint(currentEN);
1466                    } else {
1467                        noSnapNow();
1468                    }
1469                }
1470    
1471                // find out the distance, in metres, between the base point and projected point
1472                LatLon mouseLatLon = Main.map.mapView.getProjection().eastNorth2latlon(snapPoint);
1473                double distance = currentBaseNode.getCoor().greatCircleDistance(mouseLatLon);
1474                double hdg = Math.toDegrees(p0.heading(snapPoint));
1475                // heading of segment from current to calculated point, not to mouse position
1476    
1477                if (baseHeading >=0 ) { // there is previous line segment with some heading
1478                    angle = hdg - baseHeading;
1479                    if (angle < 0) {
1480                        angle+=360;
1481                    }
1482                    if (angle > 360) {
1483                        angle=0;
1484                    }
1485                }
1486                showStatusInfo(angle, hdg, distance, isSnapOn());
1487            }
1488    
1489            private void buildLabelText(double nearestAngle) {
1490                if (showAngle) {
1491                    if (fixed) {
1492                        if (absoluteFix) {
1493                            labelText = "=";
1494                        } else {
1495                            labelText = String.format(fixFmt, (int) nearestAngle);
1496                        }
1497                    } else {
1498                        labelText = String.format("%d", (int) nearestAngle);
1499                    }
1500                } else {
1501                    if (fixed) {
1502                        if (absoluteFix) {
1503                            labelText = "=";
1504                        } else {
1505                            labelText = String.format(tr("FIX"), 0);
1506                        }
1507                    } else {
1508                        labelText = "";
1509                    }
1510                }
1511            }
1512    
1513            public  EastNorth getSnapPoint(EastNorth p) {
1514                if (!active)
1515                    return p;
1516                double de=p.east()-e0;
1517                double dn=p.north()-n0;
1518                double l = de*pe+dn*pn;
1519                double delta = Main.map.mapView.getDist100Pixel()/20;
1520                if (!absoluteFix && l<delta) {
1521                    active=false;
1522                    return p;
1523                } //  do not go backward!
1524    
1525                projectionSource=null;
1526                if (snapToProjections) {
1527                    DataSet ds = getCurrentDataSet();
1528                    Collection<Way> selectedWays = ds.getSelectedWays();
1529                    if (selectedWays.size()==1) {
1530                        Way w = selectedWays.iterator().next();
1531                        Collection <EastNorth> pointsToProject = new ArrayList<EastNorth>();
1532                        if (w.getNodesCount()<1000) {
1533                            for (Node n: w.getNodes()) {
1534                                pointsToProject.add(n.getEastNorth());
1535                            }
1536                        }
1537                        if (customBaseHeading >=0 ) {
1538                            pointsToProject.add(segmentPoint1);
1539                            pointsToProject.add(segmentPoint2);
1540                        }
1541                        EastNorth enOpt=null;
1542                        double dOpt=1e5;
1543                        for (EastNorth en: pointsToProject) { // searching for besht projection
1544                            double l1 = (en.east()-e0)*pe+(en.north()-n0)*pn;
1545                            double d1 = Math.abs(l1-l);
1546                            if (d1 < delta && d1 < dOpt) {
1547                                l=l1;
1548                                enOpt = en;
1549                                dOpt = d1;
1550                            }
1551                        }
1552                        if (enOpt!=null) {
1553                            projectionSource =  enOpt;
1554                        }
1555                    }
1556                }
1557                return projected = new EastNorth(e0+l*pe, n0+l*pn);
1558            }
1559    
1560    
1561            public void noSnapNow() {
1562                active=false;
1563                dir2=null; projected=null;
1564                labelText=null;
1565            }
1566    
1567            public void setBaseSegment(WaySegment seg) {
1568                if (seg==null) return;
1569                segmentPoint1=seg.getFirstNode().getEastNorth();
1570                segmentPoint2=seg.getSecondNode().getEastNorth();
1571    
1572                double hdg = segmentPoint1.heading(segmentPoint2);
1573                hdg=Math.toDegrees(hdg);
1574                if (hdg<0) {
1575                    hdg+=360;
1576                }
1577                if (hdg>360) {
1578                    hdg-=360;
1579                }
1580                //fixed=true;
1581                //absoluteFix=true;
1582                customBaseHeading=hdg;
1583            }
1584    
1585            private void nextSnapMode() {
1586                if (snapOn) {
1587                    // turn off snapping if we are in fixed mode or no actile snapping line exist
1588                    if (fixed || !active) { snapOn=false; unsetFixedMode(); } else {
1589                        setFixedMode();
1590                    }
1591                } else {
1592                    snapOn=true;
1593                    unsetFixedMode();
1594                }
1595                checkBox.setState(snapOn);
1596                customBaseHeading=-1;
1597            }
1598    
1599            private void enableSnapping() {
1600                snapOn = true;
1601                checkBox.setState(snapOn);
1602                customBaseHeading=-1;
1603                unsetFixedMode();
1604            }
1605    
1606            private void toggleSnapping() {
1607                snapOn = !snapOn;
1608                checkBox.setState(snapOn);
1609                customBaseHeading=-1;
1610                unsetFixedMode();
1611            }
1612    
1613            public void setFixedMode() {
1614                if (active) {
1615                    fixed=true;
1616                }
1617            }
1618    
1619    
1620            public  void unsetFixedMode() {
1621                fixed=false;
1622                absoluteFix=false;
1623                lastAngle=0;
1624                active=false;
1625            }
1626    
1627            public  boolean isActive() {
1628                return active;
1629            }
1630    
1631            public  boolean isSnapOn() {
1632                return snapOn;
1633            }
1634    
1635            private double getNearestAngle(double angle) {
1636                double delta,minDelta=1e5, bestAngle=0.0;
1637                for (int i=0; i < snapAngles.length; i++) {
1638                    delta = getAngleDelta(angle,snapAngles[i]);
1639                    if (delta < minDelta) {
1640                        minDelta=delta;
1641                        bestAngle=snapAngles[i];
1642                    }
1643                }
1644                if (Math.abs(bestAngle-360) < 1e-3) {
1645                    bestAngle=0;
1646                }
1647                return bestAngle;
1648            }
1649    
1650            private double getAngleDelta(double a, double b) {
1651                double delta = Math.abs(a-b);
1652                if (delta>180)
1653                    return 360-delta;
1654                else
1655                    return delta;
1656            }
1657    
1658            private void unFixOrTurnOff() {
1659                if (absoluteFix) {
1660                    unsetFixedMode();
1661                } else {
1662                    toggleSnapping();
1663                }
1664            }
1665    
1666            MouseListener anglePopupListener = new PopupMenuLauncher( new JPopupMenu() {
1667                JCheckBoxMenuItem repeatedCb = new JCheckBoxMenuItem(new AbstractAction(tr("Toggle snapping by {0}", getShortcut().getKeyText())){
1668                    public void actionPerformed(ActionEvent e) {
1669                        boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1670                        Main.pref.put("draw.anglesnap.toggleOnRepeatedA", sel);
1671                        init();
1672                    }
1673                });
1674                JCheckBoxMenuItem helperCb = new JCheckBoxMenuItem(new AbstractAction(tr("Show helper geometry")){
1675                    public void actionPerformed(ActionEvent e) {
1676                        boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1677                        Main.pref.put("draw.anglesnap.drawConstructionGeometry", sel);
1678                        Main.pref.put("draw.anglesnap.drawProjectedPoint", sel);
1679                        Main.pref.put("draw.anglesnap.showAngle", sel);
1680                        init();
1681                        enableSnapping();
1682                    }
1683                });
1684                JCheckBoxMenuItem projectionCb = new JCheckBoxMenuItem(new AbstractAction(tr("Snap to node projections")){
1685                    public void actionPerformed(ActionEvent e) {
1686                        boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1687                        Main.pref.put("draw.anglesnap.projectionsnap", sel);
1688                        init();
1689                        enableSnapping();
1690                    }
1691                });
1692                {
1693                    helperCb.setState(Main.pref.getBoolean("draw.anglesnap.drawConstructionGeometry",true));
1694                    projectionCb.setState(Main.pref.getBoolean("draw.anglesnap.projectionsnapgvff",true));
1695                    repeatedCb.setState(Main.pref.getBoolean("draw.anglesnap.toggleOnRepeatedA",true));
1696                    add(repeatedCb);
1697                    add(helperCb);
1698                    add(projectionCb);;
1699                    add(new AbstractAction(tr("Disable")) {
1700                        public void actionPerformed(ActionEvent e) {
1701                            saveAngles("180");
1702                            init();
1703                            enableSnapping();
1704                        }
1705                    });
1706                    add(new AbstractAction(tr("0,90,...")) {
1707                        public void actionPerformed(ActionEvent e) {
1708                            saveAngles("0","90","180");
1709                            init();
1710                            enableSnapping();
1711                        }
1712                    });
1713                    add(new AbstractAction(tr("0,45,90,...")) {
1714                        public void actionPerformed(ActionEvent e) {
1715                            saveAngles("0","45","90","135","180");
1716                            init();
1717                            enableSnapping();
1718                        }
1719                    });
1720                    add(new AbstractAction(tr("0,30,45,60,90,...")) {
1721                        public void actionPerformed(ActionEvent e) {
1722                            saveAngles("0","30","45","60","90","120","135","150","180");
1723                            init();
1724                            enableSnapping();
1725                        }
1726                    });
1727                }
1728            }) {
1729                @Override
1730                public void mouseClicked(MouseEvent e) {
1731                    super.mouseClicked(e);
1732                    if (e.getButton() == MouseEvent.BUTTON1) {
1733                        toggleSnapping();
1734                        updateStatusLine();
1735                    }
1736                }
1737            };
1738        }
1739    
1740        private class SnapChangeAction extends JosmAction {
1741            public SnapChangeAction() {
1742                super(tr("Angle snapping"), "anglesnap",
1743                        tr("Switch angle snapping mode while drawing"), null, false);
1744                putValue("help", ht("/Action/Draw/AngleSnap"));
1745            }
1746    
1747            @Override
1748            public void actionPerformed(ActionEvent e) {
1749                if (snapHelper!=null) {
1750                    snapHelper.toggleSnapping();
1751                }
1752            }
1753        }
1754    }