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

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

fix #2961 - Improve usability of WMS Layer Saving/Loading

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