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