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

Last change on this file since 5820 was 5820, checked in by akks, 11 years ago

Angle indicator and angle snapping worked incorrect when line was continued from the start (no from the end)
Allow angle snapping for Alt-drawing (creating new way aligned to previous one)

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