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