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