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