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

Last change on this file since 5768 was 5768, checked in by stoecker, 11 years ago

see #8447 - fix color name

  • Property svn:eol-style set to native
File size: 65.9 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 if (selectedNode == null) {
877 if (selectedWay == null)
878 return;
879 if (selectedWay.isFirstLastNode(lastUsedNode)) {
880 currentBaseNode = lastUsedNode;
881 if (lastUsedNode == selectedWay.getNode(selectedWay.getNodesCount()-1) && selectedWay.getNodesCount() > 1) {
882 previousNode = selectedWay.getNode(selectedWay.getNodesCount()-2);
883 }
884 }
885 } else if (selectedWay == null) {
886 currentBaseNode = selectedNode;
887 } else if (!selectedWay.isDeleted()) { // fix #7118
888 if (selectedNode == selectedWay.getNode(0)){
889 currentBaseNode = selectedNode;
890 if (selectedWay.getNodesCount()>1) {
891 previousNode = selectedWay.getNode(1);
892 }
893 }
894 if (selectedNode == selectedWay.lastNode()) {
895 currentBaseNode = selectedNode;
896 if (selectedWay.getNodesCount()>1) {
897 previousNode = selectedWay.getNode(selectedWay.getNodesCount()-2);
898 }
899 }
900 }
901 }
902
903
904 /**
905 * Repaint on mouse exit so that the helper line goes away.
906 */
907 @Override public void mouseExited(MouseEvent e) {
908 if(!Main.map.mapView.isActiveLayerDrawable())
909 return;
910 mousePos = e.getPoint();
911 snapHelper.noSnapNow();
912 boolean repaintIssued = removeHighlighting();
913 // force repaint in case snapHelper needs one. If removeHighlighting
914 // caused one already, don’t do it again.
915 if(!repaintIssued) {
916 Main.map.mapView.repaint();
917 }
918 }
919
920 /**
921 * @return If the node is the end of exactly one way, return this.
922 * <code>null</code> otherwise.
923 */
924 public static Way getWayForNode(Node n) {
925 Way way = null;
926 for (Way w : Utils.filteredCollection(n.getReferrers(), Way.class)) {
927 if (!w.isUsable() || w.getNodesCount() < 1) {
928 continue;
929 }
930 Node firstNode = w.getNode(0);
931 Node lastNode = w.getNode(w.getNodesCount() - 1);
932 if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) {
933 if (way != null)
934 return null;
935 way = w;
936 }
937 }
938 return way;
939 }
940
941 public Node getCurrentBaseNode() {
942 return currentBaseNode;
943 }
944
945 private static void pruneSuccsAndReverse(List<Integer> is) {
946 HashSet<Integer> is2 = new HashSet<Integer>();
947 for (int i : is) {
948 if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
949 is2.add(i);
950 }
951 }
952 is.clear();
953 is.addAll(is2);
954 Collections.sort(is);
955 Collections.reverse(is);
956 }
957
958 /**
959 * Adjusts the position of a node to lie on a segment (or a segment
960 * intersection).
961 *
962 * If one or more than two segments are passed, the node is adjusted
963 * to lie on the first segment that is passed.
964 *
965 * If two segments are passed, the node is adjusted to be at their
966 * intersection.
967 *
968 * No action is taken if no segments are passed.
969 *
970 * @param segs the segments to use as a reference when adjusting
971 * @param n the node to adjust
972 */
973 private static void adjustNode(Collection<Pair<Node,Node>> segs, Node n) {
974
975 switch (segs.size()) {
976 case 0:
977 return;
978 case 2:
979 // This computes the intersection between
980 // the two segments and adjusts the node position.
981 Iterator<Pair<Node,Node>> i = segs.iterator();
982 Pair<Node,Node> seg = i.next();
983 EastNorth A = seg.a.getEastNorth();
984 EastNorth B = seg.b.getEastNorth();
985 seg = i.next();
986 EastNorth C = seg.a.getEastNorth();
987 EastNorth D = seg.b.getEastNorth();
988
989 double u=det(B.east() - A.east(), B.north() - A.north(), C.east() - D.east(), C.north() - D.north());
990
991 // Check for parallel segments and do nothing if they are
992 // In practice this will probably only happen when a way has been duplicated
993
994 if (u == 0)
995 return;
996
997 // q is a number between 0 and 1
998 // It is the point in the segment where the intersection occurs
999 // if the segment is scaled to lenght 1
1000
1001 double q = det(B.north() - C.north(), B.east() - C.east(), D.north() - C.north(), D.east() - C.east()) / u;
1002 EastNorth intersection = new EastNorth(
1003 B.east() + q * (A.east() - B.east()),
1004 B.north() + q * (A.north() - B.north()));
1005
1006 int snapToIntersectionThreshold
1007 = Main.pref.getInteger("edit.snap-intersection-threshold",10);
1008
1009 // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise
1010 // fall through to default action.
1011 // (for semi-parallel lines, intersection might be miles away!)
1012 if (Main.map.mapView.getPoint2D(n).distance(Main.map.mapView.getPoint2D(intersection)) < snapToIntersectionThreshold) {
1013 n.setEastNorth(intersection);
1014 return;
1015 }
1016 default:
1017 EastNorth P = n.getEastNorth();
1018 seg = segs.iterator().next();
1019 A = seg.a.getEastNorth();
1020 B = seg.b.getEastNorth();
1021 double a = P.distanceSq(B);
1022 double b = P.distanceSq(A);
1023 double c = A.distanceSq(B);
1024 q = (a - b + c) / (2*c);
1025 n.setEastNorth(new EastNorth(B.east() + q * (A.east() - B.east()), B.north() + q * (A.north() - B.north())));
1026 }
1027 }
1028
1029 // helper for adjustNode
1030 static double det(double a, double b, double c, double d) {
1031 return a * d - b * c;
1032 }
1033
1034 private void tryToMoveNodeOnIntersection(List<WaySegment> wss, Node n) {
1035 if (wss.isEmpty())
1036 return;
1037 WaySegment ws = wss.get(0);
1038 EastNorth p1=ws.getFirstNode().getEastNorth();
1039 EastNorth p2=ws.getSecondNode().getEastNorth();
1040 if (snapHelper.dir2!=null && currentBaseNode!=null) {
1041 EastNorth xPoint = Geometry.getSegmentSegmentIntersection(p1, p2, snapHelper.dir2, currentBaseNode.getEastNorth());
1042 if (xPoint!=null) {
1043 n.setEastNorth(xPoint);
1044 }
1045 }
1046 }
1047 /**
1048 * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
1049 * (if feature enabled). Also sets the target cursor if appropriate. It adds the to-be-
1050 * highlighted primitives to newHighlights but does not actually highlight them. This work is
1051 * done in redrawIfRequired. This means, calling addHighlighting() without redrawIfRequired()
1052 * will leave the data in an inconsistent state.
1053 *
1054 * The status bar derives its information from oldHighlights, so in order to update the status
1055 * bar both addHighlighting() and repaintIfRequired() are needed, since former fills newHighlights
1056 * and latter processes them into oldHighlights.
1057 */
1058 private void addHighlighting() {
1059 newHighlights = new HashSet<OsmPrimitive>();
1060
1061 // if ctrl key is held ("no join"), don't highlight anything
1062 if (ctrl) {
1063 Main.map.mapView.setNewCursor(cursor, this);
1064 redrawIfRequired();
1065 return;
1066 }
1067
1068 // This happens when nothing is selected, but we still want to highlight the "target node"
1069 if (mouseOnExistingNode == null && getCurrentDataSet().getSelected().size() == 0
1070 && mousePos != null) {
1071 mouseOnExistingNode = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
1072 }
1073
1074 if (mouseOnExistingNode != null) {
1075 Main.map.mapView.setNewCursor(cursorJoinNode, this);
1076 newHighlights.add(mouseOnExistingNode);
1077 redrawIfRequired();
1078 return;
1079 }
1080
1081 // Insert the node into all the nearby way segments
1082 if (mouseOnExistingWays.size() == 0) {
1083 Main.map.mapView.setNewCursor(cursor, this);
1084 redrawIfRequired();
1085 return;
1086 }
1087
1088 Main.map.mapView.setNewCursor(cursorJoinWay, this);
1089 newHighlights.addAll(mouseOnExistingWays);
1090 redrawIfRequired();
1091 }
1092
1093 /**
1094 * Removes target highlighting from primitives. Issues repaint if required.
1095 * Returns true if a repaint has been issued.
1096 */
1097 private boolean removeHighlighting() {
1098 newHighlights = new HashSet<OsmPrimitive>();
1099 return redrawIfRequired();
1100 }
1101
1102 public void paint(Graphics2D g, MapView mv, Bounds box) {
1103 // sanity checks
1104 if (Main.map.mapView == null || mousePos == null
1105 // don't draw line if we don't know where from or where to
1106 || currentBaseNode == null || currentMouseEastNorth == null
1107 // don't draw line if mouse is outside window
1108 || !Main.map.mapView.getBounds().contains(mousePos))
1109 return;
1110
1111 Graphics2D g2 = g;
1112 snapHelper.drawIfNeeded(g2,mv);
1113 if (!drawHelperLine || wayIsFinished || shift)
1114 return;
1115
1116 if (!snapHelper.isActive()) { // else use color and stoke from snapHelper.draw
1117 g2.setColor(rubberLineColor);
1118 g2.setStroke(rubberLineStroke);
1119 } else if (!snapHelper.drawConstructionGeometry)
1120 return;
1121 GeneralPath b = new GeneralPath();
1122 Point p1=mv.getPoint(currentBaseNode);
1123 Point p2=mv.getPoint(currentMouseEastNorth);
1124
1125 double t = Math.atan2(p2.y-p1.y, p2.x-p1.x) + Math.PI;
1126
1127 b.moveTo(p1.x,p1.y); b.lineTo(p2.x, p2.y);
1128
1129 // if alt key is held ("start new way"), draw a little perpendicular line
1130 if (alt) {
1131 b.moveTo((int)(p1.x + 8*Math.cos(t+PHI)), (int)(p1.y + 8*Math.sin(t+PHI)));
1132 b.lineTo((int)(p1.x + 8*Math.cos(t-PHI)), (int)(p1.y + 8*Math.sin(t-PHI)));
1133 }
1134
1135 g2.draw(b);
1136 g2.setStroke(BASIC_STROKE);
1137 }
1138
1139 @Override
1140 public String getModeHelpText() {
1141 String rv = "";
1142 /*
1143 * No modifiers: all (Connect, Node Re-Use, Auto-Weld)
1144 * CTRL: disables node re-use, auto-weld
1145 * Shift: do not make connection
1146 * ALT: make connection but start new way in doing so
1147 */
1148
1149 /*
1150 * Status line text generation is split into two parts to keep it maintainable.
1151 * First part looks at what will happen to the new node inserted on click and
1152 * the second part will look if a connection is made or not.
1153 *
1154 * Note that this help text is not absolutely accurate as it doesn't catch any special
1155 * cases (e.g. when preventing <---> ways). The only special that it catches is when
1156 * a way is about to be finished.
1157 *
1158 * First check what happens to the new node.
1159 */
1160
1161 // oldHighlights stores the current highlights. If this
1162 // list is empty we can assume that we won't do any joins
1163 if (ctrl || oldHighlights.isEmpty()) {
1164 rv = tr("Create new node.");
1165 } else {
1166 // oldHighlights may store a node or way, check if it's a node
1167 OsmPrimitive x = oldHighlights.iterator().next();
1168 if (x instanceof Node) {
1169 rv = tr("Select node under cursor.");
1170 } else {
1171 rv = trn("Insert new node into way.", "Insert new node into {0} ways.",
1172 oldHighlights.size(), oldHighlights.size());
1173 }
1174 }
1175
1176 /*
1177 * Check whether a connection will be made
1178 */
1179 if (currentBaseNode != null && !wayIsFinished) {
1180 if (alt) {
1181 rv += " " + tr("Start new way from last node.");
1182 } else {
1183 rv += " " + tr("Continue way from last node.");
1184 }
1185 if (snapHelper.isSnapOn()) {
1186 rv += " "+ tr("Angle snapping active.");
1187 }
1188 }
1189
1190 Node n = mouseOnExistingNode;
1191 /*
1192 * Handle special case: Highlighted node == selected node => finish drawing
1193 */
1194 if (n != null && getCurrentDataSet() != null && getCurrentDataSet().getSelectedNodes().contains(n)) {
1195 if (wayIsFinished) {
1196 rv = tr("Select node under cursor.");
1197 } else {
1198 rv = tr("Finish drawing.");
1199 }
1200 }
1201
1202 /*
1203 * Handle special case: Self-Overlapping or closing way
1204 */
1205 if (getCurrentDataSet() != null && getCurrentDataSet().getSelectedWays().size() > 0 && !wayIsFinished && !alt) {
1206 Way w = getCurrentDataSet().getSelectedWays().iterator().next();
1207 for (Node m : w.getNodes()) {
1208 if (m.equals(mouseOnExistingNode) || mouseOnExistingWays.contains(w)) {
1209 rv += " " + tr("Finish drawing.");
1210 break;
1211 }
1212 }
1213 }
1214 return rv;
1215 }
1216
1217 /**
1218 * Get selected primitives, while draw action is in progress.
1219 *
1220 * While drawing a way, technically the last node is selected.
1221 * This is inconvenient when the user tries to add tags to the
1222 * way using a keyboard shortcut. In that case, this method returns
1223 * the current way as selection, to work around this issue.
1224 * Otherwise the normal selection of the current data layer is returned.
1225 */
1226 public Collection<OsmPrimitive> getInProgressSelection() {
1227 DataSet ds = getCurrentDataSet();
1228 if (ds == null) return null;
1229 if (currentBaseNode != null && !ds.getSelected().isEmpty()) {
1230 Way continueFrom = getWayForNode(currentBaseNode);
1231 if (alt && continueFrom != null)
1232 return Collections.<OsmPrimitive>singleton(continueFrom);
1233 }
1234 return ds.getSelected();
1235 }
1236
1237 @Override
1238 public boolean layerIsSupported(Layer l) {
1239 return l instanceof OsmDataLayer;
1240 }
1241
1242 @Override
1243 protected void updateEnabledState() {
1244 setEnabled(getEditLayer() != null);
1245 }
1246
1247 @Override
1248 public void destroy() {
1249 super.destroy();
1250 snapChangeAction.destroy();
1251 }
1252
1253 public class BackSpaceAction extends AbstractAction {
1254
1255 @Override
1256 public void actionPerformed(ActionEvent e) {
1257 Main.main.undoRedo.undo();
1258 Node n=null;
1259 Command lastCmd=Main.main.undoRedo.commands.peekLast();
1260 if (lastCmd==null) return;
1261 for (OsmPrimitive p: lastCmd.getParticipatingPrimitives()) {
1262 if (p instanceof Node) {
1263 if (n==null) {
1264 n=(Node) p; // found one node
1265 wayIsFinished=false;
1266 } else {
1267 // if more than 1 node were affected by previous command,
1268 // we have no way to continue, so we forget about found node
1269 n=null;
1270 break;
1271 }
1272 }
1273 }
1274 // select last added node - maybe we will continue drawing from it
1275 if (n!=null) {
1276 getCurrentDataSet().addSelected(n);
1277 }
1278 }
1279 }
1280
1281 private class SnapHelper {
1282 boolean snapOn; // snapping is turned on
1283
1284 private boolean active; // snapping is active for current mouse position
1285 private boolean fixed; // snap angle is fixed
1286 private boolean absoluteFix; // snap angle is absolute
1287
1288 private boolean drawConstructionGeometry;
1289 private boolean showProjectedPoint;
1290 private boolean showAngle;
1291
1292 private boolean snapToProjections;
1293
1294 EastNorth dir2;
1295 EastNorth projected;
1296 String labelText;
1297 double lastAngle;
1298
1299 double customBaseHeading=-1; // angle of base line, if not last segment)
1300 private EastNorth segmentPoint1; // remembered first point of base segment
1301 private EastNorth segmentPoint2; // remembered second point of base segment
1302 private EastNorth projectionSource; // point that we are projecting to the line
1303
1304 double snapAngles[];
1305 double snapAngleTolerance;
1306
1307 double pe,pn; // (pe,pn) - direction of snapping line
1308 double e0,n0; // (e0,n0) - origin of snapping line
1309
1310 final String fixFmt="%d "+tr("FIX");
1311 Color snapHelperColor;
1312 private Color highlightColor;
1313
1314 private Stroke normalStroke;
1315 private Stroke helperStroke;
1316 private Stroke highlightStroke;
1317
1318 JCheckBoxMenuItem checkBox;
1319 public final Color ORANGE_TRANSPARENT = new Color(Color.ORANGE.getRed(),Color.ORANGE.getGreen(),Color.ORANGE.getBlue(),128);
1320
1321 public void init() {
1322 snapOn=false;
1323 checkBox.setState(snapOn);
1324 fixed=false; absoluteFix=false;
1325
1326 Collection<String> angles = Main.pref.getCollection("draw.anglesnap.angles",
1327 Arrays.asList("0","30","45","60","90","120","135","150","180"));
1328
1329 snapAngles = new double[2*angles.size()];
1330 int i=0;
1331 for (String s: angles) {
1332 try {
1333 snapAngles[i] = Double.parseDouble(s); i++;
1334 snapAngles[i] = 360-Double.parseDouble(s); i++;
1335 } catch (NumberFormatException e) {
1336 System.err.println("Warning: incorrect number in draw.anglesnap.angles preferences: "+s);
1337 snapAngles[i]=0;i++;
1338 snapAngles[i]=0;i++;
1339 }
1340 }
1341 snapAngleTolerance = Main.pref.getDouble("draw.anglesnap.tolerance", 5.0);
1342 drawConstructionGeometry = Main.pref.getBoolean("draw.anglesnap.drawConstructionGeometry", true);
1343 showProjectedPoint = Main.pref.getBoolean("draw.anglesnap.drawProjectedPoint", true);
1344 snapToProjections = Main.pref.getBoolean("draw.anglesnap.projectionsnap", true);
1345
1346 showAngle = Main.pref.getBoolean("draw.anglesnap.showAngle", true);
1347 useRepeatedShortcut = Main.pref.getBoolean("draw.anglesnap.toggleOnRepeatedA", true);
1348
1349 normalStroke = rubberLineStroke;
1350 snapHelperColor = Main.pref.getColor(marktr("draw angle snap"), Color.ORANGE);
1351
1352 highlightColor = Main.pref.getColor(marktr("draw angle snap highlight"), ORANGE_TRANSPARENT);
1353 highlightStroke = GuiHelper.getCustomizedStroke(Main.pref.get("draw.anglesnap.stroke.highlight","10"));
1354 helperStroke = GuiHelper.getCustomizedStroke(Main.pref.get("draw.anglesnap.stroke.helper","1 4"));
1355 }
1356
1357 public void saveAngles(String ... angles) {
1358 Main.pref.putCollection("draw.anglesnap.angles", Arrays.asList(angles));
1359 }
1360
1361 public void setMenuCheckBox(JCheckBoxMenuItem checkBox) {
1362 this.checkBox = checkBox;
1363 }
1364
1365 public void drawIfNeeded(Graphics2D g2, MapView mv) {
1366 if (!snapOn || !active)
1367 return;
1368 Point p1=mv.getPoint(currentBaseNode);
1369 Point p2=mv.getPoint(dir2);
1370 Point p3=mv.getPoint(projected);
1371 GeneralPath b;
1372 if (drawConstructionGeometry) {
1373 g2.setColor(snapHelperColor);
1374 g2.setStroke(helperStroke);
1375
1376 b = new GeneralPath();
1377 if (absoluteFix) {
1378 b.moveTo(p2.x,p2.y);
1379 b.lineTo(2*p1.x-p2.x,2*p1.y-p2.y); // bi-directional line
1380 } else {
1381 b.moveTo(p2.x,p2.y);
1382 b.lineTo(p3.x,p3.y);
1383 }
1384 g2.draw(b);
1385 }
1386 if (projectionSource != null) {
1387 g2.setColor(snapHelperColor);
1388 g2.setStroke(helperStroke);
1389 b = new GeneralPath();
1390 b.moveTo(p3.x,p3.y);
1391 Point pp=mv.getPoint(projectionSource);
1392 b.lineTo(pp.x,pp.y);
1393 g2.draw(b);
1394 }
1395
1396 if (customBaseHeading >= 0) {
1397 g2.setColor(highlightColor);
1398 g2.setStroke(highlightStroke);
1399 b = new GeneralPath();
1400 Point pp1=mv.getPoint(segmentPoint1);
1401 Point pp2=mv.getPoint(segmentPoint2);
1402 b.moveTo(pp1.x,pp1.y);
1403 b.lineTo(pp2.x,pp2.y);
1404 g2.draw(b);
1405 }
1406
1407 g2.setColor(rubberLineColor);
1408 g2.setStroke(normalStroke);
1409 b = new GeneralPath();
1410 b.moveTo(p1.x,p1.y);
1411 b.lineTo(p3.x,p3.y);
1412 g2.draw(b);
1413
1414 g2.drawString(labelText, p3.x-5, p3.y+20);
1415 if (showProjectedPoint) {
1416 g2.setStroke(normalStroke);
1417 g2.drawOval(p3.x-5, p3.y-5, 10, 10); // projected point
1418 }
1419
1420 g2.setColor(snapHelperColor);
1421 g2.setStroke(helperStroke);
1422 }
1423
1424 /* If mouse position is close to line at 15-30-45-... angle, remembers this direction
1425 */
1426 public void checkAngleSnapping(EastNorth currentEN, double baseHeading, double curHeading) {
1427 EastNorth p0 = currentBaseNode.getEastNorth();
1428 EastNorth snapPoint = currentEN;
1429 double angle = -1;
1430
1431 double activeBaseHeading = (customBaseHeading>=0)? customBaseHeading : baseHeading;
1432
1433 if (snapOn && (activeBaseHeading>=0)) {
1434 angle = curHeading - activeBaseHeading;
1435 if (angle < 0) {
1436 angle+=360;
1437 }
1438 if (angle > 360) {
1439 angle=0;
1440 }
1441
1442 double nearestAngle;
1443 if (fixed) {
1444 nearestAngle = lastAngle; // if direction is fixed use previous angle
1445 active = true;
1446 } else {
1447 nearestAngle = getNearestAngle(angle);
1448 if (getAngleDelta(nearestAngle, angle) < snapAngleTolerance) {
1449 active = (customBaseHeading>=0)? true : Math.abs(nearestAngle - 180) > 1e-3;
1450 // if angle is to previous segment, exclude 180 degrees
1451 lastAngle = nearestAngle;
1452 } else {
1453 active=false;
1454 }
1455 }
1456
1457 if (active) {
1458 double phi;
1459 e0 = p0.east();
1460 n0 = p0.north();
1461 buildLabelText((nearestAngle<=180) ? nearestAngle : nearestAngle-360);
1462
1463 phi = (nearestAngle + activeBaseHeading) * Math.PI / 180;
1464 // (pe,pn) - direction of snapping line
1465 pe = Math.sin(phi);
1466 pn = Math.cos(phi);
1467 double scale = 20 * Main.map.mapView.getDist100Pixel();
1468 dir2 = new EastNorth(e0 + scale * pe, n0 + scale * pn);
1469 snapPoint = getSnapPoint(currentEN);
1470 } else {
1471 noSnapNow();
1472 }
1473 }
1474
1475 // find out the distance, in metres, between the base point and projected point
1476 LatLon mouseLatLon = Main.map.mapView.getProjection().eastNorth2latlon(snapPoint);
1477 double distance = currentBaseNode.getCoor().greatCircleDistance(mouseLatLon);
1478 double hdg = Math.toDegrees(p0.heading(snapPoint));
1479 // heading of segment from current to calculated point, not to mouse position
1480
1481 if (baseHeading >=0 ) { // there is previous line segment with some heading
1482 angle = hdg - baseHeading;
1483 if (angle < 0) {
1484 angle+=360;
1485 }
1486 if (angle > 360) {
1487 angle=0;
1488 }
1489 }
1490 showStatusInfo(angle, hdg, distance, isSnapOn());
1491 }
1492
1493 private void buildLabelText(double nearestAngle) {
1494 if (showAngle) {
1495 if (fixed) {
1496 if (absoluteFix) {
1497 labelText = "=";
1498 } else {
1499 labelText = String.format(fixFmt, (int) nearestAngle);
1500 }
1501 } else {
1502 labelText = String.format("%d", (int) nearestAngle);
1503 }
1504 } else {
1505 if (fixed) {
1506 if (absoluteFix) {
1507 labelText = "=";
1508 } else {
1509 labelText = String.format(tr("FIX"), 0);
1510 }
1511 } else {
1512 labelText = "";
1513 }
1514 }
1515 }
1516
1517 public EastNorth getSnapPoint(EastNorth p) {
1518 if (!active)
1519 return p;
1520 double de=p.east()-e0;
1521 double dn=p.north()-n0;
1522 double l = de*pe+dn*pn;
1523 double delta = Main.map.mapView.getDist100Pixel()/20;
1524 if (!absoluteFix && l<delta) {
1525 active=false;
1526 return p;
1527 } // do not go backward!
1528
1529 projectionSource=null;
1530 if (snapToProjections) {
1531 DataSet ds = getCurrentDataSet();
1532 Collection<Way> selectedWays = ds.getSelectedWays();
1533 if (selectedWays.size()==1) {
1534 Way w = selectedWays.iterator().next();
1535 Collection <EastNorth> pointsToProject = new ArrayList<EastNorth>();
1536 if (w.getNodesCount()<1000) {
1537 for (Node n: w.getNodes()) {
1538 pointsToProject.add(n.getEastNorth());
1539 }
1540 }
1541 if (customBaseHeading >=0 ) {
1542 pointsToProject.add(segmentPoint1);
1543 pointsToProject.add(segmentPoint2);
1544 }
1545 EastNorth enOpt=null;
1546 double dOpt=1e5;
1547 for (EastNorth en: pointsToProject) { // searching for besht projection
1548 double l1 = (en.east()-e0)*pe+(en.north()-n0)*pn;
1549 double d1 = Math.abs(l1-l);
1550 if (d1 < delta && d1 < dOpt) {
1551 l=l1;
1552 enOpt = en;
1553 dOpt = d1;
1554 }
1555 }
1556 if (enOpt!=null) {
1557 projectionSource = enOpt;
1558 }
1559 }
1560 }
1561 return projected = new EastNorth(e0+l*pe, n0+l*pn);
1562 }
1563
1564
1565 public void noSnapNow() {
1566 active=false;
1567 dir2=null; projected=null;
1568 labelText=null;
1569 }
1570
1571 public void setBaseSegment(WaySegment seg) {
1572 if (seg==null) return;
1573 segmentPoint1=seg.getFirstNode().getEastNorth();
1574 segmentPoint2=seg.getSecondNode().getEastNorth();
1575
1576 double hdg = segmentPoint1.heading(segmentPoint2);
1577 hdg=Math.toDegrees(hdg);
1578 if (hdg<0) {
1579 hdg+=360;
1580 }
1581 if (hdg>360) {
1582 hdg-=360;
1583 }
1584 //fixed=true;
1585 //absoluteFix=true;
1586 customBaseHeading=hdg;
1587 }
1588
1589 private void nextSnapMode() {
1590 if (snapOn) {
1591 // turn off snapping if we are in fixed mode or no actile snapping line exist
1592 if (fixed || !active) { snapOn=false; unsetFixedMode(); } else {
1593 setFixedMode();
1594 }
1595 } else {
1596 snapOn=true;
1597 unsetFixedMode();
1598 }
1599 checkBox.setState(snapOn);
1600 customBaseHeading=-1;
1601 }
1602
1603 private void enableSnapping() {
1604 snapOn = true;
1605 checkBox.setState(snapOn);
1606 customBaseHeading=-1;
1607 unsetFixedMode();
1608 }
1609
1610 private void toggleSnapping() {
1611 snapOn = !snapOn;
1612 checkBox.setState(snapOn);
1613 customBaseHeading=-1;
1614 unsetFixedMode();
1615 }
1616
1617 public void setFixedMode() {
1618 if (active) {
1619 fixed=true;
1620 }
1621 }
1622
1623
1624 public void unsetFixedMode() {
1625 fixed=false;
1626 absoluteFix=false;
1627 lastAngle=0;
1628 active=false;
1629 }
1630
1631 public boolean isActive() {
1632 return active;
1633 }
1634
1635 public boolean isSnapOn() {
1636 return snapOn;
1637 }
1638
1639 private double getNearestAngle(double angle) {
1640 double delta,minDelta=1e5, bestAngle=0.0;
1641 for (int i=0; i < snapAngles.length; i++) {
1642 delta = getAngleDelta(angle,snapAngles[i]);
1643 if (delta < minDelta) {
1644 minDelta=delta;
1645 bestAngle=snapAngles[i];
1646 }
1647 }
1648 if (Math.abs(bestAngle-360) < 1e-3) {
1649 bestAngle=0;
1650 }
1651 return bestAngle;
1652 }
1653
1654 private double getAngleDelta(double a, double b) {
1655 double delta = Math.abs(a-b);
1656 if (delta>180)
1657 return 360-delta;
1658 else
1659 return delta;
1660 }
1661
1662 private void unFixOrTurnOff() {
1663 if (absoluteFix) {
1664 unsetFixedMode();
1665 } else {
1666 toggleSnapping();
1667 }
1668 }
1669
1670 MouseListener anglePopupListener = new PopupMenuLauncher( new JPopupMenu() {
1671 JCheckBoxMenuItem repeatedCb = new JCheckBoxMenuItem(new AbstractAction(tr("Toggle snapping by {0}", getShortcut().getKeyText())){
1672 public void actionPerformed(ActionEvent e) {
1673 boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1674 Main.pref.put("draw.anglesnap.toggleOnRepeatedA", sel);
1675 init();
1676 }
1677 });
1678 JCheckBoxMenuItem helperCb = new JCheckBoxMenuItem(new AbstractAction(tr("Show helper geometry")){
1679 public void actionPerformed(ActionEvent e) {
1680 boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1681 Main.pref.put("draw.anglesnap.drawConstructionGeometry", sel);
1682 Main.pref.put("draw.anglesnap.drawProjectedPoint", sel);
1683 Main.pref.put("draw.anglesnap.showAngle", sel);
1684 init();
1685 enableSnapping();
1686 }
1687 });
1688 JCheckBoxMenuItem projectionCb = new JCheckBoxMenuItem(new AbstractAction(tr("Snap to node projections")){
1689 public void actionPerformed(ActionEvent e) {
1690 boolean sel=((JCheckBoxMenuItem) e.getSource()).getState();
1691 Main.pref.put("draw.anglesnap.projectionsnap", sel);
1692 init();
1693 enableSnapping();
1694 }
1695 });
1696 {
1697 helperCb.setState(Main.pref.getBoolean("draw.anglesnap.drawConstructionGeometry",true));
1698 projectionCb.setState(Main.pref.getBoolean("draw.anglesnap.projectionsnapgvff",true));
1699 repeatedCb.setState(Main.pref.getBoolean("draw.anglesnap.toggleOnRepeatedA",true));
1700 add(repeatedCb);
1701 add(helperCb);
1702 add(projectionCb);;
1703 add(new AbstractAction(tr("Disable")) {
1704 public void actionPerformed(ActionEvent e) {
1705 saveAngles("180");
1706 init();
1707 enableSnapping();
1708 }
1709 });
1710 add(new AbstractAction(tr("0,90,...")) {
1711 public void actionPerformed(ActionEvent e) {
1712 saveAngles("0","90","180");
1713 init();
1714 enableSnapping();
1715 }
1716 });
1717 add(new AbstractAction(tr("0,45,90,...")) {
1718 public void actionPerformed(ActionEvent e) {
1719 saveAngles("0","45","90","135","180");
1720 init();
1721 enableSnapping();
1722 }
1723 });
1724 add(new AbstractAction(tr("0,30,45,60,90,...")) {
1725 public void actionPerformed(ActionEvent e) {
1726 saveAngles("0","30","45","60","90","120","135","150","180");
1727 init();
1728 enableSnapping();
1729 }
1730 });
1731 }
1732 }) {
1733 @Override
1734 public void mouseClicked(MouseEvent e) {
1735 super.mouseClicked(e);
1736 if (e.getButton() == MouseEvent.BUTTON1) {
1737 toggleSnapping();
1738 updateStatusLine();
1739 }
1740 }
1741 };
1742 }
1743
1744 private class SnapChangeAction extends JosmAction {
1745 public SnapChangeAction() {
1746 super(tr("Angle snapping"), "anglesnap",
1747 tr("Switch angle snapping mode while drawing"), null, false);
1748 putValue("help", ht("/Action/Draw/AngleSnap"));
1749 }
1750
1751 @Override
1752 public void actionPerformed(ActionEvent e) {
1753 if (snapHelper!=null) {
1754 snapHelper.toggleSnapping();
1755 }
1756 }
1757 }
1758}
Note: See TracBrowser for help on using the repository browser.