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

Revision 5223, 62.4 KB checked in by Don-vip, 2 weeks ago (diff)

should fix #7620 - NPE in unknown conditions (not reproduced)

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