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

Last change on this file since 4954 was 4954, checked in by akks, 12 years ago

Tab in draw mode allows snapping to current way point projections ant to other line segment (right-click)

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