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.ChangeCommand; 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("Mode: 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.beginUpdate(); // to prevent the selection listener to screw around with the state 250 try { 251 addSelection(ds, toAdd); 252 clearSelection(ds, toRemove); 253 } finally { 254 ds.endUpdate(); 255 } 256 } 257 258 private static void updatePreservedFlag(OsmPrimitive osm, boolean state) { 259 // Preserves selected primitives and selected way nodes 260 osm.setPreserved(state); 261 if (osm instanceof Way) { 262 for (Node n : ((Way) osm).getNodes()) { 263 n.setPreserved(state); 264 } 265 } 266 } 267 268 private static void setSelection(DataSet ds, Collection<OsmPrimitive> toSet) { 269 toSet.forEach(x -> updatePreservedFlag(x, true)); 270 ds.setSelected(toSet); 271 } 272 273 private static void setSelection(DataSet ds, OsmPrimitive toSet) { 274 updatePreservedFlag(toSet, true); 275 ds.setSelected(toSet); 276 } 277 278 private static void addSelection(DataSet ds, OsmPrimitive toAdd) { 279 updatePreservedFlag(toAdd, true); 280 ds.addSelected(toAdd); 281 } 282 283 private static void clearSelection(DataSet ds, OsmPrimitive toRemove) { 284 ds.clearSelection(toRemove); 285 updatePreservedFlag(toRemove, false); 286 } 287 288 @Override 289 public void enterMode() { 290 if (!isEnabled()) 291 return; 292 super.enterMode(); 293 readPreferences(); 294 295 // determine if selection is suitable to continue drawing. If it 296 // isn't, set wayIsFinished to true to avoid superfluous repaints. 297 determineCurrentBaseNodeAndPreviousNode(getLayerManager().getEditDataSet().getSelected()); 298 wayIsFinished = getCurrentBaseNode() == null; 299 300 toleranceMultiplier = 0.01 * NavigatableComponent.PROP_SNAP_DISTANCE.get(); 301 302 snapHelper.init(); 303 snapCheckboxMenuItem.getAction().setEnabled(true); 304 305 MapFrame map = MainApplication.getMap(); 306 map.statusLine.getAnglePanel().addMouseListener(snapHelper.anglePopupListener); 307 MainApplication.registerActionShortcut(backspaceAction, backspaceShortcut); 308 309 map.mapView.addMouseListener(this); 310 map.mapView.addMouseMotionListener(this); 311 map.mapView.addTemporaryLayer(this); 312 SelectionEventManager.getInstance().addSelectionListenerForEdt(this); 313 314 map.keyDetector.addKeyListener(this); 315 map.keyDetector.addModifierExListener(this); 316 ignoreNextKeyRelease = true; 317 } 318 319 @Override 320 public void exitMode() { 321 super.exitMode(); 322 MapFrame map = MainApplication.getMap(); 323 map.mapView.removeMouseListener(this); 324 map.mapView.removeMouseMotionListener(this); 325 map.mapView.removeTemporaryLayer(this); 326 SelectionEventManager.getInstance().removeSelectionListener(this); 327 MainApplication.unregisterActionShortcut(backspaceAction, backspaceShortcut); 328 snapHelper.unsetFixedMode(); 329 snapCheckboxMenuItem.getAction().setEnabled(false); 330 331 map.statusLine.getAnglePanel().removeMouseListener(snapHelper.anglePopupListener); 332 map.statusLine.activateAnglePanel(false); 333 334 DataSet ds = getLayerManager().getEditDataSet(); 335 if (ds != null) { 336 ds.getSelected().forEach(x -> updatePreservedFlag(x, false)); 337 } 338 339 removeHighlighting(null); 340 map.keyDetector.removeKeyListener(this); 341 map.keyDetector.removeModifierExListener(this); 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 List<Way> reuseWays = new ArrayList<>(); 522 List<Way> replacedWays = new ArrayList<>(); 523 524 if (newNode) { 525 if (n.isOutSideWorld()) { 526 JOptionPane.showMessageDialog( 527 MainApplication.getMainFrame(), 528 tr("Cannot add a node outside of the world."), 529 tr("Warning"), 530 JOptionPane.WARNING_MESSAGE 531 ); 532 return; 533 } 534 cmds.add(new AddCommand(ds, n)); 535 536 if (!ctrl) { 537 // Insert the node into all the nearby way segments 538 List<WaySegment> wss = mapView.getNearestWaySegments( 539 mapView.getPoint(n), OsmPrimitive::isSelectable); 540 if (snapHelper.isActive()) { 541 tryToMoveNodeOnIntersection(wss, n); 542 } 543 insertNodeIntoAllNearbySegments(wss, n, newSelection, cmds, replacedWays, reuseWays); 544 } 545 } 546 // now "n" is newly created or reused node that shoud be added to some way 547 548 // This part decides whether or not a "segment" (i.e. a connection) is made to an existing node. 549 550 // For a connection to be made, the user must either have a node selected (connection 551 // is made to that node), or he must have a way selected *and* one of the endpoints 552 // of that way must be the last used node (connection is made to last used node), or 553 // he must have a way and a node selected (connection is made to the selected node). 554 555 // If the above does not apply, the selection is cleared and a new try is started 556 557 boolean extendedWay = false; 558 boolean wayIsFinishedTemp = wayIsFinished; 559 wayIsFinished = false; 560 561 // don't draw lines if shift is held 562 if (!selection.isEmpty() && !shift) { 563 Node selectedNode = null; 564 Way selectedWay = null; 565 566 for (OsmPrimitive p : selection) { 567 if (p instanceof Node) { 568 if (selectedNode != null) { 569 // Too many nodes selected to do something useful 570 tryAgain(e); 571 return; 572 } 573 selectedNode = (Node) p; 574 } else if (p instanceof Way) { 575 if (selectedWay != null) { 576 // Too many ways selected to do something useful 577 tryAgain(e); 578 return; 579 } 580 selectedWay = (Way) p; 581 } 582 } 583 584 // the node from which we make a connection 585 Node n0 = findNodeToContinueFrom(selectedNode, selectedWay); 586 // We have a selection but it isn't suitable. Try again. 587 if (n0 == null) { 588 tryAgain(e); 589 return; 590 } 591 if (!wayIsFinishedTemp) { 592 if (isSelfContainedWay(selectedWay, n0, n)) 593 return; 594 595 // User clicked last node again, finish way 596 if (n0 == n) { 597 finishDrawing(); 598 return; 599 } 600 601 // Ok we know now that we'll insert a line segment, but will it connect to an 602 // existing way or make a new way of its own? The "alt" modifier means that the 603 // user wants a new way. 604 Way way = alt ? null : (selectedWay != null ? selectedWay : getWayForNode(n0)); 605 Way wayToSelect; 606 607 // Don't allow creation of self-overlapping ways 608 if (way != null) { 609 int nodeCount = 0; 610 for (Node p : way.getNodes()) { 611 if (p.equals(n0)) { 612 nodeCount++; 613 } 614 } 615 if (nodeCount > 1) { 616 way = null; 617 } 618 } 619 620 if (way == null) { 621 way = new Way(); 622 way.addNode(n0); 623 cmds.add(new AddCommand(ds, way)); 624 wayToSelect = way; 625 } else { 626 int i; 627 if ((i = replacedWays.indexOf(way)) != -1) { 628 way = reuseWays.get(i); 629 wayToSelect = way; 630 } else { 631 wayToSelect = way; 632 Way wnew = new Way(way); 633 cmds.add(new ChangeCommand(way, wnew)); 634 way = wnew; 635 } 636 } 637 638 // Connected to a node that's already in the way 639 if (way.containsNode(n)) { 640 wayIsFinished = true; 641 selection.clear(); 642 } 643 644 // Add new node to way 645 if (way.getNode(way.getNodesCount() - 1) == n0) { 646 way.addNode(n); 647 } else { 648 way.addNode(0, n); 649 } 650 651 extendedWay = true; 652 newSelection.clear(); 653 newSelection.add(wayToSelect); 654 } 655 } 656 if (!extendedWay && !newNode) { 657 return; // We didn't do anything. 658 } 659 660 String title = getTitle(newNode, n, newSelection, reuseWays, extendedWay); 661 662 Command c = new SequenceCommand(title, cmds); 663 664 UndoRedoHandler.getInstance().add(c); 665 if (!wayIsFinished) { 666 lastUsedNode = n; 667 } 668 669 setSelection(ds, newSelection); 670 671 // "viewport following" mode for tracing long features 672 // from aerial imagery or GPS tracks. 673 if (VIEWPORT_FOLLOWING.get()) { 674 mapView.smoothScrollTo(n.getEastNorth()); 675 } 676 computeHelperLine(); 677 removeHighlighting(e); 678 } 679 680 private static String getTitle(boolean newNode, Node n, Collection<OsmPrimitive> newSelection, List<Way> reuseWays, 681 boolean extendedWay) { 682 String title; 683 if (!extendedWay) { 684 if (reuseWays.isEmpty()) { 685 title = tr("Add node"); 686 } else { 687 title = tr("Add node into way"); 688 for (Way w : reuseWays) { 689 newSelection.remove(w); 690 } 691 } 692 newSelection.clear(); 693 newSelection.add(n); 694 } else if (!newNode) { 695 title = tr("Connect existing way to node"); 696 } else if (reuseWays.isEmpty()) { 697 title = tr("Add a new node to an existing way"); 698 } else { 699 title = tr("Add node into way and connect"); 700 } 701 return title; 702 } 703 704 private void insertNodeIntoAllNearbySegments(List<WaySegment> wss, Node n, Collection<OsmPrimitive> newSelection, 705 Collection<Command> cmds, List<Way> replacedWays, List<Way> reuseWays) { 706 Map<Way, List<Integer>> insertPoints = new HashMap<>(); 707 for (WaySegment ws : wss) { 708 List<Integer> is; 709 if (insertPoints.containsKey(ws.way)) { 710 is = insertPoints.get(ws.way); 711 } else { 712 is = new ArrayList<>(); 713 insertPoints.put(ws.way, is); 714 } 715 716 is.add(ws.lowerIndex); 717 } 718 719 Set<Pair<Node, Node>> segSet = new HashSet<>(); 720 721 for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) { 722 Way w = insertPoint.getKey(); 723 List<Integer> is = insertPoint.getValue(); 724 725 Way wnew = new Way(w); 726 727 pruneSuccsAndReverse(is); 728 for (int i : is) { 729 segSet.add(Pair.sort(new Pair<>(w.getNode(i), w.getNode(i+1)))); 730 wnew.addNode(i + 1, n); 731 } 732 733 // If ALT is pressed, a new way should be created and that new way should get 734 // selected. This works every time unless the ways the nodes get inserted into 735 // are already selected. This is the case when creating a self-overlapping way 736 // but pressing ALT prevents this. Therefore we must de-select the way manually 737 // here so /only/ the new way will be selected after this method finishes. 738 if (alt) { 739 newSelection.add(insertPoint.getKey()); 740 } 741 742 cmds.add(new ChangeCommand(insertPoint.getKey(), wnew)); 743 replacedWays.add(insertPoint.getKey()); 744 reuseWays.add(wnew); 745 } 746 747 adjustNode(segSet, n); 748 } 749 750 /** 751 * Prevent creation of ways that look like this: <----> 752 * This happens if users want to draw a no-exit-sideway from the main way like this: 753 * ^ 754 * |<----> 755 * | 756 * The solution isn't ideal because the main way will end in the side way, which is bad for 757 * navigation software ("drive straight on") but at least easier to fix. Maybe users will fix 758 * it on their own, too. At least it's better than producing an error. 759 * 760 * @param selectedWay the way to check 761 * @param currentNode the current node (i.e. the one the connection will be made from) 762 * @param targetNode the target node (i.e. the one the connection will be made to) 763 * @return {@code true} if this would create a selfcontaining way, {@code false} otherwise. 764 */ 765 private boolean isSelfContainedWay(Way selectedWay, Node currentNode, Node targetNode) { 766 if (selectedWay != null) { 767 int posn0 = selectedWay.getNodes().indexOf(currentNode); 768 // CHECKSTYLE.OFF: SingleSpaceSeparator 769 if ((posn0 != -1 && // n0 is part of way 770 (posn0 >= 1 && targetNode.equals(selectedWay.getNode(posn0-1)))) || // previous node 771 (posn0 < selectedWay.getNodesCount()-1 && targetNode.equals(selectedWay.getNode(posn0+1)))) { // next node 772 setSelection(getLayerManager().getEditDataSet(), targetNode); 773 lastUsedNode = targetNode; 774 return true; 775 } 776 // CHECKSTYLE.ON: SingleSpaceSeparator 777 } 778 779 return false; 780 } 781 782 /** 783 * Finds a node to continue drawing from. Decision is based upon given node and way. 784 * @param selectedNode Currently selected node, may be null 785 * @param selectedWay Currently selected way, may be null 786 * @return Node if a suitable node is found, null otherwise 787 */ 788 private Node findNodeToContinueFrom(Node selectedNode, Way selectedWay) { 789 // No nodes or ways have been selected, this occurs when a relation 790 // has been selected or the selection is empty 791 if (selectedNode == null && selectedWay == null) 792 return null; 793 794 if (selectedNode == null) { 795 if (selectedWay.isFirstLastNode(lastUsedNode)) 796 return lastUsedNode; 797 798 // We have a way selected, but no suitable node to continue from. Start anew. 799 return null; 800 } 801 802 if (selectedWay == null) 803 return selectedNode; 804 805 if (selectedWay.isFirstLastNode(selectedNode)) 806 return selectedNode; 807 808 // We have a way and node selected, but it's not at the start/end of the way. Start anew. 809 return null; 810 } 811 812 @Override 813 public void mouseDragged(MouseEvent e) { 814 mouseMoved(e); 815 } 816 817 @Override 818 public void mouseMoved(MouseEvent e) { 819 if (!MainApplication.getMap().mapView.isActiveLayerDrawable()) 820 return; 821 822 // we copy ctrl/alt/shift from the event just in case our global 823 // keyDetector didn't make it through the security manager. Unclear 824 // if that can ever happen but better be safe. 825 updateKeyModifiers(e); 826 mousePos = e.getPoint(); 827 if (snapHelper.isSnapOn() && ctrl) 828 tryToSetBaseSegmentForAngleSnap(); 829 830 computeHelperLine(); 831 addHighlighting(e); 832 } 833 834 /** 835 * This method is used to detect segment under mouse and use it as reference for angle snapping 836 */ 837 private void tryToSetBaseSegmentForAngleSnap() { 838 if (mousePos != null) { 839 WaySegment seg = MainApplication.getMap().mapView.getNearestWaySegment(mousePos, OsmPrimitive::isSelectable); 840 if (seg != null) { 841 snapHelper.setBaseSegment(seg); 842 } 843 } 844 } 845 846 /** 847 * This method prepares data required for painting the "helper line" from 848 * the last used position to the mouse cursor. It duplicates some code from 849 * mouseReleased() (FIXME). 850 */ 851 private synchronized void computeHelperLine() { 852 if (mousePos == null) { 853 // Don't draw the line. 854 currentMouseEastNorth = null; 855 currentBaseNode = null; 856 return; 857 } 858 859 DataSet ds = getLayerManager().getEditDataSet(); 860 Collection<OsmPrimitive> selection = ds != null ? ds.getSelected() : Collections.emptyList(); 861 862 MapView mv = MainApplication.getMap().mapView; 863 Node currentMouseNode = null; 864 mouseOnExistingNode = null; 865 mouseOnExistingWays = new HashSet<>(); 866 867 if (!ctrl && mousePos != null) { 868 currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive::isSelectable); 869 } 870 871 // We need this for highlighting and we'll only do so if we actually want to re-use 872 // *and* there is no node nearby (because nodes beat ways when re-using) 873 if (!ctrl && currentMouseNode == null) { 874 List<WaySegment> wss = mv.getNearestWaySegments(mousePos, OsmPrimitive::isSelectable); 875 for (WaySegment ws : wss) { 876 mouseOnExistingWays.add(ws.way); 877 } 878 } 879 880 if (currentMouseNode != null) { 881 // user clicked on node 882 if (selection.isEmpty()) return; 883 currentMouseEastNorth = currentMouseNode.getEastNorth(); 884 mouseOnExistingNode = currentMouseNode; 885 } else { 886 // no node found in clicked area 887 currentMouseEastNorth = mv.getEastNorth(mousePos.x, mousePos.y); 888 } 889 890 determineCurrentBaseNodeAndPreviousNode(selection); 891 if (previousNode == null) { 892 snapHelper.noSnapNow(); 893 } 894 895 if (getCurrentBaseNode() == null || getCurrentBaseNode() == currentMouseNode) 896 return; // Don't create zero length way segments. 897 898 showStatusInfo(-1, -1, -1, snapHelper.isSnapOn()); 899 900 double curHdg = Utils.toDegrees(getCurrentBaseNode().getEastNorth() 901 .heading(currentMouseEastNorth)); 902 double baseHdg = -1; 903 if (previousNode != null) { 904 EastNorth en = previousNode.getEastNorth(); 905 if (en != null) { 906 baseHdg = Utils.toDegrees(en.heading(getCurrentBaseNode().getEastNorth())); 907 } 908 } 909 910 snapHelper.checkAngleSnapping(currentMouseEastNorth, baseHdg, curHdg); 911 912 // status bar was filled by snapHelper 913 } 914 915 static void showStatusInfo(double angle, double hdg, double distance, boolean activeFlag) { 916 MapFrame map = MainApplication.getMap(); 917 map.statusLine.setAngle(angle); 918 map.statusLine.activateAnglePanel(activeFlag); 919 map.statusLine.setHeading(hdg); 920 map.statusLine.setDist(distance); 921 } 922 923 /** 924 * Helper function that sets fields currentBaseNode and previousNode 925 * @param selection 926 * uses also lastUsedNode field 927 */ 928 private synchronized void determineCurrentBaseNodeAndPreviousNode(Collection<OsmPrimitive> selection) { 929 Node selectedNode = null; 930 Way selectedWay = null; 931 for (OsmPrimitive p : selection) { 932 if (p instanceof Node) { 933 if (selectedNode != null) 934 return; 935 selectedNode = (Node) p; 936 } else if (p instanceof Way) { 937 if (selectedWay != null) 938 return; 939 selectedWay = (Way) p; 940 } 941 } 942 // we are here, if not more than 1 way or node is selected, 943 944 // the node from which we make a connection 945 currentBaseNode = null; 946 previousNode = null; 947 948 // Try to find an open way to measure angle from it. The way is not to be continued! 949 // warning: may result in changes of currentBaseNode and previousNode 950 // please remove if bugs arise 951 if (selectedWay == null && selectedNode != null) { 952 for (OsmPrimitive p: selectedNode.getReferrers()) { 953 if (p.isUsable() && p instanceof Way && ((Way) p).isFirstLastNode(selectedNode)) { 954 if (selectedWay != null) { // two uncontinued ways, nothing to take as reference 955 selectedWay = null; 956 break; 957 } else { 958 // set us ~continue this way (measure angle from it) 959 selectedWay = (Way) p; 960 } 961 } 962 } 963 } 964 965 if (selectedNode == null) { 966 if (selectedWay == null) 967 return; 968 continueWayFromNode(selectedWay, lastUsedNode); 969 } else if (selectedWay == null) { 970 currentBaseNode = selectedNode; 971 } else if (!selectedWay.isDeleted()) { // fix #7118 972 continueWayFromNode(selectedWay, selectedNode); 973 } 974 } 975 976 /** 977 * if one of the ends of {@code way} is given {@code node}, 978 * then set currentBaseNode = node and previousNode = adjacent node of way 979 * @param way way to continue 980 * @param node starting node 981 */ 982 private void continueWayFromNode(Way way, Node node) { 983 int n = way.getNodesCount(); 984 if (node == way.firstNode()) { 985 currentBaseNode = node; 986 if (n > 1) previousNode = way.getNode(1); 987 } else if (node == way.lastNode()) { 988 currentBaseNode = node; 989 if (n > 1) previousNode = way.getNode(n-2); 990 } 991 } 992 993 /** 994 * Repaint on mouse exit so that the helper line goes away. 995 */ 996 @Override 997 public void mouseExited(MouseEvent e) { 998 OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer(); 999 if (editLayer == null) 1000 return; 1001 mousePos = e.getPoint(); 1002 snapHelper.noSnapNow(); 1003 boolean repaintIssued = removeHighlighting(e); 1004 // force repaint in case snapHelper needs one. If removeHighlighting 1005 // caused one already, don't do it again. 1006 if (!repaintIssued) { 1007 editLayer.invalidate(); 1008 } 1009 } 1010 1011 /** 1012 * Replies the parent way of a node, if it is the end of exactly one usable way. 1013 * @param n node 1014 * @return If the node is the end of exactly one way, return this. 1015 * <code>null</code> otherwise. 1016 */ 1017 public static Way getWayForNode(Node n) { 1018 Way way = null; 1019 for (Way w : (Iterable<Way>) n.referrers(Way.class)::iterator) { 1020 if (!w.isUsable() || w.getNodesCount() < 1) { 1021 continue; 1022 } 1023 Node firstNode = w.firstNode(); 1024 Node lastNode = w.lastNode(); 1025 if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) { 1026 if (way != null) 1027 return null; 1028 way = w; 1029 } 1030 } 1031 return way; 1032 } 1033 1034 /** 1035 * Replies the current base node, after having checked it is still usable (see #11105). 1036 * @return the current base node (can be null). If not-null, it's guaranteed the node is usable 1037 */ 1038 public synchronized Node getCurrentBaseNode() { 1039 if (currentBaseNode != null && (currentBaseNode.getDataSet() == null || !currentBaseNode.isUsable())) { 1040 currentBaseNode = null; 1041 } 1042 return currentBaseNode; 1043 } 1044 1045 private static void pruneSuccsAndReverse(List<Integer> is) { 1046 Set<Integer> is2 = new HashSet<>(); 1047 for (int i : is) { 1048 if (!is2.contains(i - 1) && !is2.contains(i + 1)) { 1049 is2.add(i); 1050 } 1051 } 1052 is.clear(); 1053 is.addAll(is2); 1054 Collections.sort(is); 1055 Collections.reverse(is); 1056 } 1057 1058 /** 1059 * Adjusts the position of a node to lie on a segment (or a segment intersection). 1060 * 1061 * If one or more than two segments are passed, the node is adjusted 1062 * to lie on the first segment that is passed. 1063 * 1064 * If two segments are passed, the node is adjusted to be at their intersection. 1065 * 1066 * No action is taken if no segments are passed. 1067 * 1068 * @param segs the segments to use as a reference when adjusting 1069 * @param n the node to adjust 1070 */ 1071 private static void adjustNode(Collection<Pair<Node, Node>> segs, Node n) { 1072 switch (segs.size()) { 1073 case 0: 1074 return; 1075 case 2: 1076 adjustNodeTwoSegments(segs, n); 1077 break; 1078 default: 1079 adjustNodeDefault(segs, n); 1080 } 1081 } 1082 1083 private static void adjustNodeTwoSegments(Collection<Pair<Node, Node>> segs, Node n) { 1084 // This computes the intersection between the two segments and adjusts the node position. 1085 Iterator<Pair<Node, Node>> i = segs.iterator(); 1086 Pair<Node, Node> seg = i.next(); 1087 EastNorth pA = seg.a.getEastNorth(); 1088 EastNorth pB = seg.b.getEastNorth(); 1089 seg = i.next(); 1090 EastNorth pC = seg.a.getEastNorth(); 1091 EastNorth pD = seg.b.getEastNorth(); 1092 1093 double u = det(pB.east() - pA.east(), pB.north() - pA.north(), pC.east() - pD.east(), pC.north() - pD.north()); 1094 1095 // Check for parallel segments and do nothing if they are 1096 // In practice this will probably only happen when a way has been duplicated 1097 1098 if (u == 0) 1099 return; 1100 1101 // q is a number between 0 and 1 1102 // It is the point in the segment where the intersection occurs 1103 // if the segment is scaled to length 1 1104 1105 double q = det(pB.north() - pC.north(), pB.east() - pC.east(), pD.north() - pC.north(), pD.east() - pC.east()) / u; 1106 EastNorth intersection = new EastNorth( 1107 pB.east() + q * (pA.east() - pB.east()), 1108 pB.north() + q * (pA.north() - pB.north())); 1109 1110 1111 // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise 1112 // fall through to default action. 1113 // (for semi-parallel lines, intersection might be miles away!) 1114 MapFrame map = MainApplication.getMap(); 1115 if (map.mapView.getPoint2D(n).distance(map.mapView.getPoint2D(intersection)) < SNAP_TO_INTERSECTION_THRESHOLD.get()) { 1116 n.setEastNorth(intersection); 1117 return; 1118 } 1119 1120 adjustNodeDefault(segs, n); 1121 } 1122 1123 private static void adjustNodeDefault(Collection<Pair<Node, Node>> segs, Node n) { 1124 EastNorth p = n.getEastNorth(); 1125 Pair<Node, Node> seg = segs.iterator().next(); 1126 EastNorth pA = seg.a.getEastNorth(); 1127 EastNorth pB = seg.b.getEastNorth(); 1128 double a = p.distanceSq(pB); 1129 double b = p.distanceSq(pA); 1130 double c = pA.distanceSq(pB); 1131 double q = (a - b + c) / (2*c); 1132 n.setEastNorth(new EastNorth(pB.east() + q * (pA.east() - pB.east()), pB.north() + q * (pA.north() - pB.north()))); 1133 } 1134 1135 // helper for adjustNode 1136 static double det(double a, double b, double c, double d) { 1137 return a * d - b * c; 1138 } 1139 1140 private void tryToMoveNodeOnIntersection(List<WaySegment> wss, Node n) { 1141 if (wss.isEmpty()) 1142 return; 1143 WaySegment ws = wss.get(0); 1144 EastNorth p1 = ws.getFirstNode().getEastNorth(); 1145 EastNorth p2 = ws.getSecondNode().getEastNorth(); 1146 if (snapHelper.dir2 != null && getCurrentBaseNode() != null) { 1147 EastNorth xPoint = Geometry.getSegmentSegmentIntersection(p1, p2, snapHelper.dir2, 1148 getCurrentBaseNode().getEastNorth()); 1149 if (xPoint != null) { 1150 n.setEastNorth(xPoint); 1151 } 1152 } 1153 } 1154 1155 /** 1156 * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted 1157 * (if feature enabled). Also sets the target cursor if appropriate. It adds the to-be- 1158 * highlighted primitives to newHighlights but does not actually highlight them. This work is 1159 * done in redrawIfRequired. This means, calling addHighlighting() without redrawIfRequired() 1160 * will leave the data in an inconsistent state. 1161 * 1162 * The status bar derives its information from oldHighlights, so in order to update the status 1163 * bar both addHighlighting() and repaintIfRequired() are needed, since former fills newHighlights 1164 * and latter processes them into oldHighlights. 1165 * @param event event, can be null 1166 */ 1167 private void addHighlighting(Object event) { 1168 newHighlights = new HashSet<>(); 1169 MapView mapView = MainApplication.getMap().mapView; 1170 1171 // if ctrl key is held ("no join"), don't highlight anything 1172 if (ctrl) { 1173 mapView.setNewCursor(cursor, this); 1174 redrawIfRequired(event); 1175 return; 1176 } 1177 1178 // This happens when nothing is selected, but we still want to highlight the "target node" 1179 DataSet ds = getLayerManager().getEditDataSet(); 1180 if (mouseOnExistingNode == null && mousePos != null && ds != null && ds.selectionEmpty()) { 1181 mouseOnExistingNode = mapView.getNearestNode(mousePos, OsmPrimitive::isSelectable); 1182 } 1183 1184 if (mouseOnExistingNode != null) { 1185 mapView.setNewCursor(cursorJoinNode, this); 1186 newHighlights.add(mouseOnExistingNode); 1187 redrawIfRequired(event); 1188 return; 1189 } 1190 1191 // Insert the node into all the nearby way segments 1192 if (mouseOnExistingWays.isEmpty()) { 1193 mapView.setNewCursor(cursor, this); 1194 redrawIfRequired(event); 1195 return; 1196 } 1197 1198 mapView.setNewCursor(cursorJoinWay, this); 1199 newHighlights.addAll(mouseOnExistingWays); 1200 redrawIfRequired(event); 1201 } 1202 1203 /** 1204 * Removes target highlighting from primitives. Issues repaint if required. 1205 * @param event event, can be null 1206 * @return true if a repaint has been issued. 1207 */ 1208 private boolean removeHighlighting(Object event) { 1209 newHighlights = new HashSet<>(); 1210 return redrawIfRequired(event); 1211 } 1212 1213 @Override 1214 public synchronized void paint(Graphics2D g, MapView mv, Bounds box) { 1215 // sanity checks 1216 MapView mapView = MainApplication.getMap().mapView; 1217 if (mapView == null || mousePos == null 1218 // don't draw line if we don't know where from or where to 1219 || currentMouseEastNorth == null || getCurrentBaseNode() == null 1220 // don't draw line if mouse is outside window 1221 || !mapView.getState().getForView(mousePos.getX(), mousePos.getY()).isInView()) 1222 return; 1223 1224 Graphics2D g2 = g; 1225 snapHelper.drawIfNeeded(g2, mv.getState()); 1226 if (!DRAW_HELPER_LINE.get() || wayIsFinished || shift) 1227 return; 1228 1229 if (!snapHelper.isActive()) { 1230 g2.setColor(RUBBER_LINE_COLOR.get()); 1231 g2.setStroke(RUBBER_LINE_STROKE.get()); 1232 paintConstructionGeometry(mv, g2); 1233 } else if (DRAW_CONSTRUCTION_GEOMETRY.get()) { 1234 // else use color and stoke from snapHelper.draw 1235 paintConstructionGeometry(mv, g2); 1236 } 1237 } 1238 1239 private void paintConstructionGeometry(MapView mv, Graphics2D g2) { 1240 MapPath2D b = new MapPath2D(); 1241 MapViewPoint p1 = mv.getState().getPointFor(getCurrentBaseNode()); 1242 MapViewPoint p2 = mv.getState().getPointFor(currentMouseEastNorth); 1243 1244 b.moveTo(p1); 1245 b.lineTo(p2); 1246 1247 // if alt key is held ("start new way"), draw a little perpendicular line 1248 if (alt) { 1249 START_WAY_INDICATOR.paintArrowAt(b, p1, p2); 1250 } 1251 1252 g2.draw(b); 1253 g2.setStroke(BASIC_STROKE); 1254 } 1255 1256 @Override 1257 public String getModeHelpText() { 1258 StringBuilder rv; 1259 /* 1260 * No modifiers: all (Connect, Node Re-Use, Auto-Weld) 1261 * CTRL: disables node re-use, auto-weld 1262 * Shift: do not make connection 1263 * ALT: make connection but start new way in doing so 1264 */ 1265 1266 /* 1267 * Status line text generation is split into two parts to keep it maintainable. 1268 * First part looks at what will happen to the new node inserted on click and 1269 * the second part will look if a connection is made or not. 1270 * 1271 * Note that this help text is not absolutely accurate as it doesn't catch any special 1272 * cases (e.g. when preventing <---> ways). The only special that it catches is when 1273 * a way is about to be finished. 1274 * 1275 * First check what happens to the new node. 1276 */ 1277 1278 // oldHighlights stores the current highlights. If this 1279 // list is empty we can assume that we won't do any joins 1280 if (ctrl || oldHighlights.isEmpty()) { 1281 rv = new StringBuilder(tr("Create new node.")); 1282 } else { 1283 // oldHighlights may store a node or way, check if it's a node 1284 OsmPrimitive x = oldHighlights.iterator().next(); 1285 if (x instanceof Node) { 1286 rv = new StringBuilder(tr("Select node under cursor.")); 1287 } else { 1288 rv = new StringBuilder(trn("Insert new node into way.", "Insert new node into {0} ways.", 1289 oldHighlights.size(), oldHighlights.size())); 1290 } 1291 } 1292 1293 /* 1294 * Check whether a connection will be made 1295 */ 1296 if (!wayIsFinished && getCurrentBaseNode() != null) { 1297 if (alt) { 1298 rv.append(' ').append(tr("Start new way from last node.")); 1299 } else { 1300 rv.append(' ').append(tr("Continue way from last node.")); 1301 } 1302 if (snapHelper.isSnapOn()) { 1303 rv.append(' ').append(tr("Angle snapping active.")); 1304 } 1305 } 1306 1307 Node n = mouseOnExistingNode; 1308 DataSet ds = getLayerManager().getEditDataSet(); 1309 /* 1310 * Handle special case: Highlighted node == selected node => finish drawing 1311 */ 1312 if (n != null && ds != null && ds.getSelectedNodes().contains(n)) { 1313 if (wayIsFinished) { 1314 rv = new StringBuilder(tr("Select node under cursor.")); 1315 } else { 1316 rv = new StringBuilder(tr("Finish drawing.")); 1317 } 1318 } 1319 1320 /* 1321 * Handle special case: Self-Overlapping or closing way 1322 */ 1323 if (ds != null && !ds.getSelectedWays().isEmpty() && !wayIsFinished && !alt) { 1324 Way w = ds.getSelectedWays().iterator().next(); 1325 for (Node m : w.getNodes()) { 1326 if (m.equals(mouseOnExistingNode) || mouseOnExistingWays.contains(w)) { 1327 rv.append(' ').append(tr("Finish drawing.")); 1328 break; 1329 } 1330 } 1331 } 1332 return rv.toString(); 1333 } 1334 1335 /** 1336 * Get selected primitives, while draw action is in progress. 1337 * 1338 * While drawing a way, technically the last node is selected. 1339 * This is inconvenient when the user tries to add/edit tags to the way. 1340 * For this case, this method returns the current way as selection, 1341 * to work around this issue. 1342 * Otherwise the normal selection of the current data layer is returned. 1343 * @return selected primitives, while draw action is in progress 1344 */ 1345 public Collection<OsmPrimitive> getInProgressSelection() { 1346 DataSet ds = getLayerManager().getEditDataSet(); 1347 if (ds == null) return Collections.emptyList(); 1348 if (getCurrentBaseNode() != null && !ds.selectionEmpty()) { 1349 Way continueFrom = getWayForNode(getCurrentBaseNode()); 1350 if (continueFrom != null) 1351 return Collections.<OsmPrimitive>singleton(continueFrom); 1352 } 1353 return ds.getSelected(); 1354 } 1355 1356 @Override 1357 public boolean layerIsSupported(Layer l) { 1358 return isEditableDataLayer(l); 1359 } 1360 1361 @Override 1362 protected void updateEnabledState() { 1363 setEnabled(getLayerManager().getEditLayer() != null); 1364 } 1365 1366 @Override 1367 public void destroy() { 1368 super.destroy(); 1369 finishDrawing(); 1370 MainApplication.getMenu().editMenu.remove(snapCheckboxMenuItem); 1371 snapChangeAction.destroy(); 1372 } 1373 1374 /** 1375 * Undo the last command. Binded by default to backspace key. 1376 */ 1377 public class BackSpaceAction extends AbstractAction { 1378 1379 @Override 1380 public void actionPerformed(ActionEvent e) { 1381 UndoRedoHandler.getInstance().undo(); 1382 Command lastCmd = UndoRedoHandler.getInstance().getLastCommand(); 1383 if (lastCmd == null) return; 1384 Node n = null; 1385 for (OsmPrimitive p: lastCmd.getParticipatingPrimitives()) { 1386 if (p instanceof Node) { 1387 if (n == null) { 1388 n = (Node) p; // found one node 1389 wayIsFinished = false; 1390 } else { 1391 // if more than 1 node were affected by previous command, 1392 // we have no way to continue, so we forget about found node 1393 n = null; 1394 break; 1395 } 1396 } 1397 } 1398 // select last added node - maybe we will continue drawing from it 1399 if (n != null) { 1400 addSelection(getLayerManager().getEditDataSet(), n); 1401 } 1402 } 1403 } 1404 1405 private class SnapChangeAction extends JosmAction { 1406 /** 1407 * Constructs a new {@code SnapChangeAction}. 1408 */ 1409 SnapChangeAction() { 1410 super(tr("Angle snapping"), /* ICON() */ "anglesnap", 1411 tr("Switch angle snapping mode while drawing"), null, false); 1412 setHelpId(ht("/Action/Draw/AngleSnap")); 1413 } 1414 1415 @Override 1416 public void actionPerformed(ActionEvent e) { 1417 if (snapHelper != null) { 1418 snapHelper.toggleSnapping(); 1419 } 1420 } 1421 1422 @Override 1423 protected void updateEnabledState() { 1424 MapFrame map = MainApplication.getMap(); 1425 setEnabled(map != null && map.mapMode instanceof DrawAction); 1426 } 1427 } 1428}