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

Last change on this file since 5098 was 5098, checked in by xeen, 12 years ago

Reduce repaints required when in draw mode. This should improve performance a bit with target highlighting activated. See #7503

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