001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.mapmode; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007import static org.openstreetmap.josm.tools.I18n.trn; 008 009import java.awt.BasicStroke; 010import java.awt.Color; 011import java.awt.Cursor; 012import java.awt.Graphics2D; 013import java.awt.Point; 014import java.awt.event.ActionEvent; 015import java.awt.event.KeyEvent; 016import java.awt.event.MouseEvent; 017import java.util.ArrayList; 018import java.util.Collection; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.Iterator; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027 028import javax.swing.AbstractAction; 029import javax.swing.JCheckBoxMenuItem; 030import javax.swing.JOptionPane; 031import javax.swing.SwingUtilities; 032 033import org.openstreetmap.josm.actions.JosmAction; 034import org.openstreetmap.josm.command.AddCommand; 035import org.openstreetmap.josm.command.ChangeNodesCommand; 036import org.openstreetmap.josm.command.Command; 037import org.openstreetmap.josm.command.SequenceCommand; 038import org.openstreetmap.josm.data.Bounds; 039import org.openstreetmap.josm.data.UndoRedoHandler; 040import org.openstreetmap.josm.data.coor.EastNorth; 041import org.openstreetmap.josm.data.osm.DataSelectionListener; 042import org.openstreetmap.josm.data.osm.DataSet; 043import org.openstreetmap.josm.data.osm.Node; 044import org.openstreetmap.josm.data.osm.OsmPrimitive; 045import org.openstreetmap.josm.data.osm.Way; 046import org.openstreetmap.josm.data.osm.WaySegment; 047import org.openstreetmap.josm.data.osm.event.SelectionEventManager; 048import org.openstreetmap.josm.data.osm.visitor.paint.ArrowPaintHelper; 049import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 050import org.openstreetmap.josm.data.preferences.AbstractProperty; 051import org.openstreetmap.josm.data.preferences.BooleanProperty; 052import org.openstreetmap.josm.data.preferences.CachingProperty; 053import org.openstreetmap.josm.data.preferences.DoubleProperty; 054import org.openstreetmap.josm.data.preferences.NamedColorProperty; 055import org.openstreetmap.josm.data.preferences.StrokeProperty; 056import org.openstreetmap.josm.gui.MainApplication; 057import org.openstreetmap.josm.gui.MainMenu; 058import org.openstreetmap.josm.gui.MapFrame; 059import org.openstreetmap.josm.gui.MapView; 060import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 061import org.openstreetmap.josm.gui.NavigatableComponent; 062import org.openstreetmap.josm.gui.draw.MapPath2D; 063import org.openstreetmap.josm.gui.layer.Layer; 064import org.openstreetmap.josm.gui.layer.MapViewPaintable; 065import org.openstreetmap.josm.gui.layer.OsmDataLayer; 066import org.openstreetmap.josm.gui.util.KeyPressReleaseListener; 067import org.openstreetmap.josm.gui.util.ModifierExListener; 068import org.openstreetmap.josm.tools.Geometry; 069import org.openstreetmap.josm.tools.ImageProvider; 070import org.openstreetmap.josm.tools.Pair; 071import org.openstreetmap.josm.tools.Shortcut; 072import org.openstreetmap.josm.tools.Utils; 073 074/** 075 * Mapmode to add nodes, create and extend ways. 076 */ 077public class DrawAction extends MapMode implements MapViewPaintable, DataSelectionListener, KeyPressReleaseListener, ModifierExListener { 078 079 /** 080 * If this property is set, the draw action moves the viewport when adding new points. 081 * @since 12182 082 */ 083 public static final CachingProperty<Boolean> VIEWPORT_FOLLOWING = new BooleanProperty("draw.viewport.following", false).cached(); 084 085 private static final Color ORANGE_TRANSPARENT = new Color(Color.ORANGE.getRed(), Color.ORANGE.getGreen(), Color.ORANGE.getBlue(), 128); 086 087 private static final ArrowPaintHelper START_WAY_INDICATOR = new ArrowPaintHelper(Utils.toRadians(90), 8); 088 089 static final CachingProperty<Boolean> USE_REPEATED_SHORTCUT 090 = new BooleanProperty("draw.anglesnap.toggleOnRepeatedA", true).cached(); 091 static final CachingProperty<BasicStroke> RUBBER_LINE_STROKE 092 = new StrokeProperty("draw.stroke.helper-line", "3").cached(); 093 094 static final CachingProperty<BasicStroke> HIGHLIGHT_STROKE 095 = new StrokeProperty("draw.anglesnap.stroke.highlight", "10").cached(); 096 static final CachingProperty<BasicStroke> HELPER_STROKE 097 = new StrokeProperty("draw.anglesnap.stroke.helper", "1 4").cached(); 098 099 static final CachingProperty<Double> SNAP_ANGLE_TOLERANCE 100 = new DoubleProperty("draw.anglesnap.tolerance", 5.0).cached(); 101 static final CachingProperty<Boolean> DRAW_CONSTRUCTION_GEOMETRY 102 = new BooleanProperty("draw.anglesnap.drawConstructionGeometry", true).cached(); 103 static final CachingProperty<Boolean> SHOW_PROJECTED_POINT 104 = new BooleanProperty("draw.anglesnap.drawProjectedPoint", true).cached(); 105 static final CachingProperty<Boolean> SNAP_TO_PROJECTIONS 106 = new BooleanProperty("draw.anglesnap.projectionsnap", true).cached(); 107 108 static final CachingProperty<Boolean> SHOW_ANGLE 109 = new BooleanProperty("draw.anglesnap.showAngle", true).cached(); 110 111 static final CachingProperty<Color> SNAP_HELPER_COLOR 112 = new NamedColorProperty(marktr("draw angle snap"), Color.ORANGE).cached(); 113 114 static final CachingProperty<Color> HIGHLIGHT_COLOR 115 = new NamedColorProperty(marktr("draw angle snap highlight"), ORANGE_TRANSPARENT).cached(); 116 117 static final AbstractProperty<Color> RUBBER_LINE_COLOR 118 = PaintColors.SELECTED.getProperty().getChildColor(marktr("helper line")); 119 120 static final CachingProperty<Boolean> DRAW_HELPER_LINE 121 = new BooleanProperty("draw.helper-line", true).cached(); 122 static final CachingProperty<Boolean> DRAW_TARGET_HIGHLIGHT 123 = new BooleanProperty("draw.target-highlight", true).cached(); 124 static final CachingProperty<Double> SNAP_TO_INTERSECTION_THRESHOLD 125 = new DoubleProperty("edit.snap-intersection-threshold", 10).cached(); 126 127 private final Cursor cursorJoinNode; 128 private final Cursor cursorJoinWay; 129 130 private transient Node lastUsedNode; 131 private double toleranceMultiplier; 132 133 private transient Node mouseOnExistingNode; 134 private transient Set<Way> mouseOnExistingWays = new HashSet<>(); 135 // old highlights store which primitives are currently highlighted. This 136 // is true, even if target highlighting is disabled since the status bar 137 // derives its information from this list as well. 138 private transient Set<OsmPrimitive> oldHighlights = new HashSet<>(); 139 // new highlights contains a list of primitives that should be highlighted 140 // but haven't been so far. The idea is to compare old and new and only 141 // repaint if there are changes. 142 private transient Set<OsmPrimitive> newHighlights = new HashSet<>(); 143 private boolean wayIsFinished; 144 private Point mousePos; 145 private Point oldMousePos; 146 147 private transient Node currentBaseNode; 148 private transient Node previousNode; 149 private EastNorth currentMouseEastNorth; 150 151 private final transient DrawSnapHelper snapHelper = new DrawSnapHelper(this); 152 153 private final transient Shortcut backspaceShortcut; 154 private final BackSpaceAction backspaceAction; 155 private final transient Shortcut snappingShortcut; 156 private boolean ignoreNextKeyRelease; 157 158 private final SnapChangeAction snapChangeAction; 159 private final JCheckBoxMenuItem snapCheckboxMenuItem; 160 private static final BasicStroke BASIC_STROKE = new BasicStroke(1); 161 162 private Point rightClickPressPos; 163 164 /** 165 * Constructs a new {@code DrawAction}. 166 * @since 11713 167 */ 168 public DrawAction() { 169 super(tr("Draw"), "node/autonode", tr("Draw nodes"), 170 Shortcut.registerShortcut("mapmode:draw", tr("Mode: {0}", tr("Draw")), KeyEvent.VK_A, Shortcut.DIRECT), 171 ImageProvider.getCursor("crosshair", null)); 172 173 snappingShortcut = Shortcut.registerShortcut("mapmode:drawanglesnapping", 174 tr("Edit: {0}", tr("Draw Angle snapping")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE); 175 snapChangeAction = new SnapChangeAction(); 176 snapCheckboxMenuItem = addMenuItem(); 177 snapHelper.setMenuCheckBox(snapCheckboxMenuItem); 178 backspaceShortcut = Shortcut.registerShortcut("mapmode:backspace", 179 tr("Backspace in Add mode"), KeyEvent.VK_BACK_SPACE, Shortcut.DIRECT); 180 backspaceAction = new BackSpaceAction(); 181 cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode"); 182 cursorJoinWay = ImageProvider.getCursor("crosshair", "joinway"); 183 184 snapHelper.init(); 185 } 186 187 private JCheckBoxMenuItem addMenuItem() { 188 int n = MainApplication.getMenu().editMenu.getItemCount(); 189 return MainMenu.addWithCheckbox(MainApplication.getMenu().editMenu, snapChangeAction, n >= 5 ? n-5 : -1, false); 190 } 191 192 /** 193 * Checks if a map redraw is required and does so if needed. Also updates the status bar. 194 * @param e event, can be null 195 * @return true if a repaint is needed 196 */ 197 private boolean redrawIfRequired(Object e) { 198 updateStatusLine(); 199 // repaint required if the helper line is active. 200 boolean needsRepaint = DRAW_HELPER_LINE.get() && !wayIsFinished; 201 if (DRAW_TARGET_HIGHLIGHT.get()) { 202 // move newHighlights to oldHighlights; only update changed primitives 203 for (OsmPrimitive x : newHighlights) { 204 if (oldHighlights.contains(x)) { 205 continue; 206 } 207 x.setHighlighted(true); 208 needsRepaint = true; 209 } 210 oldHighlights.removeAll(newHighlights); 211 for (OsmPrimitive x : oldHighlights) { 212 x.setHighlighted(false); 213 needsRepaint = true; 214 } 215 } 216 // required in order to print correct help text 217 oldHighlights = newHighlights; 218 219 if (!needsRepaint && !DRAW_TARGET_HIGHLIGHT.get()) 220 return false; 221 222 // update selection to reflect which way being modified 223 OsmDataLayer editLayer = getLayerManager().getEditLayer(); 224 Node baseNode = getCurrentBaseNode(); 225 if (editLayer != null && baseNode != null && !editLayer.data.selectionEmpty()) { 226 DataSet currentDataSet = editLayer.getDataSet(); 227 Way continueFrom = getWayForNode(baseNode); 228 if (alt && continueFrom != null && (!baseNode.isSelected() || continueFrom.isSelected())) { 229 addRemoveSelection(currentDataSet, baseNode, continueFrom); 230 needsRepaint = true; 231 } else if (!alt && continueFrom != null && !continueFrom.isSelected()) { 232 addSelection(currentDataSet, continueFrom); 233 needsRepaint = true; 234 } 235 } 236 237 if (!needsRepaint && e instanceof SelectionChangeEvent) { 238 SelectionChangeEvent event = (SelectionChangeEvent) e; 239 needsRepaint = !event.getOldSelection().isEmpty() && event.getSelection().isEmpty(); 240 } 241 242 if (needsRepaint && editLayer != null) { 243 editLayer.invalidate(); 244 } 245 return needsRepaint; 246 } 247 248 private static void addRemoveSelection(DataSet ds, OsmPrimitive toAdd, OsmPrimitive toRemove) { 249 ds.update(() -> { // to prevent the selection listener to screw around with the state 250 addSelection(ds, toAdd); 251 clearSelection(ds, toRemove); 252 }); 253 } 254 255 private static void updatePreservedFlag(OsmPrimitive osm, boolean state) { 256 // Preserves selected primitives and selected way nodes 257 osm.setPreserved(state); 258 if (osm instanceof Way) { 259 for (Node n : ((Way) osm).getNodes()) { 260 n.setPreserved(state); 261 } 262 } 263 } 264 265 private static void setSelection(DataSet ds, Collection<OsmPrimitive> toSet) { 266 toSet.forEach(x -> updatePreservedFlag(x, true)); 267 ds.setSelected(toSet); 268 } 269 270 private static void setSelection(DataSet ds, OsmPrimitive toSet) { 271 updatePreservedFlag(toSet, true); 272 ds.setSelected(toSet); 273 } 274 275 private static void addSelection(DataSet ds, OsmPrimitive toAdd) { 276 updatePreservedFlag(toAdd, true); 277 ds.addSelected(toAdd); 278 } 279 280 private static void clearSelection(DataSet ds, OsmPrimitive toRemove) { 281 ds.clearSelection(toRemove); 282 updatePreservedFlag(toRemove, false); 283 } 284 285 @Override 286 public void enterMode() { 287 if (!isEnabled()) 288 return; 289 super.enterMode(); 290 readPreferences(); 291 292 // determine if selection is suitable to continue drawing. If it 293 // isn't, set wayIsFinished to true to avoid superfluous repaints. 294 determineCurrentBaseNodeAndPreviousNode(getLayerManager().getEditDataSet().getSelected()); 295 wayIsFinished = getCurrentBaseNode() == null; 296 297 toleranceMultiplier = 0.01 * NavigatableComponent.PROP_SNAP_DISTANCE.get(); 298 299 snapHelper.init(); 300 snapCheckboxMenuItem.getAction().setEnabled(true); 301 302 MapFrame map = MainApplication.getMap(); 303 map.statusLine.getAnglePanel().addMouseListener(snapHelper.anglePopupListener); 304 MainApplication.registerActionShortcut(backspaceAction, backspaceShortcut); 305 306 map.mapView.addMouseListener(this); 307 map.mapView.addMouseMotionListener(this); 308 map.mapView.addTemporaryLayer(this); 309 SelectionEventManager.getInstance().addSelectionListenerForEdt(this); 310 311 map.keyDetector.addKeyListener(this); 312 map.keyDetector.addModifierExListener(this); 313 ignoreNextKeyRelease = true; 314 } 315 316 @Override 317 public void exitMode() { 318 super.exitMode(); 319 320 MapFrame map = MainApplication.getMap(); 321 map.mapView.removeMouseListener(this); 322 map.mapView.removeMouseMotionListener(this); 323 map.mapView.removeTemporaryLayer(this); 324 SelectionEventManager.getInstance().removeSelectionListener(this); 325 MainApplication.unregisterActionShortcut(backspaceAction, backspaceShortcut); 326 snapHelper.unsetFixedMode(); 327 snapCheckboxMenuItem.getAction().setEnabled(false); 328 329 map.statusLine.getAnglePanel().removeMouseListener(snapHelper.anglePopupListener); 330 map.statusLine.activateAnglePanel(false); 331 332 DataSet ds = getLayerManager().getEditDataSet(); 333 if (ds != null) { 334 ds.getSelected().forEach(x -> updatePreservedFlag(x, false)); 335 map.statusLine.setDist(ds.getSelectedWays()); 336 } 337 338 removeHighlighting(null); 339 map.keyDetector.removeKeyListener(this); 340 map.keyDetector.removeModifierExListener(this); 341 lastUsedNode = null; 342 } 343 344 /** 345 * redraw to (possibly) get rid of helper line if selection changes. 346 */ 347 @Override 348 public void modifiersExChanged(int modifiers) { 349 if (!MainApplication.isDisplayingMapView() || !MainApplication.getMap().mapView.isActiveLayerDrawable()) 350 return; 351 updateKeyModifiersEx(modifiers); 352 computeHelperLine(); 353 addHighlighting(null); 354 } 355 356 @Override 357 public void doKeyPressed(KeyEvent e) { 358 if (!snappingShortcut.isEvent(e) && !(USE_REPEATED_SHORTCUT.get() && getShortcut().isEvent(e))) 359 return; 360 snapHelper.setFixedMode(); 361 computeHelperLine(); 362 redrawIfRequired(e); 363 } 364 365 @Override 366 public void doKeyReleased(KeyEvent e) { 367 if (!snappingShortcut.isEvent(e) && !(USE_REPEATED_SHORTCUT.get() && getShortcut().isEvent(e))) 368 return; 369 if (ignoreNextKeyRelease) { 370 ignoreNextKeyRelease = false; 371 return; 372 } 373 snapHelper.unFixOrTurnOff(); 374 computeHelperLine(); 375 redrawIfRequired(e); 376 } 377 378 /** 379 * redraw to (possibly) get rid of helper line if selection changes. 380 */ 381 @Override 382 public void selectionChanged(SelectionChangeEvent event) { 383 if (!MainApplication.getMap().mapView.isActiveLayerDrawable()) 384 return; 385 if (event.getSelection().isEmpty()) 386 finishDrawing(); 387 // Make sure helper line is computed later (causes deadlock in selection event chain otherwise) 388 SwingUtilities.invokeLater(() -> { 389 event.getOldSelection().forEach(x -> updatePreservedFlag(x, false)); 390 event.getSelection().forEach(x -> updatePreservedFlag(x, true)); 391 if (MainApplication.getMap() != null) { 392 computeHelperLine(); 393 addHighlighting(event); 394 } 395 }); 396 } 397 398 private void tryAgain(MouseEvent e) { 399 getLayerManager().getEditDataSet().clearSelection(); 400 mouseReleased(e); 401 } 402 403 /** 404 * This function should be called when the user wishes to finish his current draw action. 405 * If Potlatch Style is enabled, it will switch to select tool, otherwise simply disable 406 * the helper line until the user chooses to draw something else. 407 */ 408 private void finishDrawing() { 409 lastUsedNode = null; 410 wayIsFinished = true; 411 MainApplication.getMap().selectSelectTool(true); 412 snapHelper.noSnapNow(); 413 414 // Redraw to remove the helper line stub 415 computeHelperLine(); 416 removeHighlighting(null); 417 } 418 419 @Override 420 public void mousePressed(MouseEvent e) { 421 if (e.getButton() == MouseEvent.BUTTON3) { 422 rightClickPressPos = e.getPoint(); 423 } 424 } 425 426 /** 427 * If user clicked with the left button, add a node at the current mouse 428 * position. 429 * 430 * If in nodeway mode, insert the node into the way. 431 */ 432 @Override 433 public void mouseReleased(MouseEvent e) { 434 if (e.getButton() == MouseEvent.BUTTON3) { 435 Point curMousePos = e.getPoint(); 436 if (curMousePos.equals(rightClickPressPos)) { 437 tryToSetBaseSegmentForAngleSnap(); 438 } 439 return; 440 } 441 if (e.getButton() != MouseEvent.BUTTON1) 442 return; 443 MapView mapView = MainApplication.getMap().mapView; 444 if (!mapView.isActiveLayerDrawable()) 445 return; 446 // request focus in order to enable the expected keyboard shortcuts 447 // 448 mapView.requestFocus(); 449 450 if (e.getClickCount() > 1 && mousePos != null && mousePos.equals(oldMousePos)) { 451 // A double click equals "user clicked last node again, finish way" 452 // Change draw tool only if mouse position is nearly the same, as 453 // otherwise fast clicks will count as a double click 454 finishDrawing(); 455 return; 456 } 457 oldMousePos = mousePos; 458 459 // we copy ctrl/alt/shift from the event just in case our global 460 // keyDetector didn't make it through the security manager. Unclear 461 // if that can ever happen but better be safe. 462 updateKeyModifiers(e); 463 mousePos = e.getPoint(); 464 465 DataSet ds = getLayerManager().getEditDataSet(); 466 Collection<OsmPrimitive> selection = new ArrayList<>(ds.getSelected()); 467 468 boolean newNode = false; 469 Node n = mapView.getNearestNode(mousePos, OsmPrimitive::isSelectable); 470 if (ctrl) { 471 Iterator<Way> it = ds.getSelectedWays().iterator(); 472 if (it.hasNext()) { 473 // ctrl-click on node of selected way = reuse node despite of ctrl 474 if (!it.next().containsNode(n)) n = null; 475 } else { 476 n = null; // ctrl-click + no selected way = new node 477 } 478 } 479 480 if (n != null && !snapHelper.isActive()) { 481 // user clicked on node 482 if (selection.isEmpty() || wayIsFinished) { 483 // select the clicked node and do nothing else 484 // (this is just a convenience option so that people don't 485 // have to switch modes) 486 487 setSelection(ds, n); 488 // If we extend/continue an existing way, select it already now to make it obvious 489 Way continueFrom = getWayForNode(n); 490 if (continueFrom != null) { 491 addSelection(ds, continueFrom); 492 } 493 494 // The user explicitly selected a node, so let him continue drawing 495 wayIsFinished = false; 496 return; 497 } 498 } else { 499 EastNorth newEN; 500 if (n != null) { 501 EastNorth foundPoint = n.getEastNorth(); 502 // project found node to snapping line 503 newEN = snapHelper.getSnapPoint(foundPoint); 504 // do not add new node if there is some node within snapping distance 505 double tolerance = mapView.getDist100Pixel() * toleranceMultiplier; 506 if (foundPoint.distance(newEN) > tolerance) { 507 n = new Node(newEN); // point != projected, so we create new node 508 newNode = true; 509 } 510 } else { // n==null, no node found in clicked area 511 EastNorth mouseEN = mapView.getEastNorth(e.getX(), e.getY()); 512 newEN = snapHelper.isSnapOn() ? snapHelper.getSnapPoint(mouseEN) : mouseEN; 513 n = new Node(newEN); //create node at clicked point 514 newNode = true; 515 } 516 snapHelper.unsetFixedMode(); 517 } 518 519 Collection<Command> cmds = new LinkedList<>(); 520 Collection<OsmPrimitive> newSelection = new LinkedList<>(ds.getSelected()); 521 Map<Way, List<Node>> reuseWays = new HashMap<>(); 522 523 if (newNode) { 524 if (n.isOutSideWorld()) { 525 JOptionPane.showMessageDialog( 526 MainApplication.getMainFrame(), 527 tr("Cannot add a node outside of the world."), 528 tr("Warning"), 529 JOptionPane.WARNING_MESSAGE 530 ); 531 return; 532 } 533 cmds.add(new AddCommand(ds, n)); 534 535 if (!ctrl) { 536 // Insert the node into all the nearby way segments 537 List<WaySegment> wss = mapView.getNearestWaySegments( 538 mapView.getPoint(n), OsmPrimitive::isSelectable); 539 if (snapHelper.isActive()) { 540 tryToMoveNodeOnIntersection(wss, n); 541 } 542 insertNodeIntoAllNearbySegments(wss, n, newSelection, cmds, reuseWays); 543 } 544 } 545 // now "n" is newly created or reused node that shoud be added to some way 546 547 // This part decides whether or not a "segment" (i.e. a connection) is made to an existing node. 548 549 // For a connection to be made, the user must either have a node selected (connection 550 // is made to that node), or he must have a way selected *and* one of the endpoints 551 // of that way must be the last used node (connection is made to last used node), or 552 // he must have a way and a node selected (connection is made to the selected node). 553 554 // If the above does not apply, the selection is cleared and a new try is started 555 556 boolean extendedWay = false; 557 boolean wayIsFinishedTemp = wayIsFinished; 558 wayIsFinished = false; 559 560 // don't draw lines if shift is held 561 if (!selection.isEmpty() && !shift) { 562 Node selectedNode = null; 563 Way selectedWay = null; 564 565 for (OsmPrimitive p : selection) { 566 if (p instanceof Node) { 567 if (selectedNode != null) { 568 // Too many nodes selected to do something useful 569 tryAgain(e); 570 return; 571 } 572 selectedNode = (Node) p; 573 } else if (p instanceof Way) { 574 if (selectedWay != null) { 575 // Too many ways selected to do something useful 576 tryAgain(e); 577 return; 578 } 579 selectedWay = (Way) p; 580 } 581 } 582 583 // the node from which we make a connection 584 Node n0 = findNodeToContinueFrom(selectedNode, selectedWay); 585 // We have a selection but it isn't suitable. Try again. 586 if (n0 == null) { 587 tryAgain(e); 588 return; 589 } 590 if (!wayIsFinishedTemp) { 591 if (isSelfContainedWay(selectedWay, n0, n)) 592 return; 593 594 // User clicked last node again, finish way 595 if (n0 == n) { 596 finishDrawing(); 597 return; 598 } 599 600 // Ok we know now that we'll insert a line segment, but will it connect to an 601 // existing way or make a new way of its own? The "alt" modifier means that the 602 // user wants a new way. 603 Way way = alt ? null : (selectedWay != null ? selectedWay : getWayForNode(n0)); 604 605 // Don't allow creation of self-overlapping ways 606 if (way != null) { 607 long nodeCount = way.getNodes().stream().filter(p -> p.equals(n0)).count(); 608 if (nodeCount > 1) { 609 way = null; 610 } 611 } 612 613 if (way == null) { 614 way = new Way(); 615 way.addNode(n0); 616 way.addNode(n); 617 cmds.add(new AddCommand(ds, way)); 618 } else { 619 List<Node> modNodes = reuseWays.get(way); 620 boolean reuse = (modNodes != null); 621 if (modNodes == null) { 622 modNodes = way.getNodes(); 623 } 624 // Connected to a node that's already in the way 625 if (modNodes.contains(n)) { 626 wayIsFinished = true; 627 selection.clear(); 628 } 629 if (modNodes.get(modNodes.size() - 1) == n0) 630 modNodes.add(n); 631 else 632 modNodes.add(0, n); 633 if (!reuse) { 634 cmds.add(new ChangeNodesCommand(way, modNodes)); 635 } 636 } 637 638 extendedWay = true; 639 newSelection.clear(); 640 newSelection.add(way); 641 } 642 } 643 if (!extendedWay && !newNode) { 644 return; // We didn't do anything. 645 } 646 647 String title = getTitle(newNode, n, newSelection, reuseWays, extendedWay); 648 649 Command c = new SequenceCommand(title, cmds); 650 651 UndoRedoHandler.getInstance().add(c); 652 if (!wayIsFinished) { 653 lastUsedNode = n; 654 } 655 656 setSelection(ds, newSelection); 657 658 // "viewport following" mode for tracing long features 659 // from aerial imagery or GPS tracks. 660 if (VIEWPORT_FOLLOWING.get()) { 661 mapView.smoothScrollTo(n.getEastNorth()); 662 } 663 computeHelperLine(); 664 removeHighlighting(e); 665 } 666 667 private static String getTitle(boolean newNode, Node n, Collection<OsmPrimitive> newSelection, Map<Way, List<Node>> reuseWays, 668 boolean extendedWay) { 669 String title; 670 if (!extendedWay) { 671 if (reuseWays.isEmpty()) { 672 title = tr("Add node"); 673 } else { 674 title = tr("Add node into way"); 675 for (Way w : reuseWays.keySet()) { 676 newSelection.remove(w); 677 } 678 } 679 newSelection.clear(); 680 newSelection.add(n); 681 } else if (!newNode) { 682 title = tr("Connect existing way to node"); 683 } else if (reuseWays.isEmpty()) { 684 title = tr("Add a new node to an existing way"); 685 } else { 686 title = tr("Add node into way and connect"); 687 } 688 return title; 689 } 690 691 private void insertNodeIntoAllNearbySegments(List<WaySegment> wss, Node n, Collection<OsmPrimitive> newSelection, 692 Collection<Command> cmds, Map<Way, List<Node>> reuseWays) { 693 Map<Way, List<Integer>> insertPoints = new HashMap<>(); 694 for (WaySegment ws : wss) { 695 List<Integer> is; 696 if (insertPoints.containsKey(ws.getWay())) { 697 is = insertPoints.get(ws.getWay()); 698 } else { 699 is = new ArrayList<>(); 700 insertPoints.put(ws.getWay(), is); 701 } 702 703 is.add(ws.getLowerIndex()); 704 } 705 706 Set<Pair<Node, Node>> segSet = new HashSet<>(); 707 708 for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) { 709 Way w = insertPoint.getKey(); 710 List<Integer> is = insertPoint.getValue(); 711 712 List<Node> modNodes = w.getNodes(); 713 pruneSuccsAndReverse(is); 714 for (int i : is) { 715 segSet.add(Pair.sort(new Pair<>(w.getNode(i), w.getNode(i+1)))); 716 modNodes.add(i + 1, n); 717 } 718 719 // If ALT is pressed, a new way should be created and that new way should get 720 // selected. This works every time unless the ways the nodes get inserted into 721 // are already selected. This is the case when creating a self-overlapping way 722 // but pressing ALT prevents this. Therefore we must de-select the way manually 723 // here so /only/ the new way will be selected after this method finishes. 724 if (alt) { 725 newSelection.add(insertPoint.getKey()); 726 } 727 728 cmds.add(new ChangeNodesCommand(insertPoint.getKey(), modNodes)); 729 reuseWays.put(w, modNodes); 730 } 731 732 adjustNode(segSet, n); 733 } 734 735 /** 736 * Prevent creation of ways that look like this: <----> 737 * This happens if users want to draw a no-exit-sideway from the main way like this: 738 * ^ 739 * |<----> 740 * | 741 * The solution isn't ideal because the main way will end in the side way, which is bad for 742 * navigation software ("drive straight on") but at least easier to fix. Maybe users will fix 743 * it on their own, too. At least it's better than producing an error. 744 * 745 * @param selectedWay the way to check 746 * @param currentNode the current node (i.e. the one the connection will be made from) 747 * @param targetNode the target node (i.e. the one the connection will be made to) 748 * @return {@code true} if this would create a selfcontaining way, {@code false} otherwise. 749 */ 750 private boolean isSelfContainedWay(Way selectedWay, Node currentNode, Node targetNode) { 751 if (selectedWay != null) { 752 int posn0 = selectedWay.getNodes().indexOf(currentNode); 753 // CHECKSTYLE.OFF: SingleSpaceSeparator 754 if ((posn0 != -1 && // n0 is part of way 755 (posn0 >= 1 && targetNode.equals(selectedWay.getNode(posn0-1)))) || // previous node 756 (posn0 < selectedWay.getNodesCount()-1 && targetNode.equals(selectedWay.getNode(posn0+1)))) { // next node 757 setSelection(getLayerManager().getEditDataSet(), targetNode); 758 lastUsedNode = targetNode; 759 return true; 760 } 761 // CHECKSTYLE.ON: SingleSpaceSeparator 762 } 763 764 return false; 765 } 766 767 /** 768 * Finds a node to continue drawing from. Decision is based upon given node and way. 769 * @param selectedNode Currently selected node, may be null 770 * @param selectedWay Currently selected way, may be null 771 * @return Node if a suitable node is found, null otherwise 772 */ 773 private Node findNodeToContinueFrom(Node selectedNode, Way selectedWay) { 774 // No nodes or ways have been selected, this occurs when a relation 775 // has been selected or the selection is empty 776 if (selectedNode == null && selectedWay == null) 777 return null; 778 779 if (selectedNode == null) { 780 if (selectedWay.isFirstLastNode(lastUsedNode)) 781 return lastUsedNode; 782 783 // We have a way selected, but no suitable node to continue from. Start anew. 784 return null; 785 } 786 787 if (selectedWay == null) 788 return selectedNode; 789 790 if (selectedWay.isFirstLastNode(selectedNode)) 791 return selectedNode; 792 793 // We have a way and node selected, but it's not at the start/end of the way. Start anew. 794 return null; 795 } 796 797 @Override 798 public void mouseDragged(MouseEvent e) { 799 mouseMoved(e); 800 } 801 802 @Override 803 public void mouseMoved(MouseEvent e) { 804 if (!MainApplication.getMap().mapView.isActiveLayerDrawable()) 805 return; 806 807 // we copy ctrl/alt/shift from the event just in case our global 808 // keyDetector didn't make it through the security manager. Unclear 809 // if that can ever happen but better be safe. 810 updateKeyModifiers(e); 811 mousePos = e.getPoint(); 812 if (snapHelper.isSnapOn() && ctrl) 813 tryToSetBaseSegmentForAngleSnap(); 814 815 computeHelperLine(); 816 addHighlighting(e); 817 } 818 819 /** 820 * This method is used to detect segment under mouse and use it as reference for angle snapping 821 */ 822 private void tryToSetBaseSegmentForAngleSnap() { 823 if (mousePos != null) { 824 WaySegment seg = MainApplication.getMap().mapView.getNearestWaySegment(mousePos, OsmPrimitive::isSelectable); 825 if (seg != null) { 826 snapHelper.setBaseSegment(seg); 827 } 828 } 829 } 830 831 /** 832 * This method prepares data required for painting the "helper line" from 833 * the last used position to the mouse cursor. It duplicates some code from 834 * mouseReleased() (FIXME). 835 */ 836 private synchronized void computeHelperLine() { 837 if (mousePos == null) { 838 // Don't draw the line. 839 currentMouseEastNorth = null; 840 currentBaseNode = null; 841 return; 842 } 843 844 DataSet ds = getLayerManager().getEditDataSet(); 845 Collection<OsmPrimitive> selection = ds != null ? ds.getSelected() : Collections.emptyList(); 846 847 MapView mv = MainApplication.getMap().mapView; 848 Node currentMouseNode = null; 849 mouseOnExistingNode = null; 850 mouseOnExistingWays = new HashSet<>(); 851 852 if (!ctrl && mousePos != null) { 853 currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive::isSelectable); 854 } 855 856 // We need this for highlighting and we'll only do so if we actually want to re-use 857 // *and* there is no node nearby (because nodes beat ways when re-using) 858 if (!ctrl && currentMouseNode == null) { 859 List<WaySegment> wss = mv.getNearestWaySegments(mousePos, OsmPrimitive::isSelectable); 860 for (WaySegment ws : wss) { 861 mouseOnExistingWays.add(ws.getWay()); 862 } 863 } 864 865 if (currentMouseNode != null) { 866 // user clicked on node 867 if (selection.isEmpty()) return; 868 currentMouseEastNorth = currentMouseNode.getEastNorth(); 869 mouseOnExistingNode = currentMouseNode; 870 } else { 871 // no node found in clicked area 872 currentMouseEastNorth = mv.getEastNorth(mousePos.x, mousePos.y); 873 } 874 875 determineCurrentBaseNodeAndPreviousNode(selection); 876 if (previousNode == null) { 877 snapHelper.noSnapNow(); 878 } 879 880 if (getCurrentBaseNode() == null || getCurrentBaseNode() == currentMouseNode) 881 return; // Don't create zero length way segments. 882 883 showStatusInfo(Double.NaN, -1, -1, snapHelper.isSnapOn()); 884 885 double curHdg = Utils.toDegrees(getCurrentBaseNode().getEastNorth() 886 .heading(currentMouseEastNorth)); 887 double baseHdg = -1; 888 if (previousNode != null) { 889 EastNorth en = previousNode.getEastNorth(); 890 if (en != null) { 891 baseHdg = Utils.toDegrees(en.heading(getCurrentBaseNode().getEastNorth())); 892 } 893 } 894 895 snapHelper.checkAngleSnapping(currentMouseEastNorth, baseHdg, curHdg); 896 897 // status bar was filled by snapHelper 898 } 899 900 static void showStatusInfo(double angle, double hdg, double distance, boolean activeFlag) { 901 MapFrame map = MainApplication.getMap(); 902 map.statusLine.setAngleNaN(angle); 903 map.statusLine.activateAnglePanel(activeFlag); 904 map.statusLine.setHeading(hdg); 905 map.statusLine.setDist(distance); 906 } 907 908 /** 909 * Helper function that sets fields currentBaseNode and previousNode 910 * @param selection 911 * uses also lastUsedNode field 912 */ 913 private synchronized void determineCurrentBaseNodeAndPreviousNode(Collection<OsmPrimitive> selection) { 914 Node selectedNode = null; 915 Way selectedWay = null; 916 for (OsmPrimitive p : selection) { 917 if (p instanceof Node) { 918 if (selectedNode != null) 919 return; 920 selectedNode = (Node) p; 921 } else if (p instanceof Way) { 922 if (selectedWay != null) 923 return; 924 selectedWay = (Way) p; 925 } 926 } 927 // we are here, if not more than 1 way or node is selected, 928 929 // the node from which we make a connection 930 currentBaseNode = null; 931 previousNode = null; 932 933 // Try to find an open way to measure angle from it. The way is not to be continued! 934 // warning: may result in changes of currentBaseNode and previousNode 935 // please remove if bugs arise 936 if (selectedWay == null && selectedNode != null) { 937 for (OsmPrimitive p: selectedNode.getReferrers()) { 938 if (p.isUsable() && p instanceof Way && ((Way) p).isFirstLastNode(selectedNode)) { 939 if (selectedWay != null) { // two uncontinued ways, nothing to take as reference 940 selectedWay = null; 941 break; 942 } else { 943 // set us ~continue this way (measure angle from it) 944 selectedWay = (Way) p; 945 } 946 } 947 } 948 } 949 950 if (selectedNode == null) { 951 if (selectedWay == null) 952 return; 953 continueWayFromNode(selectedWay, lastUsedNode); 954 } else if (selectedWay == null) { 955 currentBaseNode = selectedNode; 956 } else if (!selectedWay.isDeleted()) { // fix #7118 957 continueWayFromNode(selectedWay, selectedNode); 958 } 959 } 960 961 /** 962 * if one of the ends of {@code way} is given {@code node}, 963 * then set currentBaseNode = node and previousNode = adjacent node of way 964 * @param way way to continue 965 * @param node starting node 966 */ 967 private void continueWayFromNode(Way way, Node node) { 968 int n = way.getNodesCount(); 969 if (node == way.firstNode()) { 970 currentBaseNode = node; 971 if (n > 1) previousNode = way.getNode(1); 972 } else if (node == way.lastNode()) { 973 currentBaseNode = node; 974 if (n > 1) previousNode = way.getNode(n-2); 975 } 976 } 977 978 /** 979 * Repaint on mouse exit so that the helper line goes away. 980 */ 981 @Override 982 public void mouseExited(MouseEvent e) { 983 OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer(); 984 if (editLayer == null) 985 return; 986 mousePos = e.getPoint(); 987 snapHelper.noSnapNow(); 988 boolean repaintIssued = removeHighlighting(e); 989 // force repaint in case snapHelper needs one. If removeHighlighting 990 // caused one already, don't do it again. 991 if (!repaintIssued) { 992 editLayer.invalidate(); 993 } 994 } 995 996 /** 997 * Replies the parent way of a node, if it is the end of exactly one usable way. 998 * @param n node 999 * @return If the node is the end of exactly one way, return this. 1000 * <code>null</code> otherwise. 1001 */ 1002 public static Way getWayForNode(Node n) { 1003 Way way = null; 1004 for (Way w : (Iterable<Way>) n.referrers(Way.class)::iterator) { 1005 if (!w.isUsable() || w.getNodesCount() < 1) { 1006 continue; 1007 } 1008 Node firstNode = w.firstNode(); 1009 Node lastNode = w.lastNode(); 1010 if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) { 1011 if (way != null) 1012 return null; 1013 way = w; 1014 } 1015 } 1016 return way; 1017 } 1018 1019 /** 1020 * Replies the current base node, after having checked it is still usable (see #11105). 1021 * @return the current base node (can be null). If not-null, it's guaranteed the node is usable 1022 */ 1023 public synchronized Node getCurrentBaseNode() { 1024 if (currentBaseNode != null && (currentBaseNode.getDataSet() == null || !currentBaseNode.isUsable())) { 1025 currentBaseNode = null; 1026 } 1027 return currentBaseNode; 1028 } 1029 1030 private static void pruneSuccsAndReverse(List<Integer> is) { 1031 Set<Integer> is2 = new HashSet<>(); 1032 for (int i : is) { 1033 if (!is2.contains(i - 1) && !is2.contains(i + 1)) { 1034 is2.add(i); 1035 } 1036 } 1037 is.clear(); 1038 is.addAll(is2); 1039 Collections.sort(is); 1040 Collections.reverse(is); 1041 } 1042 1043 /** 1044 * Adjusts the position of a node to lie on a segment (or a segment intersection). 1045 * 1046 * If one or more than two segments are passed, the node is adjusted 1047 * to lie on the first segment that is passed. 1048 * 1049 * If two segments are passed, the node is adjusted to be at their intersection. 1050 * 1051 * No action is taken if no segments are passed. 1052 * 1053 * @param segs the segments to use as a reference when adjusting 1054 * @param n the node to adjust 1055 */ 1056 private static void adjustNode(Collection<Pair<Node, Node>> segs, Node n) { 1057 switch (segs.size()) { 1058 case 0: 1059 return; 1060 case 2: 1061 adjustNodeTwoSegments(segs, n); 1062 break; 1063 default: 1064 adjustNodeDefault(segs, n); 1065 } 1066 } 1067 1068 private static void adjustNodeTwoSegments(Collection<Pair<Node, Node>> segs, Node n) { 1069 // This computes the intersection between the two segments and adjusts the node position. 1070 Iterator<Pair<Node, Node>> i = segs.iterator(); 1071 Pair<Node, Node> seg = i.next(); 1072 EastNorth pA = seg.a.getEastNorth(); 1073 EastNorth pB = seg.b.getEastNorth(); 1074 seg = i.next(); 1075 EastNorth pC = seg.a.getEastNorth(); 1076 EastNorth pD = seg.b.getEastNorth(); 1077 1078 double u = det(pB.east() - pA.east(), pB.north() - pA.north(), pC.east() - pD.east(), pC.north() - pD.north()); 1079 1080 // Check for parallel segments and do nothing if they are 1081 // In practice this will probably only happen when a way has been duplicated 1082 1083 if (u == 0) 1084 return; 1085 1086 // q is a number between 0 and 1 1087 // It is the point in the segment where the intersection occurs 1088 // if the segment is scaled to length 1 1089 1090 double q = det(pB.north() - pC.north(), pB.east() - pC.east(), pD.north() - pC.north(), pD.east() - pC.east()) / u; 1091 EastNorth intersection = new EastNorth( 1092 pB.east() + q * (pA.east() - pB.east()), 1093 pB.north() + q * (pA.north() - pB.north())); 1094 1095 1096 // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise 1097 // fall through to default action. 1098 // (for semi-parallel lines, intersection might be miles away!) 1099 MapFrame map = MainApplication.getMap(); 1100 if (map.mapView.getPoint2D(n).distance(map.mapView.getPoint2D(intersection)) < SNAP_TO_INTERSECTION_THRESHOLD.get()) { 1101 n.setEastNorth(intersection); 1102 return; 1103 } 1104 1105 adjustNodeDefault(segs, n); 1106 } 1107 1108 private static void adjustNodeDefault(Collection<Pair<Node, Node>> segs, Node n) { 1109 EastNorth p = n.getEastNorth(); 1110 Pair<Node, Node> seg = segs.iterator().next(); 1111 EastNorth pA = seg.a.getEastNorth(); 1112 EastNorth pB = seg.b.getEastNorth(); 1113 double a = p.distanceSq(pB); 1114 double b = p.distanceSq(pA); 1115 double c = pA.distanceSq(pB); 1116 double q = (a - b + c) / (2*c); 1117 n.setEastNorth(new EastNorth(pB.east() + q * (pA.east() - pB.east()), pB.north() + q * (pA.north() - pB.north()))); 1118 } 1119 1120 // helper for adjustNode 1121 static double det(double a, double b, double c, double d) { 1122 return a * d - b * c; 1123 } 1124 1125 private void tryToMoveNodeOnIntersection(List<WaySegment> wss, Node n) { 1126 if (wss.isEmpty()) 1127 return; 1128 WaySegment ws = wss.get(0); 1129 EastNorth p1 = ws.getFirstNode().getEastNorth(); 1130 EastNorth p2 = ws.getSecondNode().getEastNorth(); 1131 if (snapHelper.dir2 != null && getCurrentBaseNode() != null) { 1132 EastNorth xPoint = Geometry.getSegmentSegmentIntersection(p1, p2, snapHelper.dir2, 1133 getCurrentBaseNode().getEastNorth()); 1134 if (xPoint != null) { 1135 n.setEastNorth(xPoint); 1136 } 1137 } 1138 } 1139 1140 /** 1141 * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted 1142 * (if feature enabled). Also sets the target cursor if appropriate. It adds the to-be- 1143 * highlighted primitives to newHighlights but does not actually highlight them. This work is 1144 * done in redrawIfRequired. This means, calling addHighlighting() without redrawIfRequired() 1145 * will leave the data in an inconsistent state. 1146 * 1147 * The status bar derives its information from oldHighlights, so in order to update the status 1148 * bar both addHighlighting() and repaintIfRequired() are needed, since former fills newHighlights 1149 * and latter processes them into oldHighlights. 1150 * @param event event, can be null 1151 */ 1152 private void addHighlighting(Object event) { 1153 newHighlights = new HashSet<>(); 1154 MapView mapView = MainApplication.getMap().mapView; 1155 1156 // if ctrl key is held ("no join"), don't highlight anything 1157 if (ctrl) { 1158 mapView.setNewCursor(cursor, this); 1159 redrawIfRequired(event); 1160 return; 1161 } 1162 1163 // This happens when nothing is selected, but we still want to highlight the "target node" 1164 DataSet ds = getLayerManager().getEditDataSet(); 1165 if (mouseOnExistingNode == null && mousePos != null && ds != null && ds.selectionEmpty()) { 1166 mouseOnExistingNode = mapView.getNearestNode(mousePos, OsmPrimitive::isSelectable); 1167 } 1168 1169 if (mouseOnExistingNode != null) { 1170 mapView.setNewCursor(cursorJoinNode, this); 1171 newHighlights.add(mouseOnExistingNode); 1172 redrawIfRequired(event); 1173 return; 1174 } 1175 1176 // Insert the node into all the nearby way segments 1177 if (mouseOnExistingWays.isEmpty()) { 1178 mapView.setNewCursor(cursor, this); 1179 redrawIfRequired(event); 1180 return; 1181 } 1182 1183 mapView.setNewCursor(cursorJoinWay, this); 1184 newHighlights.addAll(mouseOnExistingWays); 1185 redrawIfRequired(event); 1186 } 1187 1188 /** 1189 * Removes target highlighting from primitives. Issues repaint if required. 1190 * @param event event, can be null 1191 * @return true if a repaint has been issued. 1192 */ 1193 private boolean removeHighlighting(Object event) { 1194 newHighlights = new HashSet<>(); 1195 return redrawIfRequired(event); 1196 } 1197 1198 @Override 1199 public synchronized void paint(Graphics2D g, MapView mv, Bounds box) { 1200 // sanity checks 1201 MapView mapView = MainApplication.getMap().mapView; 1202 if (mapView == null || mousePos == null 1203 // don't draw line if we don't know where from or where to 1204 || currentMouseEastNorth == null || getCurrentBaseNode() == null 1205 // don't draw line if mouse is outside window 1206 || !mapView.getState().getForView(mousePos.getX(), mousePos.getY()).isInView()) 1207 return; 1208 1209 Graphics2D g2 = g; 1210 snapHelper.drawIfNeeded(g2, mv.getState()); 1211 if (!DRAW_HELPER_LINE.get() || wayIsFinished || shift) 1212 return; 1213 1214 if (!snapHelper.isActive()) { 1215 g2.setColor(RUBBER_LINE_COLOR.get()); 1216 g2.setStroke(RUBBER_LINE_STROKE.get()); 1217 paintConstructionGeometry(mv, g2); 1218 } else if (DRAW_CONSTRUCTION_GEOMETRY.get()) { 1219 // else use color and stoke from snapHelper.draw 1220 paintConstructionGeometry(mv, g2); 1221 } 1222 } 1223 1224 private void paintConstructionGeometry(MapView mv, Graphics2D g2) { 1225 MapPath2D b = new MapPath2D(); 1226 MapViewPoint p1 = mv.getState().getPointFor(getCurrentBaseNode()); 1227 MapViewPoint p2 = mv.getState().getPointFor(currentMouseEastNorth); 1228 1229 b.moveTo(p1); 1230 b.lineTo(p2); 1231 1232 // if alt key is held ("start new way"), draw a little perpendicular line 1233 if (alt) { 1234 START_WAY_INDICATOR.paintArrowAt(b, p1, p2); 1235 } 1236 1237 g2.draw(b); 1238 g2.setStroke(BASIC_STROKE); 1239 } 1240 1241 @Override 1242 public String getModeHelpText() { 1243 StringBuilder rv; 1244 /* 1245 * No modifiers: all (Connect, Node Re-Use, Auto-Weld) 1246 * CTRL: disables node re-use, auto-weld 1247 * Shift: do not make connection 1248 * ALT: make connection but start new way in doing so 1249 */ 1250 1251 /* 1252 * Status line text generation is split into two parts to keep it maintainable. 1253 * First part looks at what will happen to the new node inserted on click and 1254 * the second part will look if a connection is made or not. 1255 * 1256 * Note that this help text is not absolutely accurate as it doesn't catch any special 1257 * cases (e.g. when preventing <---> ways). The only special that it catches is when 1258 * a way is about to be finished. 1259 * 1260 * First check what happens to the new node. 1261 */ 1262 1263 // oldHighlights stores the current highlights. If this 1264 // list is empty we can assume that we won't do any joins 1265 if (ctrl || oldHighlights.isEmpty()) { 1266 rv = new StringBuilder(tr("Create new node.")); 1267 } else { 1268 // oldHighlights may store a node or way, check if it's a node 1269 OsmPrimitive x = oldHighlights.iterator().next(); 1270 if (x instanceof Node) { 1271 rv = new StringBuilder(tr("Select node under cursor.")); 1272 } else { 1273 rv = new StringBuilder(trn("Insert new node into way.", "Insert new node into {0} ways.", 1274 oldHighlights.size(), oldHighlights.size())); 1275 } 1276 } 1277 1278 /* 1279 * Check whether a connection will be made 1280 */ 1281 if (!wayIsFinished && getCurrentBaseNode() != null) { 1282 if (alt) { 1283 rv.append(' ').append(tr("Start new way from last node.")); 1284 } else { 1285 rv.append(' ').append(tr("Continue way from last node.")); 1286 } 1287 if (snapHelper.isSnapOn()) { 1288 rv.append(' ').append(tr("Angle snapping active.")); 1289 } 1290 } 1291 1292 Node n = mouseOnExistingNode; 1293 DataSet ds = getLayerManager().getEditDataSet(); 1294 /* 1295 * Handle special case: Highlighted node == selected node => finish drawing 1296 */ 1297 if (n != null && ds != null && ds.getSelectedNodes().contains(n)) { 1298 if (wayIsFinished) { 1299 rv = new StringBuilder(tr("Select node under cursor.")); 1300 } else { 1301 rv = new StringBuilder(tr("Finish drawing.")); 1302 } 1303 } 1304 1305 /* 1306 * Handle special case: Self-Overlapping or closing way 1307 */ 1308 if (ds != null && !ds.getSelectedWays().isEmpty() && !wayIsFinished && !alt) { 1309 Way w = ds.getSelectedWays().iterator().next(); 1310 if (w.getNodes().stream().anyMatch(m -> m.equals(mouseOnExistingNode) || mouseOnExistingWays.contains(w))) { 1311 rv.append(' ').append(tr("Finish drawing.")); 1312 } 1313 } 1314 return rv.toString(); 1315 } 1316 1317 /** 1318 * Get selected primitives, while draw action is in progress. 1319 * 1320 * While drawing a way, technically the last node is selected. 1321 * This is inconvenient when the user tries to add/edit tags to the way. 1322 * For this case, this method returns the current way as selection, 1323 * to work around this issue. 1324 * Otherwise the normal selection of the current data layer is returned. 1325 * @return selected primitives, while draw action is in progress 1326 */ 1327 public Collection<OsmPrimitive> getInProgressSelection() { 1328 DataSet ds = getLayerManager().getEditDataSet(); 1329 if (ds == null) return Collections.emptyList(); 1330 if (getCurrentBaseNode() != null && !ds.selectionEmpty()) { 1331 Way continueFrom = getWayForNode(getCurrentBaseNode()); 1332 if (continueFrom != null) 1333 return Collections.<OsmPrimitive>singleton(continueFrom); 1334 } 1335 return ds.getSelected(); 1336 } 1337 1338 @Override 1339 public boolean layerIsSupported(Layer l) { 1340 return isEditableDataLayer(l); 1341 } 1342 1343 @Override 1344 protected void updateEnabledState() { 1345 setEnabled(getLayerManager().getEditLayer() != null); 1346 } 1347 1348 @Override 1349 public void destroy() { 1350 super.destroy(); 1351 finishDrawing(); 1352 MainApplication.getMenu().editMenu.remove(snapCheckboxMenuItem); 1353 snapChangeAction.destroy(); 1354 } 1355 1356 /** 1357 * Undo the last command. Binded by default to backspace key. 1358 */ 1359 public class BackSpaceAction extends AbstractAction { 1360 1361 @Override 1362 public void actionPerformed(ActionEvent e) { 1363 UndoRedoHandler.getInstance().undo(); 1364 Command lastCmd = UndoRedoHandler.getInstance().getLastCommand(); 1365 if (lastCmd == null) return; 1366 Node n = null; 1367 for (OsmPrimitive p: lastCmd.getParticipatingPrimitives()) { 1368 if (p instanceof Node) { 1369 if (n == null) { 1370 n = (Node) p; // found one node 1371 wayIsFinished = false; 1372 } else { 1373 // if more than 1 node were affected by previous command, 1374 // we have no way to continue, so we forget about found node 1375 n = null; 1376 break; 1377 } 1378 } 1379 } 1380 // select last added node - maybe we will continue drawing from it 1381 if (n != null) { 1382 addSelection(getLayerManager().getEditDataSet(), n); 1383 } 1384 } 1385 } 1386 1387 private class SnapChangeAction extends JosmAction { 1388 /** 1389 * Constructs a new {@code SnapChangeAction}. 1390 */ 1391 SnapChangeAction() { 1392 super(tr("Angle snapping"), /* ICON() */ "anglesnap", 1393 tr("Switch angle snapping mode while drawing"), null, false); 1394 setHelpId(ht("/Action/Draw/AngleSnap")); 1395 } 1396 1397 @Override 1398 public void actionPerformed(ActionEvent e) { 1399 if (snapHelper != null) { 1400 snapHelper.toggleSnapping(); 1401 } 1402 } 1403 1404 @Override 1405 protected void updateEnabledState() { 1406 MapFrame map = MainApplication.getMap(); 1407 setEnabled(map != null && map.mapMode instanceof DrawAction); 1408 } 1409 } 1410}