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

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

Andle Snapping improvements: activation on A (Tab can be disabled, see #7438), better projections, show activation in status line

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