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

Last change on this file since 4327 was 4327, checked in by xeen, 13 years ago

updates visual appearance of highlights and adds them to select and delete action

in more detail:

  • add target highlighting to select action
  • add target cursor to select action
  • add target highlighting to delete action
  • unify ctrl/alt/shift modifier detection in MapMode actions
  • highlights are now a halo around the way/node instead of a color change
  • default highlight color is now the same as the select color (red)
  • ability to highlight WaySegments and VirtualNodes
  • various style/whitespace nits
  • fixes #2411

This patch touches a lot of areas, so please report any regressions in the map mode
tools. Also please add a comment to #2411 if you find to highlighting in select
mode distracting, so it can be fine tuned (or turned off by default).

  • Property svn:eol-style set to native
File size: 34.8 KB
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.AWTEvent;
8import java.awt.BasicStroke;
9import java.awt.Color;
10import java.awt.Cursor;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.Toolkit;
14import java.awt.event.AWTEventListener;
15import java.awt.event.InputEvent;
16import java.awt.event.KeyEvent;
17import java.awt.event.MouseEvent;
18import java.awt.geom.GeneralPath;
19import java.util.ArrayList;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.HashMap;
23import java.util.HashSet;
24import java.util.Iterator;
25import java.util.LinkedList;
26import java.util.List;
27import java.util.Map;
28import java.util.Set;
29
30import javax.swing.JOptionPane;
31
32import org.openstreetmap.josm.Main;
33import org.openstreetmap.josm.command.AddCommand;
34import org.openstreetmap.josm.command.ChangeCommand;
35import org.openstreetmap.josm.command.Command;
36import org.openstreetmap.josm.command.SequenceCommand;
37import org.openstreetmap.josm.data.Bounds;
38import org.openstreetmap.josm.data.SelectionChangedListener;
39import org.openstreetmap.josm.data.coor.EastNorth;
40import org.openstreetmap.josm.data.coor.LatLon;
41import org.openstreetmap.josm.data.osm.DataSet;
42import org.openstreetmap.josm.data.osm.Node;
43import org.openstreetmap.josm.data.osm.OsmPrimitive;
44import org.openstreetmap.josm.data.osm.Way;
45import org.openstreetmap.josm.data.osm.WaySegment;
46import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
47import org.openstreetmap.josm.gui.MapFrame;
48import org.openstreetmap.josm.gui.MapView;
49import org.openstreetmap.josm.gui.layer.Layer;
50import org.openstreetmap.josm.gui.layer.MapViewPaintable;
51import org.openstreetmap.josm.gui.layer.OsmDataLayer;
52import org.openstreetmap.josm.tools.ImageProvider;
53import org.openstreetmap.josm.tools.Pair;
54import org.openstreetmap.josm.tools.Shortcut;
55
56/**
57 *
58 */
59public class DrawAction extends MapMode implements MapViewPaintable, SelectionChangedListener, AWTEventListener {
60 final private Cursor cursorJoinNode;
61 final private Cursor cursorJoinWay;
62
63 private Node lastUsedNode = null;
64 private double PHI=Math.toRadians(90);
65
66 private Node mouseOnExistingNode;
67 private Set<Way> mouseOnExistingWays = new HashSet<Way>();
68 private Set<OsmPrimitive> oldHighlights = new HashSet<OsmPrimitive>();
69 private boolean drawHelperLine;
70 private boolean wayIsFinished = false;
71 private boolean drawTargetHighlight;
72 private Point mousePos;
73 private Point oldMousePos;
74 private Color selectedColor;
75
76 private Node currentBaseNode;
77 private EastNorth currentMouseEastNorth;
78
79 private Shortcut extraShortcut;
80
81 public DrawAction(MapFrame mapFrame) {
82 super(tr("Draw"), "node/autonode", tr("Draw nodes"),
83 Shortcut.registerShortcut("mapmode:draw", tr("Mode: {0}", tr("Draw")), KeyEvent.VK_A, Shortcut.GROUP_EDIT),
84 mapFrame, ImageProvider.getCursor("crosshair", null));
85
86 // Add extra shortcut N
87 extraShortcut = Shortcut.registerShortcut("mapmode:drawfocus", tr("Mode: Draw Focus"), KeyEvent.VK_N, Shortcut.GROUP_EDIT);
88 Main.registerActionShortcut(this, extraShortcut);
89
90 cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode");
91 cursorJoinWay = ImageProvider.getCursor("crosshair", "joinway");
92 }
93
94 /**
95 * Checks if a map redraw is required and does so if needed. Also updates the status bar
96 */
97 private void redrawIfRequired() {
98 updateStatusLine();
99 if ((!drawHelperLine || wayIsFinished) && !drawTargetHighlight) return;
100 Main.map.mapView.repaint();
101 }
102
103 /**
104 * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
105 * (if feature enabled). Also sets the target cursor if appropriate.
106 */
107 private void addHighlighting() {
108 removeHighlighting();
109 // if ctrl key is held ("no join"), don't highlight anything
110 if (ctrl) {
111 Main.map.mapView.setNewCursor(cursor, this);
112 return;
113 }
114
115 // This happens when nothing is selected, but we still want to highlight the "target node"
116 if (mouseOnExistingNode == null && getCurrentDataSet().getSelected().size() == 0
117 && mousePos != null) {
118 mouseOnExistingNode = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
119 }
120
121 if (mouseOnExistingNode != null) {
122 Main.map.mapView.setNewCursor(cursorJoinNode, this);
123 // We also need this list for the statusbar help text
124 oldHighlights.add(mouseOnExistingNode);
125 if(drawTargetHighlight) {
126 mouseOnExistingNode.setHighlighted(true);
127 }
128 return;
129 }
130
131 // Insert the node into all the nearby way segments
132 if (mouseOnExistingWays.size() == 0) {
133 Main.map.mapView.setNewCursor(cursor, this);
134 return;
135 }
136
137 Main.map.mapView.setNewCursor(cursorJoinWay, this);
138
139 // We also need this list for the statusbar help text
140 oldHighlights.addAll(mouseOnExistingWays);
141 if (!drawTargetHighlight) return;
142 for (Way w : mouseOnExistingWays) {
143 w.setHighlighted(true);
144 }
145 }
146
147 /**
148 * Removes target highlighting from primitives
149 */
150 private void removeHighlighting() {
151 for(OsmPrimitive prim : oldHighlights) {
152 prim.setHighlighted(false);
153 }
154 oldHighlights = new HashSet<OsmPrimitive>();
155 }
156
157 @Override public void enterMode() {
158 if (!isEnabled())
159 return;
160 super.enterMode();
161 selectedColor =PaintColors.SELECTED.get();
162 drawHelperLine = Main.pref.getBoolean("draw.helper-line", true);
163 drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true);
164 wayIsFinished = false;
165
166 Main.map.mapView.addMouseListener(this);
167 Main.map.mapView.addMouseMotionListener(this);
168 Main.map.mapView.addTemporaryLayer(this);
169 DataSet.addSelectionListener(this);
170
171 try {
172 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
173 } catch (SecurityException ex) {
174 }
175 // would like to but haven't got mouse position yet:
176 // computeHelperLine(false, false, false);
177 }
178
179 @Override public void exitMode() {
180 super.exitMode();
181 Main.map.mapView.removeMouseListener(this);
182 Main.map.mapView.removeMouseMotionListener(this);
183 Main.map.mapView.removeTemporaryLayer(this);
184 DataSet.removeSelectionListener(this);
185 removeHighlighting();
186 try {
187 Toolkit.getDefaultToolkit().removeAWTEventListener(this);
188 } catch (SecurityException ex) {
189 }
190
191 // when exiting we let everybody know about the currently selected
192 // primitives
193 //
194 DataSet ds = getCurrentDataSet();
195 if(ds != null) {
196 ds.fireSelectionChanged();
197 }
198 }
199
200 /**
201 * redraw to (possibly) get rid of helper line if selection changes.
202 */
203 public void eventDispatched(AWTEvent event) {
204 if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
205 return;
206 updateKeyModifiers((InputEvent) event);
207 computeHelperLine();
208 addHighlighting();
209 redrawIfRequired();
210 }
211 /**
212 * redraw to (possibly) get rid of helper line if selection changes.
213 */
214 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
215 if(!Main.map.mapView.isActiveLayerDrawable())
216 return;
217 computeHelperLine();
218 addHighlighting();
219 redrawIfRequired();
220 }
221
222 private void tryAgain(MouseEvent e) {
223 getCurrentDataSet().setSelected();
224 mouseReleased(e);
225 }
226
227 /**
228 * This function should be called when the user wishes to finish his current draw action.
229 * If Potlatch Style is enabled, it will switch to select tool, otherwise simply disable
230 * the helper line until the user chooses to draw something else.
231 */
232 private void finishDrawing() {
233 // let everybody else know about the current selection
234 //
235 Main.main.getCurrentDataSet().fireSelectionChanged();
236 lastUsedNode = null;
237 wayIsFinished = true;
238 Main.map.selectSelectTool(true);
239
240 // Redraw to remove the helper line stub
241 computeHelperLine();
242 removeHighlighting();
243 redrawIfRequired();
244 }
245
246 /**
247 * If user clicked with the left button, add a node at the current mouse
248 * position.
249 *
250 * If in nodeway mode, insert the node into the way.
251 */
252 @Override public void mouseReleased(MouseEvent e) {
253 if (e.getButton() != MouseEvent.BUTTON1)
254 return;
255 if(!Main.map.mapView.isActiveLayerDrawable())
256 return;
257 // request focus in order to enable the expected keyboard shortcuts
258 //
259 Main.map.mapView.requestFocus();
260
261 if(e.getClickCount() > 1 && mousePos != null && mousePos.equals(oldMousePos)) {
262 // A double click equals "user clicked last node again, finish way"
263 // Change draw tool only if mouse position is nearly the same, as
264 // otherwise fast clicks will count as a double click
265 finishDrawing();
266 return;
267 }
268 oldMousePos = mousePos;
269
270 // we copy ctrl/alt/shift from the event just in case our global
271 // AWTEvent didn't make it through the security manager. Unclear
272 // if that can ever happen but better be safe.
273 updateKeyModifiers(e);
274 mousePos = e.getPoint();
275
276 DataSet ds = getCurrentDataSet();
277 Collection<OsmPrimitive> selection = new ArrayList<OsmPrimitive>(ds.getSelected());
278 Collection<Command> cmds = new LinkedList<Command>();
279 Collection<OsmPrimitive> newSelection = new LinkedList<OsmPrimitive>(ds.getSelected());
280
281 ArrayList<Way> reuseWays = new ArrayList<Way>(),
282 replacedWays = new ArrayList<Way>();
283 boolean newNode = false;
284 Node n = null;
285
286 if (!ctrl) {
287 n = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
288 }
289
290 if (n != null) {
291 // user clicked on node
292 if (selection.isEmpty() || wayIsFinished) {
293 // select the clicked node and do nothing else
294 // (this is just a convenience option so that people don't
295 // have to switch modes)
296 newSelection.clear();
297 newSelection.add(n);
298 getCurrentDataSet().setSelected(n);
299 // The user explicitly selected a node, so let him continue drawing
300 wayIsFinished = false;
301 return;
302 }
303 } else {
304 // no node found in clicked area
305 n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY()));
306 if (n.getCoor().isOutSideWorld()) {
307 JOptionPane.showMessageDialog(
308 Main.parent,
309 tr("Cannot add a node outside of the world."),
310 tr("Warning"),
311 JOptionPane.WARNING_MESSAGE
312 );
313 return;
314 }
315 newNode = true;
316
317 cmds.add(new AddCommand(n));
318
319 if (!ctrl) {
320 // Insert the node into all the nearby way segments
321 List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(e.getPoint(), OsmPrimitive.isSelectablePredicate);
322 Map<Way, List<Integer>> insertPoints = new HashMap<Way, List<Integer>>();
323 for (WaySegment ws : wss) {
324 List<Integer> is;
325 if (insertPoints.containsKey(ws.way)) {
326 is = insertPoints.get(ws.way);
327 } else {
328 is = new ArrayList<Integer>();
329 insertPoints.put(ws.way, is);
330 }
331
332 is.add(ws.lowerIndex);
333 }
334
335 Set<Pair<Node,Node>> segSet = new HashSet<Pair<Node,Node>>();
336
337 for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
338 Way w = insertPoint.getKey();
339 List<Integer> is = insertPoint.getValue();
340
341 Way wnew = new Way(w);
342
343 pruneSuccsAndReverse(is);
344 for (int i : is) {
345 segSet.add(
346 Pair.sort(new Pair<Node,Node>(w.getNode(i), w.getNode(i+1))));
347 }
348 for (int i : is) {
349 wnew.addNode(i + 1, n);
350 }
351
352 // If ALT is pressed, a new way should be created and that new way should get
353 // selected. This works everytime unless the ways the nodes get inserted into
354 // are already selected. This is the case when creating a self-overlapping way
355 // but pressing ALT prevents this. Therefore we must de-select the way manually
356 // here so /only/ the new way will be selected after this method finishes.
357 if(alt) {
358 newSelection.add(insertPoint.getKey());
359 }
360
361 cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
362 replacedWays.add(insertPoint.getKey());
363 reuseWays.add(wnew);
364 }
365
366 adjustNode(segSet, n);
367 }
368 }
369
370 // This part decides whether or not a "segment" (i.e. a connection) is made to an
371 // existing node.
372
373 // For a connection to be made, the user must either have a node selected (connection
374 // is made to that node), or he must have a way selected *and* one of the endpoints
375 // of that way must be the last used node (connection is made to last used node), or
376 // he must have a way and a node selected (connection is made to the selected node).
377
378 // If the above does not apply, the selection is cleared and a new try is started
379
380 boolean extendedWay = false;
381 boolean wayIsFinishedTemp = wayIsFinished;
382 wayIsFinished = false;
383
384 // don't draw lines if shift is held
385 if (selection.size() > 0 && !shift) {
386 Node selectedNode = null;
387 Way selectedWay = null;
388
389 for (OsmPrimitive p : selection) {
390 if (p instanceof Node) {
391 if (selectedNode != null) {
392 // Too many nodes selected to do something useful
393 tryAgain(e);
394 return;
395 }
396 selectedNode = (Node) p;
397 } else if (p instanceof Way) {
398 if (selectedWay != null) {
399 // Too many ways selected to do something useful
400 tryAgain(e);
401 return;
402 }
403 selectedWay = (Way) p;
404 }
405 }
406
407 // the node from which we make a connection
408 Node n0 = findNodeToContinueFrom(selectedNode, selectedWay);
409 // We have a selection but it isn't suitable. Try again.
410 if(n0 == null) {
411 tryAgain(e);
412 return;
413 }
414 if(!wayIsFinishedTemp){
415 if(isSelfContainedWay(selectedWay, n0, n))
416 return;
417
418 // User clicked last node again, finish way
419 if(n0 == n) {
420 finishDrawing();
421 return;
422 }
423
424 // Ok we know now that we'll insert a line segment, but will it connect to an
425 // existing way or make a new way of its own? The "alt" modifier means that the
426 // user wants a new way.
427 Way way = alt ? null : (selectedWay != null) ? selectedWay : getWayForNode(n0);
428 Way wayToSelect;
429
430 // Don't allow creation of self-overlapping ways
431 if(way != null) {
432 int nodeCount=0;
433 for (Node p : way.getNodes())
434 if(p.equals(n0)) {
435 nodeCount++;
436 }
437 if(nodeCount > 1) {
438 way = null;
439 }
440 }
441
442 if (way == null) {
443 way = new Way();
444 way.addNode(n0);
445 cmds.add(new AddCommand(way));
446 wayToSelect = way;
447 } else {
448 int i;
449 if ((i = replacedWays.indexOf(way)) != -1) {
450 way = reuseWays.get(i);
451 wayToSelect = way;
452 } else {
453 wayToSelect = way;
454 Way wnew = new Way(way);
455 cmds.add(new ChangeCommand(way, wnew));
456 way = wnew;
457 }
458 }
459
460 // Connected to a node that's already in the way
461 if(way.containsNode(n)) {
462 wayIsFinished = true;
463 selection.clear();
464 }
465
466 // Add new node to way
467 if (way.getNode(way.getNodesCount() - 1) == n0) {
468 way.addNode(n);
469 } else {
470 way.addNode(0, n);
471 }
472
473 extendedWay = true;
474 newSelection.clear();
475 newSelection.add(wayToSelect);
476 }
477 }
478
479 String title;
480 if (!extendedWay) {
481 if (!newNode)
482 return; // We didn't do anything.
483 else if (reuseWays.isEmpty()) {
484 title = tr("Add node");
485 } else {
486 title = tr("Add node into way");
487 for (Way w : reuseWays) {
488 newSelection.remove(w);
489 }
490 }
491 newSelection.clear();
492 newSelection.add(n);
493 } else if (!newNode) {
494 title = tr("Connect existing way to node");
495 } else if (reuseWays.isEmpty()) {
496 title = tr("Add a new node to an existing way");
497 } else {
498 title = tr("Add node into way and connect");
499 }
500
501 Command c = new SequenceCommand(title, cmds);
502
503 Main.main.undoRedo.add(c);
504 if(!wayIsFinished) {
505 lastUsedNode = n;
506 }
507
508 getCurrentDataSet().setSelected(newSelection);
509
510 // "viewport following" mode for tracing long features
511 // from aerial imagery or GPS tracks.
512 if (n != null && Main.map.mapView.viewportFollowing) {
513 Main.map.mapView.smoothScrollTo(n.getEastNorth());
514 };
515 computeHelperLine();
516 removeHighlighting();
517 redrawIfRequired();
518 }
519
520 /**
521 * Prevent creation of ways that look like this: <---->
522 * This happens if users want to draw a no-exit-sideway from the main way like this:
523 * ^
524 * |<---->
525 * |
526 * The solution isn't ideal because the main way will end in the side way, which is bad for
527 * navigation software ("drive straight on") but at least easier to fix. Maybe users will fix
528 * it on their own, too. At least it's better than producing an error.
529 *
530 * @param Way the way to check
531 * @param Node the current node (i.e. the one the connection will be made from)
532 * @param Node the target node (i.e. the one the connection will be made to)
533 * @return Boolean True if this would create a selfcontaining way, false otherwise.
534 */
535 private boolean isSelfContainedWay(Way selectedWay, Node currentNode, Node targetNode) {
536 if(selectedWay != null) {
537 int posn0 = selectedWay.getNodes().indexOf(currentNode);
538 if( posn0 != -1 && // n0 is part of way
539 (posn0 >= 1 && targetNode.equals(selectedWay.getNode(posn0-1))) || // previous node
540 (posn0 < selectedWay.getNodesCount()-1) && targetNode.equals(selectedWay.getNode(posn0+1))) { // next node
541 getCurrentDataSet().setSelected(targetNode);
542 lastUsedNode = targetNode;
543 return true;
544 }
545 }
546
547 return false;
548 }
549
550 /**
551 * Finds a node to continue drawing from. Decision is based upon given node and way.
552 * @param selectedNode Currently selected node, may be null
553 * @param selectedWay Currently selected way, may be null
554 * @return Node if a suitable node is found, null otherwise
555 */
556 private Node findNodeToContinueFrom(Node selectedNode, Way selectedWay) {
557 // No nodes or ways have been selected, this occurs when a relation
558 // has been selected or the selection is empty
559 if(selectedNode == null && selectedWay == null)
560 return null;
561
562 if (selectedNode == null) {
563 if (selectedWay.isFirstLastNode(lastUsedNode))
564 return lastUsedNode;
565
566 // We have a way selected, but no suitable node to continue from. Start anew.
567 return null;
568 }
569
570 if (selectedWay == null)
571 return selectedNode;
572
573 if (selectedWay.isFirstLastNode(selectedNode))
574 return selectedNode;
575
576 // We have a way and node selected, but it's not at the start/end of the way. Start anew.
577 return null;
578 }
579
580 @Override public void mouseDragged(MouseEvent e) {
581 mouseMoved(e);
582 }
583
584 @Override public void mouseMoved(MouseEvent e) {
585 if(!Main.map.mapView.isActiveLayerDrawable())
586 return;
587
588 // we copy ctrl/alt/shift from the event just in case our global
589 // AWTEvent didn't make it through the security manager. Unclear
590 // if that can ever happen but better be safe.
591 updateKeyModifiers(e);
592 mousePos = e.getPoint();
593
594 computeHelperLine();
595 addHighlighting();
596 redrawIfRequired();
597 }
598
599 /**
600 * This method prepares data required for painting the "helper line" from
601 * the last used position to the mouse cursor. It duplicates some code from
602 * mouseReleased() (FIXME).
603 */
604 private void computeHelperLine() {
605 MapView mv = Main.map.mapView;
606 if (mousePos == null) {
607 // Don't draw the line.
608 currentMouseEastNorth = null;
609 currentBaseNode = null;
610 return;
611 }
612
613 double distance = -1;
614 double angle = -1;
615
616 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
617
618 Node selectedNode = null;
619 Way selectedWay = null;
620 Node currentMouseNode = null;
621 mouseOnExistingNode = null;
622 mouseOnExistingWays = new HashSet<Way>();
623
624 Main.map.statusLine.setAngle(-1);
625 Main.map.statusLine.setHeading(-1);
626 Main.map.statusLine.setDist(-1);
627
628 if (!ctrl && mousePos != null) {
629 currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
630 }
631
632 // We need this for highlighting and we'll only do so if we actually want to re-use
633 // *and* there is no node nearby (because nodes beat ways when re-using)
634 if(!ctrl && currentMouseNode == null) {
635 List<WaySegment> wss = mv.getNearestWaySegments(mousePos, OsmPrimitive.isSelectablePredicate);
636 for(WaySegment ws : wss) {
637 mouseOnExistingWays.add(ws.way);
638 }
639 }
640
641 if (currentMouseNode != null) {
642 // user clicked on node
643 if (selection.isEmpty()) return;
644 currentMouseEastNorth = currentMouseNode.getEastNorth();
645 mouseOnExistingNode = currentMouseNode;
646 } else {
647 // no node found in clicked area
648 currentMouseEastNorth = mv.getEastNorth(mousePos.x, mousePos.y);
649 }
650
651 for (OsmPrimitive p : selection) {
652 if (p instanceof Node) {
653 if (selectedNode != null) return;
654 selectedNode = (Node) p;
655 } else if (p instanceof Way) {
656 if (selectedWay != null) return;
657 selectedWay = (Way) p;
658 }
659 }
660
661 // the node from which we make a connection
662 currentBaseNode = null;
663 Node previousNode = null;
664
665 if (selectedNode == null) {
666 if (selectedWay == null)
667 return;
668 if (selectedWay.isFirstLastNode(lastUsedNode)) {
669 currentBaseNode = lastUsedNode;
670 if (lastUsedNode == selectedWay.getNode(selectedWay.getNodesCount()-1) && selectedWay.getNodesCount() > 1) {
671 previousNode = selectedWay.getNode(selectedWay.getNodesCount()-2);
672 }
673 }
674 } else if (selectedWay == null) {
675 currentBaseNode = selectedNode;
676 } else {
677 if (selectedNode == selectedWay.getNode(0) || selectedNode == selectedWay.getNode(selectedWay.getNodesCount()-1)) {
678 currentBaseNode = selectedNode;
679 }
680 }
681
682 if (currentBaseNode == null || currentBaseNode == currentMouseNode)
683 return; // Don't create zero length way segments.
684
685 // find out the distance, in metres, between the base point and the mouse cursor
686 LatLon mouseLatLon = mv.getProjection().eastNorth2latlon(currentMouseEastNorth);
687 distance = currentBaseNode.getCoor().greatCircleDistance(mouseLatLon);
688
689 double hdg = Math.toDegrees(currentBaseNode.getEastNorth()
690 .heading(currentMouseEastNorth));
691 if (previousNode != null) {
692 angle = hdg - Math.toDegrees(previousNode.getEastNorth()
693 .heading(currentBaseNode.getEastNorth()));
694 angle += angle < 0 ? 360 : 0;
695 }
696
697 Main.map.statusLine.setAngle(angle);
698 Main.map.statusLine.setHeading(hdg);
699 Main.map.statusLine.setDist(distance);
700 // Now done in redrawIfRequired()
701 //updateStatusLine();
702 }
703
704 /**
705 * Repaint on mouse exit so that the helper line goes away.
706 */
707 @Override public void mouseExited(MouseEvent e) {
708 if(!Main.map.mapView.isActiveLayerDrawable())
709 return;
710 mousePos = e.getPoint();
711 Main.map.mapView.repaint();
712 }
713
714 /**
715 * @return If the node is the end of exactly one way, return this.
716 * <code>null</code> otherwise.
717 */
718 public Way getWayForNode(Node n) {
719 Way way = null;
720 for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) {
721 if (!w.isUsable() || w.getNodesCount() < 1) {
722 continue;
723 }
724 Node firstNode = w.getNode(0);
725 Node lastNode = w.getNode(w.getNodesCount() - 1);
726 if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) {
727 if (way != null)
728 return null;
729 way = w;
730 }
731 }
732 return way;
733 }
734
735 public Node getCurrentBaseNode() {
736 return currentBaseNode;
737 }
738
739 private static void pruneSuccsAndReverse(List<Integer> is) {
740 //if (is.size() < 2) return;
741
742 HashSet<Integer> is2 = new HashSet<Integer>();
743 for (int i : is) {
744 if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
745 is2.add(i);
746 }
747 }
748 is.clear();
749 is.addAll(is2);
750 Collections.sort(is);
751 Collections.reverse(is);
752 }
753
754 /**
755 * Adjusts the position of a node to lie on a segment (or a segment
756 * intersection).
757 *
758 * If one or more than two segments are passed, the node is adjusted
759 * to lie on the first segment that is passed.
760 *
761 * If two segments are passed, the node is adjusted to be at their
762 * intersection.
763 *
764 * No action is taken if no segments are passed.
765 *
766 * @param segs the segments to use as a reference when adjusting
767 * @param n the node to adjust
768 */
769 private static void adjustNode(Collection<Pair<Node,Node>> segs, Node n) {
770
771 switch (segs.size()) {
772 case 0:
773 return;
774 case 2:
775 // This computes the intersection between
776 // the two segments and adjusts the node position.
777 Iterator<Pair<Node,Node>> i = segs.iterator();
778 Pair<Node,Node> seg = i.next();
779 EastNorth A = seg.a.getEastNorth();
780 EastNorth B = seg.b.getEastNorth();
781 seg = i.next();
782 EastNorth C = seg.a.getEastNorth();
783 EastNorth D = seg.b.getEastNorth();
784
785 double u=det(B.east() - A.east(), B.north() - A.north(), C.east() - D.east(), C.north() - D.north());
786
787 // Check for parallel segments and do nothing if they are
788 // In practice this will probably only happen when a way has been duplicated
789
790 if (u == 0) return;
791
792 // q is a number between 0 and 1
793 // It is the point in the segment where the intersection occurs
794 // if the segment is scaled to lenght 1
795
796 double q = det(B.north() - C.north(), B.east() - C.east(), D.north() - C.north(), D.east() - C.east()) / u;
797 EastNorth intersection = new EastNorth(
798 B.east() + q * (A.east() - B.east()),
799 B.north() + q * (A.north() - B.north()));
800
801 int snapToIntersectionThreshold
802 = Main.pref.getInteger("edit.snap-intersection-threshold",10);
803
804 // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise
805 // fall through to default action.
806 // (for semi-parallel lines, intersection might be miles away!)
807 if (Main.map.mapView.getPoint(n).distance(Main.map.mapView.getPoint(intersection)) < snapToIntersectionThreshold) {
808 n.setEastNorth(intersection);
809 return;
810 }
811
812 default:
813 EastNorth P = n.getEastNorth();
814 seg = segs.iterator().next();
815 A = seg.a.getEastNorth();
816 B = seg.b.getEastNorth();
817 double a = P.distanceSq(B);
818 double b = P.distanceSq(A);
819 double c = A.distanceSq(B);
820 q = (a - b + c) / (2*c);
821 n.setEastNorth(new EastNorth(B.east() + q * (A.east() - B.east()), B.north() + q * (A.north() - B.north())));
822 }
823 }
824
825 // helper for adjustNode
826 static double det(double a, double b, double c, double d) {
827 return a * d - b * c;
828 }
829
830 public void paint(Graphics2D g, MapView mv, Bounds box) {
831 if (!drawHelperLine || wayIsFinished || shift) return;
832
833 // sanity checks
834 if (Main.map.mapView == null) return;
835 if (mousePos == null) return;
836
837 // don't draw line if we don't know where from or where to
838 if (currentBaseNode == null || currentMouseEastNorth == null) return;
839
840 // don't draw line if mouse is outside window
841 if (!Main.map.mapView.getBounds().contains(mousePos)) return;
842
843 Graphics2D g2 = g;
844 g2.setColor(selectedColor);
845 g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
846 GeneralPath b = new GeneralPath();
847 Point p1=mv.getPoint(currentBaseNode);
848 Point p2=mv.getPoint(currentMouseEastNorth);
849
850 double t = Math.atan2(p2.y-p1.y, p2.x-p1.x) + Math.PI;
851
852 b.moveTo(p1.x,p1.y); b.lineTo(p2.x, p2.y);
853
854 // if alt key is held ("start new way"), draw a little perpendicular line
855 if (alt) {
856 b.moveTo((int)(p1.x + 8*Math.cos(t+PHI)), (int)(p1.y + 8*Math.sin(t+PHI)));
857 b.lineTo((int)(p1.x + 8*Math.cos(t-PHI)), (int)(p1.y + 8*Math.sin(t-PHI)));
858 }
859
860 g2.draw(b);
861 g2.setStroke(new BasicStroke(1));
862 }
863
864 @Override public String getModeHelpText() {
865 String rv = "";
866 /*
867 * No modifiers: all (Connect, Node Re-Use, Auto-Weld)
868 * CTRL: disables node re-use, auto-weld
869 * Shift: do not make connection
870 * ALT: make connection but start new way in doing so
871 */
872
873 /*
874 * Status line text generation is split into two parts to keep it maintainable.
875 * First part looks at what will happen to the new node inserted on click and
876 * the second part will look if a connection is made or not.
877 *
878 * Note that this help text is not absolutely accurate as it doesn't catch any special
879 * cases (e.g. when preventing <---> ways). The only special that it catches is when
880 * a way is about to be finished.
881 *
882 * First check what happens to the new node.
883 */
884
885 // oldHighlights stores the current highlights. If this
886 // list is empty we can assume that we won't do any joins
887 if (ctrl || oldHighlights.isEmpty()) {
888 rv = tr("Create new node.");
889 } else {
890 // oldHighlights may store a node or way, check if it's a node
891 OsmPrimitive x = oldHighlights.iterator().next();
892 if (x instanceof Node) {
893 rv = tr("Select node under cursor.");
894 } else {
895 rv = trn("Insert new node into way.", "Insert new node into {0} ways.",
896 oldHighlights.size(), oldHighlights.size());
897 }
898 }
899
900 /*
901 * Check whether a connection will be made
902 */
903 if (currentBaseNode != null && !wayIsFinished) {
904 if (alt) {
905 rv += " " + tr("Start new way from last node.");
906 } else {
907 rv += " " + tr("Continue way from last node.");
908 }
909 }
910
911 Node n = mouseOnExistingNode;
912 /*
913 * Handle special case: Highlighted node == selected node => finish drawing
914 */
915 if (n != null && getCurrentDataSet() != null && getCurrentDataSet().getSelectedNodes().contains(n)) {
916 if (wayIsFinished) {
917 rv = tr("Select node under cursor.");
918 } else {
919 rv = tr("Finish drawing.");
920 }
921 }
922
923 /*
924 * Handle special case: Self-Overlapping or closing way
925 */
926 if (getCurrentDataSet() != null && getCurrentDataSet().getSelectedWays().size() > 0 && !wayIsFinished && !alt) {
927 Way w = getCurrentDataSet().getSelectedWays().iterator().next();
928 for (Node m : w.getNodes()) {
929 if (m.equals(mouseOnExistingNode) || mouseOnExistingWays.contains(w)) {
930 rv += " " + tr("Finish drawing.");
931 break;
932 }
933 }
934 }
935 return rv;
936 }
937
938 @Override public boolean layerIsSupported(Layer l) {
939 return l instanceof OsmDataLayer;
940 }
941
942 @Override
943 protected void updateEnabledState() {
944 setEnabled(getEditLayer() != null);
945 }
946
947 @Override
948 public void destroy() {
949 super.destroy();
950 Main.unregisterActionShortcut(extraShortcut);
951 }
952}
Note: See TracBrowser for help on using the repository browser.