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