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

Last change on this file since 7217 was 7217, checked in by akks, 10 years ago

see #10104: refactor key press/release detection introducing Main.map.keyDetector

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