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

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

see #13309 - fix most of deprecation warnings

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