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