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

Last change on this file since 5447 was 5382, checked in by Don-vip, 12 years ago

Small code cleanup/refactorization in shortcut unregistration

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