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

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

Tab button issue, see #7250

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