001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.mapmode; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007 008import java.awt.BasicStroke; 009import java.awt.Color; 010import java.awt.Cursor; 011import java.awt.Graphics2D; 012import java.awt.Point; 013import java.awt.Rectangle; 014import java.awt.Stroke; 015import java.awt.event.ActionEvent; 016import java.awt.event.KeyEvent; 017import java.awt.event.MouseEvent; 018import java.awt.geom.AffineTransform; 019import java.awt.geom.GeneralPath; 020import java.awt.geom.Line2D; 021import java.awt.geom.NoninvertibleTransformException; 022import java.awt.geom.Point2D; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.LinkedList; 026import java.util.List; 027 028import javax.swing.JCheckBoxMenuItem; 029import javax.swing.JMenuItem; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.actions.JosmAction; 033import org.openstreetmap.josm.actions.MergeNodesAction; 034import org.openstreetmap.josm.command.AddCommand; 035import org.openstreetmap.josm.command.ChangeCommand; 036import org.openstreetmap.josm.command.Command; 037import org.openstreetmap.josm.command.MoveCommand; 038import org.openstreetmap.josm.command.SequenceCommand; 039import org.openstreetmap.josm.data.Bounds; 040import org.openstreetmap.josm.data.coor.EastNorth; 041import org.openstreetmap.josm.data.osm.DataIntegrityProblemException; 042import org.openstreetmap.josm.data.osm.DataSet; 043import org.openstreetmap.josm.data.osm.Node; 044import org.openstreetmap.josm.data.osm.OsmPrimitive; 045import org.openstreetmap.josm.data.osm.Way; 046import org.openstreetmap.josm.data.osm.WaySegment; 047import org.openstreetmap.josm.data.preferences.NamedColorProperty; 048import org.openstreetmap.josm.gui.MainApplication; 049import org.openstreetmap.josm.gui.MainMenu; 050import org.openstreetmap.josm.gui.MapFrame; 051import org.openstreetmap.josm.gui.MapView; 052import org.openstreetmap.josm.gui.draw.MapViewPath; 053import org.openstreetmap.josm.gui.draw.SymbolShape; 054import org.openstreetmap.josm.gui.layer.Layer; 055import org.openstreetmap.josm.gui.layer.MapViewPaintable; 056import org.openstreetmap.josm.gui.util.GuiHelper; 057import org.openstreetmap.josm.gui.util.KeyPressReleaseListener; 058import org.openstreetmap.josm.gui.util.ModifierExListener; 059import org.openstreetmap.josm.spi.preferences.Config; 060import org.openstreetmap.josm.tools.Geometry; 061import org.openstreetmap.josm.tools.ImageProvider; 062import org.openstreetmap.josm.tools.Logging; 063import org.openstreetmap.josm.tools.Shortcut; 064 065/** 066 * Makes a rectangle from a line, or modifies a rectangle. 067 */ 068public class ExtrudeAction extends MapMode implements MapViewPaintable, KeyPressReleaseListener, ModifierExListener { 069 070 enum Mode { extrude, translate, select, create_new, translate_node } 071 072 private Mode mode = Mode.select; 073 074 /** 075 * If {@code true}, when extruding create new node(s) even if segments are parallel. 076 */ 077 private boolean alwaysCreateNodes; 078 private boolean nodeDragWithoutCtrl; 079 080 private long mouseDownTime; 081 private transient WaySegment selectedSegment; 082 private transient Node selectedNode; 083 private Color mainColor; 084 private transient Stroke mainStroke; 085 086 /** settings value whether shared nodes should be ignored or not */ 087 private boolean ignoreSharedNodes; 088 089 private boolean keepSegmentDirection; 090 091 /** 092 * drawing settings for helper lines 093 */ 094 private Color helperColor; 095 private transient Stroke helperStrokeDash; 096 private transient Stroke helperStrokeRA; 097 098 private transient Stroke oldLineStroke; 099 private double symbolSize; 100 /** 101 * Possible directions to move to. 102 */ 103 private transient List<ReferenceSegment> possibleMoveDirections; 104 105 106 /** 107 * Collection of nodes that is moved 108 */ 109 private transient List<Node> movingNodeList; 110 111 /** 112 * The direction that is currently active. 113 */ 114 private transient ReferenceSegment activeMoveDirection; 115 116 /** 117 * The position of the mouse cursor when the drag action was initiated. 118 */ 119 private Point initialMousePos; 120 /** 121 * The time which needs to pass between click and release before something 122 * counts as a move, in milliseconds 123 */ 124 private int initialMoveDelay = 200; 125 /** 126 * The minimal shift of mouse (in pixels) befire something counts as move 127 */ 128 private int initialMoveThreshold = 1; 129 130 /** 131 * The initial EastNorths of node1 and node2 132 */ 133 private EastNorth initialN1en; 134 private EastNorth initialN2en; 135 /** 136 * The new EastNorths of node1 and node2 137 */ 138 private EastNorth newN1en; 139 private EastNorth newN2en; 140 141 /** 142 * the command that performed last move. 143 */ 144 private transient MoveCommand moveCommand; 145 /** 146 * The command used for dual alignment movement. 147 * Needs to be separate, due to two nodes moving in different directions. 148 */ 149 private transient MoveCommand moveCommand2; 150 151 /** The cursor for the 'create_new' mode. */ 152 private final Cursor cursorCreateNew; 153 154 /** The cursor for the 'translate' mode. */ 155 private final Cursor cursorTranslate; 156 157 /** The cursor for the 'alwaysCreateNodes' submode. */ 158 private final Cursor cursorCreateNodes; 159 160 private static class ReferenceSegment { 161 public final EastNorth en; 162 public final EastNorth p1; 163 public final EastNorth p2; 164 public final boolean perpendicular; 165 166 ReferenceSegment(EastNorth en, EastNorth p1, EastNorth p2, boolean perpendicular) { 167 this.en = en; 168 this.p1 = p1; 169 this.p2 = p2; 170 this.perpendicular = perpendicular; 171 } 172 173 @Override 174 public String toString() { 175 return "ReferenceSegment[en=" + en + ", p1=" + p1 + ", p2=" + p2 + ", perp=" + perpendicular + ']'; 176 } 177 } 178 179 // Dual alignment mode stuff 180 /** {@code true}, if dual alignment mode is enabled. User wants following extrude to be dual aligned. */ 181 private boolean dualAlignEnabled; 182 /** {@code true}, if dual alignment is active. User is dragging the mouse, required conditions are met. 183 * Treat {@link #mode} (extrude/translate/create_new) as dual aligned. */ 184 private boolean dualAlignActive; 185 /** Dual alignment reference segments */ 186 private transient ReferenceSegment dualAlignSegment1, dualAlignSegment2; 187 /** {@code true}, if new segment was collapsed */ 188 private boolean dualAlignSegmentCollapsed; 189 // Dual alignment UI stuff 190 private final DualAlignChangeAction dualAlignChangeAction; 191 private final JCheckBoxMenuItem dualAlignCheckboxMenuItem; 192 private final transient Shortcut dualAlignShortcut; 193 private boolean useRepeatedShortcut; 194 private boolean ignoreNextKeyRelease; 195 196 private class DualAlignChangeAction extends JosmAction { 197 DualAlignChangeAction() { 198 super(tr("Dual alignment"), /* ICON() */ "mapmode/extrude/dualalign", 199 tr("Switch dual alignment mode while extruding"), null, false); 200 putValue("help", ht("/Action/Extrude#DualAlign")); 201 } 202 203 @Override 204 public void actionPerformed(ActionEvent e) { 205 toggleDualAlign(); 206 } 207 208 @Override 209 protected void updateEnabledState() { 210 MapFrame map = MainApplication.getMap(); 211 setEnabled(map != null && map.mapMode instanceof ExtrudeAction); 212 } 213 } 214 215 /** 216 * Creates a new ExtrudeAction 217 * @since 11713 218 */ 219 public ExtrudeAction() { 220 super(tr("Extrude"), /* ICON(mapmode/) */ "extrude/extrude", tr("Create areas"), 221 Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT), 222 ImageProvider.getCursor("normal", "rectangle")); 223 putValue("help", ht("/Action/Extrude")); 224 cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus"); 225 cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move"); 226 cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall"); 227 228 dualAlignEnabled = false; 229 dualAlignChangeAction = new DualAlignChangeAction(); 230 dualAlignCheckboxMenuItem = addDualAlignMenuItem(); 231 dualAlignCheckboxMenuItem.getAction().setEnabled(false); 232 dualAlignCheckboxMenuItem.setState(dualAlignEnabled); 233 dualAlignShortcut = Shortcut.registerShortcut("mapmode:extrudedualalign", 234 tr("Mode: {0}", tr("Extrude Dual alignment")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE); 235 readPreferences(); // to show prefernces in table before entering the mode 236 } 237 238 @Override 239 public void destroy() { 240 super.destroy(); 241 dualAlignChangeAction.destroy(); 242 } 243 244 private JCheckBoxMenuItem addDualAlignMenuItem() { 245 int n = MainApplication.getMenu().editMenu.getItemCount(); 246 for (int i = n-1; i > 0; i--) { 247 JMenuItem item = MainApplication.getMenu().editMenu.getItem(i); 248 if (item != null && item.getAction() != null && item.getAction() instanceof DualAlignChangeAction) { 249 MainApplication.getMenu().editMenu.remove(i); 250 } 251 } 252 return MainMenu.addWithCheckbox(MainApplication.getMenu().editMenu, dualAlignChangeAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE); 253 } 254 255 // ------------------------------------------------------------------------- 256 // Mode methods 257 // ------------------------------------------------------------------------- 258 259 @Override 260 public String getModeHelpText() { 261 StringBuilder rv; 262 if (mode == Mode.select) { 263 rv = new StringBuilder(tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " + 264 "Alt-drag to create a new rectangle, double click to add a new node.")); 265 if (dualAlignEnabled) { 266 rv.append(' ').append(tr("Dual alignment active.")); 267 if (dualAlignSegmentCollapsed) 268 rv.append(' ').append(tr("Segment collapsed due to its direction reversing.")); 269 } 270 } else { 271 if (mode == Mode.translate) 272 rv = new StringBuilder(tr("Move a segment along its normal, then release the mouse button.")); 273 else if (mode == Mode.translate_node) 274 rv = new StringBuilder(tr("Move the node along one of the segments, then release the mouse button.")); 275 else if (mode == Mode.extrude || mode == Mode.create_new) 276 rv = new StringBuilder(tr("Draw a rectangle of the desired size, then release the mouse button.")); 277 else { 278 Logging.warn("Extrude: unknown mode " + mode); 279 rv = new StringBuilder(); 280 } 281 if (dualAlignActive) { 282 rv.append(' ').append(tr("Dual alignment active.")); 283 if (dualAlignSegmentCollapsed) { 284 rv.append(' ').append(tr("Segment collapsed due to its direction reversing.")); 285 } 286 } 287 } 288 return rv.toString(); 289 } 290 291 @Override 292 public boolean layerIsSupported(Layer l) { 293 return isEditableDataLayer(l); 294 } 295 296 @Override 297 public void enterMode() { 298 super.enterMode(); 299 MapFrame map = MainApplication.getMap(); 300 map.mapView.addMouseListener(this); 301 map.mapView.addMouseMotionListener(this); 302 ignoreNextKeyRelease = true; 303 map.keyDetector.addKeyListener(this); 304 map.keyDetector.addModifierExListener(this); 305 } 306 307 @Override 308 protected void readPreferences() { 309 initialMoveDelay = Config.getPref().getInt("edit.initial-move-delay", 200); 310 initialMoveThreshold = Config.getPref().getInt("extrude.initial-move-threshold", 1); 311 mainColor = new NamedColorProperty(marktr("Extrude: main line"), Color.RED).get(); 312 helperColor = new NamedColorProperty(marktr("Extrude: helper line"), Color.ORANGE).get(); 313 helperStrokeDash = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.stroke.helper-line", "1 4")); 314 helperStrokeRA = new BasicStroke(1); 315 symbolSize = Config.getPref().getDouble("extrude.angle-symbol-radius", 8); 316 nodeDragWithoutCtrl = Config.getPref().getBoolean("extrude.drag-nodes-without-ctrl", false); 317 oldLineStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.ctrl.stroke.old-line", "1")); 318 mainStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.stroke.main", "3")); 319 320 ignoreSharedNodes = Config.getPref().getBoolean("extrude.ignore-shared-nodes", true); 321 dualAlignCheckboxMenuItem.getAction().setEnabled(true); 322 useRepeatedShortcut = Config.getPref().getBoolean("extrude.dualalign.toggleOnRepeatedX", true); 323 keepSegmentDirection = Config.getPref().getBoolean("extrude.dualalign.keep-segment-direction", true); 324 } 325 326 @Override 327 public void exitMode() { 328 MapFrame map = MainApplication.getMap(); 329 map.mapView.removeMouseListener(this); 330 map.mapView.removeMouseMotionListener(this); 331 map.mapView.removeTemporaryLayer(this); 332 dualAlignCheckboxMenuItem.getAction().setEnabled(false); 333 map.keyDetector.removeKeyListener(this); 334 map.keyDetector.removeModifierExListener(this); 335 super.exitMode(); 336 } 337 338 // ------------------------------------------------------------------------- 339 // Event handlers 340 // ------------------------------------------------------------------------- 341 342 /** 343 * This method is called to indicate different modes via cursor when the Alt/Ctrl/Shift modifier is pressed, 344 */ 345 @Override 346 public void modifiersExChanged(int modifiers) { 347 MapFrame map = MainApplication.getMap(); 348 if (!MainApplication.isDisplayingMapView() || !map.mapView.isActiveLayerDrawable()) 349 return; 350 updateKeyModifiersEx(modifiers); 351 if (mode == Mode.select) { 352 map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this); 353 } 354 } 355 356 @Override 357 public void doKeyPressed(KeyEvent e) { 358 // Do nothing 359 } 360 361 @Override 362 public void doKeyReleased(KeyEvent e) { 363 if (!dualAlignShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e))) 364 return; 365 if (ignoreNextKeyRelease) { 366 ignoreNextKeyRelease = false; 367 } else { 368 toggleDualAlign(); 369 } 370 } 371 372 /** 373 * Toggles dual alignment mode. 374 */ 375 private void toggleDualAlign() { 376 dualAlignEnabled = !dualAlignEnabled; 377 dualAlignCheckboxMenuItem.setState(dualAlignEnabled); 378 updateStatusLine(); 379 } 380 381 /** 382 * If the left mouse button is pressed over a segment or a node, switches 383 * to appropriate {@link #mode}, depending on Ctrl/Alt/Shift modifiers and 384 * {@link #dualAlignEnabled}. 385 * @param e current mouse event 386 */ 387 @Override 388 public void mousePressed(MouseEvent e) { 389 MapFrame map = MainApplication.getMap(); 390 if (!map.mapView.isActiveLayerVisible()) 391 return; 392 if (!(Boolean) this.getValue("active")) 393 return; 394 if (e.getButton() != MouseEvent.BUTTON1) 395 return; 396 397 requestFocusInMapView(); 398 updateKeyModifiers(e); 399 400 selectedNode = map.mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable); 401 selectedSegment = map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable); 402 403 // If nothing gets caught, stay in select mode 404 if (selectedSegment == null && selectedNode == null) return; 405 406 if (selectedNode != null) { 407 if (ctrl || nodeDragWithoutCtrl) { 408 movingNodeList = new ArrayList<>(); 409 movingNodeList.add(selectedNode); 410 calculatePossibleDirectionsByNode(); 411 if (possibleMoveDirections.isEmpty()) { 412 // if no directions fould, do not enter dragging mode 413 return; 414 } 415 mode = Mode.translate_node; 416 dualAlignActive = false; 417 } 418 } else { 419 // Otherwise switch to another mode 420 if (dualAlignEnabled && checkDualAlignConditions()) { 421 dualAlignActive = true; 422 calculatePossibleDirectionsForDualAlign(); 423 dualAlignSegmentCollapsed = false; 424 } else { 425 dualAlignActive = false; 426 calculatePossibleDirectionsBySegment(); 427 } 428 if (ctrl) { 429 mode = Mode.translate; 430 movingNodeList = new ArrayList<>(); 431 movingNodeList.add(selectedSegment.getFirstNode()); 432 movingNodeList.add(selectedSegment.getSecondNode()); 433 } else if (alt) { 434 mode = Mode.create_new; 435 // create a new segment and then select and extrude the new segment 436 getLayerManager().getEditDataSet().setSelected(selectedSegment.way); 437 alwaysCreateNodes = true; 438 } else { 439 mode = Mode.extrude; 440 getLayerManager().getEditDataSet().setSelected(selectedSegment.way); 441 alwaysCreateNodes = shift; 442 } 443 } 444 445 // Signifies that nothing has happened yet 446 newN1en = null; 447 newN2en = null; 448 moveCommand = null; 449 moveCommand2 = null; 450 451 map.mapView.addTemporaryLayer(this); 452 453 updateStatusLine(); 454 map.mapView.repaint(); 455 456 // Make note of time pressed 457 mouseDownTime = System.currentTimeMillis(); 458 459 // Make note of mouse position 460 initialMousePos = e.getPoint(); 461 } 462 463 /** 464 * Performs action depending on what {@link #mode} we're in. 465 * @param e current mouse event 466 */ 467 @Override 468 public void mouseDragged(MouseEvent e) { 469 MapView mapView = MainApplication.getMap().mapView; 470 if (!mapView.isActiveLayerVisible()) 471 return; 472 473 // do not count anything as a drag if it lasts less than 100 milliseconds. 474 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) 475 return; 476 477 if (mode == Mode.select) { 478 // Just sit tight and wait for mouse to be released. 479 } else { 480 //move, create new and extrude mode - move the selected segment 481 482 EastNorth mouseEn = mapView.getEastNorth(e.getPoint().x, e.getPoint().y); 483 EastNorth bestMovement = calculateBestMovementAndNewNodes(mouseEn); 484 485 mapView.setNewCursor(Cursor.MOVE_CURSOR, this); 486 487 if (dualAlignActive) { 488 if (mode == Mode.extrude || mode == Mode.create_new) { 489 // nothing here 490 } else if (mode == Mode.translate) { 491 EastNorth movement1 = newN1en.subtract(initialN1en); 492 EastNorth movement2 = newN2en.subtract(initialN2en); 493 // move nodes to new position 494 if (moveCommand == null || moveCommand2 == null) { 495 // make a new move commands 496 moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY()); 497 moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY()); 498 Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2); 499 MainApplication.undoRedo.add(c); 500 } else { 501 // reuse existing move commands 502 moveCommand.moveAgainTo(movement1.getX(), movement1.getY()); 503 moveCommand2.moveAgainTo(movement2.getX(), movement2.getY()); 504 } 505 } 506 } else if (bestMovement != null) { 507 if (mode == Mode.extrude || mode == Mode.create_new) { 508 //nothing here 509 } else if (mode == Mode.translate_node || mode == Mode.translate) { 510 //move nodes to new position 511 if (moveCommand == null) { 512 //make a new move command 513 moveCommand = new MoveCommand(new ArrayList<OsmPrimitive>(movingNodeList), bestMovement); 514 MainApplication.undoRedo.add(moveCommand); 515 } else { 516 //reuse existing move command 517 moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY()); 518 } 519 } 520 } 521 522 mapView.repaint(); 523 } 524 } 525 526 /** 527 * Does anything that needs to be done, then switches back to select mode. 528 * @param e current mouse event 529 */ 530 @Override 531 public void mouseReleased(MouseEvent e) { 532 533 MapView mapView = MainApplication.getMap().mapView; 534 if (!mapView.isActiveLayerVisible()) 535 return; 536 537 if (mode == Mode.select) { 538 // Nothing to be done 539 } else { 540 if (mode == Mode.create_new) { 541 if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null) { 542 createNewRectangle(); 543 } 544 } else if (mode == Mode.extrude) { 545 if (e.getClickCount() == 2 && e.getPoint().equals(initialMousePos)) { 546 // double click adds a new node 547 addNewNode(e); 548 } else if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null && selectedSegment != null) { 549 try { 550 // main extrusion commands 551 performExtrusion(); 552 } catch (DataIntegrityProblemException ex) { 553 // Can occur if calling undo while extruding, see #12870 554 Logging.error(ex); 555 } 556 } 557 } else if (mode == Mode.translate || mode == Mode.translate_node) { 558 //Commit translate 559 //the move command is already committed in mouseDragged 560 joinNodesIfCollapsed(movingNodeList); 561 } 562 563 updateKeyModifiers(e); 564 // Switch back into select mode 565 mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this); 566 mapView.removeTemporaryLayer(this); 567 selectedSegment = null; 568 moveCommand = null; 569 mode = Mode.select; 570 dualAlignSegmentCollapsed = false; 571 updateStatusLine(); 572 mapView.repaint(); 573 } 574 } 575 576 // ------------------------------------------------------------------------- 577 // Custom methods 578 // ------------------------------------------------------------------------- 579 580 /** 581 * Inserts node into nearby segment. 582 * @param e current mouse point 583 */ 584 private static void addNewNode(MouseEvent e) { 585 // Should maybe do the same as in DrawAction and fetch all nearby segments? 586 MapView mapView = MainApplication.getMap().mapView; 587 WaySegment ws = mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable); 588 if (ws != null) { 589 Node n = new Node(mapView.getLatLon(e.getX(), e.getY())); 590 EastNorth a = ws.getFirstNode().getEastNorth(); 591 EastNorth b = ws.getSecondNode().getEastNorth(); 592 n.setEastNorth(Geometry.closestPointToSegment(a, b, n.getEastNorth())); 593 Way wnew = new Way(ws.way); 594 wnew.addNode(ws.lowerIndex+1, n); 595 DataSet ds = ws.way.getDataSet(); 596 MainApplication.undoRedo.add(new SequenceCommand(tr("Add a new node to an existing way"), 597 new AddCommand(ds, n), new ChangeCommand(ds, ws.way, wnew))); 598 } 599 } 600 601 /** 602 * Creates a new way that shares segment with selected way. 603 */ 604 private void createNewRectangle() { 605 if (selectedSegment == null) return; 606 DataSet ds = getLayerManager().getEditDataSet(); 607 // create a new rectangle 608 Collection<Command> cmds = new LinkedList<>(); 609 Node third = new Node(newN2en); 610 Node fourth = new Node(newN1en); 611 Way wnew = new Way(); 612 wnew.addNode(selectedSegment.getFirstNode()); 613 wnew.addNode(selectedSegment.getSecondNode()); 614 wnew.addNode(third); 615 if (!dualAlignSegmentCollapsed) { 616 // rectangle can degrade to triangle for dual alignment after collapsing 617 wnew.addNode(fourth); 618 } 619 // ... and close the way 620 wnew.addNode(selectedSegment.getFirstNode()); 621 // undo support 622 cmds.add(new AddCommand(ds, third)); 623 if (!dualAlignSegmentCollapsed) { 624 cmds.add(new AddCommand(ds, fourth)); 625 } 626 cmds.add(new AddCommand(ds, wnew)); 627 Command c = new SequenceCommand(tr("Extrude Way"), cmds); 628 MainApplication.undoRedo.add(c); 629 ds.setSelected(wnew); 630 } 631 632 /** 633 * Does actual extrusion of {@link #selectedSegment}. 634 * Uses {@link #initialN1en}, {@link #initialN2en} saved in calculatePossibleDirections* call 635 * Uses {@link #newN1en}, {@link #newN2en} calculated by {@link #calculateBestMovementAndNewNodes} 636 */ 637 private void performExtrusion() { 638 DataSet ds = getLayerManager().getEditDataSet(); 639 // create extrusion 640 Collection<Command> cmds = new LinkedList<>(); 641 Way wnew = new Way(selectedSegment.way); 642 boolean wayWasModified = false; 643 boolean wayWasSingleSegment = wnew.getNodesCount() == 2; 644 int insertionPoint = selectedSegment.lowerIndex + 1; 645 646 //find if the new points overlap existing segments (in case of 90 degree angles) 647 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 648 boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en); 649 // segmentAngleZero marks subset of nodeOverlapsSegment. 650 // nodeOverlapsSegment is true if angle between segments is 0 or PI, segmentAngleZero only if angle is 0 651 boolean segmentAngleZero = prevNode != null && Math.abs(Geometry.getCornerAngle(prevNode.getEastNorth(), initialN1en, newN1en)) < 1e-5; 652 boolean hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way); 653 List<Node> changedNodes = new ArrayList<>(); 654 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { 655 //move existing node 656 Node n1Old = selectedSegment.getFirstNode(); 657 cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en))); 658 changedNodes.add(n1Old); 659 } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) { 660 // replace shared node with new one 661 Node n1Old = selectedSegment.getFirstNode(); 662 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en)); 663 wnew.addNode(insertionPoint, n1New); 664 wnew.removeNode(n1Old); 665 wayWasModified = true; 666 cmds.add(new AddCommand(ds, n1New)); 667 changedNodes.add(n1New); 668 } else { 669 //introduce new node 670 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en)); 671 wnew.addNode(insertionPoint, n1New); 672 wayWasModified = true; 673 insertionPoint++; 674 cmds.add(new AddCommand(ds, n1New)); 675 changedNodes.add(n1New); 676 } 677 678 //find if the new points overlap existing segments (in case of 90 degree angles) 679 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 680 nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en); 681 segmentAngleZero = nextNode != null && Math.abs(Geometry.getCornerAngle(nextNode.getEastNorth(), initialN2en, newN2en)) < 1e-5; 682 hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way); 683 684 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { 685 //move existing node 686 Node n2Old = selectedSegment.getSecondNode(); 687 cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en))); 688 changedNodes.add(n2Old); 689 } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) { 690 // replace shared node with new one 691 Node n2Old = selectedSegment.getSecondNode(); 692 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en)); 693 wnew.addNode(insertionPoint, n2New); 694 wnew.removeNode(n2Old); 695 wayWasModified = true; 696 cmds.add(new AddCommand(ds, n2New)); 697 changedNodes.add(n2New); 698 } else { 699 //introduce new node 700 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en)); 701 wnew.addNode(insertionPoint, n2New); 702 wayWasModified = true; 703 cmds.add(new AddCommand(ds, n2New)); 704 changedNodes.add(n2New); 705 } 706 707 //the way was a single segment, close the way 708 if (wayWasSingleSegment) { 709 wnew.addNode(selectedSegment.getFirstNode()); 710 wayWasModified = true; 711 } 712 if (wayWasModified) { 713 // we only need to change the way if its node list was really modified 714 cmds.add(new ChangeCommand(selectedSegment.way, wnew)); 715 } 716 Command c = new SequenceCommand(tr("Extrude Way"), cmds); 717 MainApplication.undoRedo.add(c); 718 joinNodesIfCollapsed(changedNodes); 719 } 720 721 private void joinNodesIfCollapsed(List<Node> changedNodes) { 722 if (!dualAlignActive || newN1en == null || newN2en == null) return; 723 if (newN1en.distance(newN2en) > 1e-6) return; 724 // If the dual alignment moved two nodes to the same point, merge them 725 Node targetNode = MergeNodesAction.selectTargetNode(changedNodes); 726 Node locNode = MergeNodesAction.selectTargetLocationNode(changedNodes); 727 Command mergeCmd = MergeNodesAction.mergeNodes(changedNodes, targetNode, locNode); 728 if (mergeCmd != null) { 729 MainApplication.undoRedo.add(mergeCmd); 730 } else { 731 // undo extruding command itself 732 MainApplication.undoRedo.undo(); 733 } 734 } 735 736 /** 737 * This method tests if {@code node} has other ways apart from the given one. 738 * @param node node to test 739 * @param myWay way known to contain this node 740 * @return {@code true} if {@code node} belongs only to {@code myWay}, false if there are more ways. 741 */ 742 private static boolean hasNodeOtherWays(Node node, Way myWay) { 743 for (OsmPrimitive p : node.getReferrers()) { 744 if (p instanceof Way && p.isUsable() && p != myWay) 745 return true; 746 } 747 return false; 748 } 749 750 /** 751 * Determines best movement from {@link #initialMousePos} to current mouse position, 752 * choosing one of the directions from {@link #possibleMoveDirections}. 753 * @param mouseEn current mouse position 754 * @return movement vector 755 */ 756 private EastNorth calculateBestMovement(EastNorth mouseEn) { 757 758 EastNorth initialMouseEn = MainApplication.getMap().mapView.getEastNorth(initialMousePos.x, initialMousePos.y); 759 EastNorth mouseMovement = mouseEn.subtract(initialMouseEn); 760 761 double bestDistance = Double.POSITIVE_INFINITY; 762 EastNorth bestMovement = null; 763 activeMoveDirection = null; 764 765 //find the best movement direction and vector 766 for (ReferenceSegment direction : possibleMoveDirections) { 767 EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction.en, mouseEn); 768 if (movement == null) { 769 //if direction parallel to segment. 770 continue; 771 } 772 773 double distanceFromMouseMovement = movement.distance(mouseMovement); 774 if (bestDistance > distanceFromMouseMovement) { 775 bestDistance = distanceFromMouseMovement; 776 activeMoveDirection = direction; 777 bestMovement = movement; 778 } 779 } 780 return bestMovement; 781 } 782 783 /*** 784 * This method calculates offset amount by which to move the given segment 785 * perpendicularly for it to be in line with mouse position. 786 * @param segmentP1 segment's first point 787 * @param segmentP2 segment's second point 788 * @param moveDirection direction of movement 789 * @param targetPos mouse position 790 * @return offset amount of P1 and P2. 791 */ 792 private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection, 793 EastNorth targetPos) { 794 EastNorth intersectionPoint; 795 if (segmentP1.distanceSq(segmentP2) > 1e-7) { 796 intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, targetPos.add(moveDirection)); 797 } else { 798 intersectionPoint = Geometry.closestPointToLine(targetPos, targetPos.add(moveDirection), segmentP1); 799 } 800 801 if (intersectionPoint == null) 802 return null; 803 else 804 //return distance form base to target position 805 return targetPos.subtract(intersectionPoint); 806 } 807 808 /** 809 * Gathers possible move directions - perpendicular to the selected segment 810 * and parallel to neighboring segments. 811 */ 812 private void calculatePossibleDirectionsBySegment() { 813 // remember initial positions for segment nodes. 814 initialN1en = selectedSegment.getFirstNode().getEastNorth(); 815 initialN2en = selectedSegment.getSecondNode().getEastNorth(); 816 817 //add direction perpendicular to the selected segment 818 possibleMoveDirections = new ArrayList<>(); 819 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 820 initialN1en.getY() - initialN2en.getY(), 821 initialN2en.getX() - initialN1en.getX() 822 ), initialN1en, initialN2en, true)); 823 824 825 //add directions parallel to neighbor segments 826 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 827 if (prevNode != null) { 828 EastNorth en = prevNode.getEastNorth(); 829 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 830 initialN1en.getX() - en.getX(), 831 initialN1en.getY() - en.getY() 832 ), initialN1en, en, false)); 833 } 834 835 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 836 if (nextNode != null) { 837 EastNorth en = nextNode.getEastNorth(); 838 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 839 initialN2en.getX() - en.getX(), 840 initialN2en.getY() - en.getY() 841 ), initialN2en, en, false)); 842 } 843 } 844 845 /** 846 * Gathers possible move directions - along all adjacent segments. 847 */ 848 private void calculatePossibleDirectionsByNode() { 849 // remember initial positions for segment nodes. 850 initialN1en = selectedNode.getEastNorth(); 851 initialN2en = initialN1en; 852 possibleMoveDirections = new ArrayList<>(); 853 for (OsmPrimitive p: selectedNode.getReferrers()) { 854 if (p instanceof Way && p.isUsable()) { 855 for (Node neighbor: ((Way) p).getNeighbours(selectedNode)) { 856 EastNorth en = neighbor.getEastNorth(); 857 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 858 initialN1en.getX() - en.getX(), 859 initialN1en.getY() - en.getY() 860 ), initialN1en, en, false)); 861 } 862 } 863 } 864 } 865 866 /** 867 * Checks dual alignment conditions: 868 * 1. selected segment has both neighboring segments, 869 * 2. selected segment is not parallel with neighboring segments. 870 * @return {@code true} if dual alignment conditions are satisfied 871 */ 872 private boolean checkDualAlignConditions() { 873 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 874 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 875 if (prevNode == null || nextNode == null) { 876 return false; 877 } 878 879 EastNorth n1en = selectedSegment.getFirstNode().getEastNorth(); 880 EastNorth n2en = selectedSegment.getSecondNode().getEastNorth(); 881 if (n1en.distance(prevNode.getEastNorth()) < 1e-4 || 882 n2en.distance(nextNode.getEastNorth()) < 1e-4) { 883 return false; 884 } 885 886 boolean prevSegmentParallel = Geometry.segmentsParallel(n1en, prevNode.getEastNorth(), n1en, n2en); 887 boolean nextSegmentParallel = Geometry.segmentsParallel(n2en, nextNode.getEastNorth(), n1en, n2en); 888 return !prevSegmentParallel && !nextSegmentParallel; 889 } 890 891 /** 892 * Gathers possible move directions - perpendicular to the selected segment only. 893 * Neighboring segments go to {@link #dualAlignSegment1} and {@link #dualAlignSegment2}. 894 */ 895 private void calculatePossibleDirectionsForDualAlign() { 896 // remember initial positions for segment nodes. 897 initialN1en = selectedSegment.getFirstNode().getEastNorth(); 898 initialN2en = selectedSegment.getSecondNode().getEastNorth(); 899 900 // add direction perpendicular to the selected segment 901 possibleMoveDirections = new ArrayList<>(); 902 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 903 initialN1en.getY() - initialN2en.getY(), 904 initialN2en.getX() - initialN1en.getX() 905 ), initialN1en, initialN2en, true)); 906 907 // set neighboring segments 908 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 909 if (prevNode != null) { 910 EastNorth prevNodeEn = prevNode.getEastNorth(); 911 dualAlignSegment1 = new ReferenceSegment(new EastNorth( 912 initialN1en.getX() - prevNodeEn.getX(), 913 initialN1en.getY() - prevNodeEn.getY() 914 ), initialN1en, prevNodeEn, false); 915 } 916 917 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 918 if (nextNode != null) { 919 EastNorth nextNodeEn = nextNode.getEastNorth(); 920 dualAlignSegment2 = new ReferenceSegment(new EastNorth( 921 initialN2en.getX() - nextNodeEn.getX(), 922 initialN2en.getY() - nextNodeEn.getY() 923 ), initialN2en, nextNodeEn, false); 924 } 925 } 926 927 /** 928 * Calculate newN1en, newN2en best suitable for given mouse coordinates 929 * For dual align, calculates positions of new nodes, aligning them to neighboring segments. 930 * Elsewhere, just adds the vetor returned by calculateBestMovement to {@link #initialN1en}, {@link #initialN2en}. 931 * @param mouseEn mouse coordinates 932 * @return best movement vector 933 */ 934 private EastNorth calculateBestMovementAndNewNodes(EastNorth mouseEn) { 935 EastNorth bestMovement = calculateBestMovement(mouseEn); 936 EastNorth n1movedEn = initialN1en.add(bestMovement), n2movedEn; 937 938 // find out the movement distance, in metres 939 double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance( 940 Main.getProjection().eastNorth2latlon(n1movedEn)); 941 MainApplication.getMap().statusLine.setDist(distance); 942 updateStatusLine(); 943 944 if (dualAlignActive) { 945 // new positions of selected segment's nodes, without applying dual alignment 946 n1movedEn = initialN1en.add(bestMovement); 947 n2movedEn = initialN2en.add(bestMovement); 948 949 // calculate intersections of parallel shifted segment and the adjacent lines 950 newN1en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment1.p1, dualAlignSegment1.p2); 951 newN2en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment2.p1, dualAlignSegment2.p2); 952 if (newN1en == null || newN2en == null) return bestMovement; 953 if (keepSegmentDirection && isOppositeDirection(newN1en, newN2en, initialN1en, initialN2en)) { 954 EastNorth collapsedSegmentPosition = Geometry.getLineLineIntersection(dualAlignSegment1.p1, dualAlignSegment1.p2, 955 dualAlignSegment2.p1, dualAlignSegment2.p2); 956 newN1en = collapsedSegmentPosition; 957 newN2en = collapsedSegmentPosition; 958 dualAlignSegmentCollapsed = true; 959 } else { 960 dualAlignSegmentCollapsed = false; 961 } 962 } else { 963 newN1en = n1movedEn; 964 newN2en = initialN2en.add(bestMovement); 965 } 966 return bestMovement; 967 } 968 969 /** 970 * Gets a node index from selected way before given index. 971 * @param index index of current node 972 * @return index of previous node or <code>-1</code> if there are no nodes there. 973 */ 974 private int getPreviousNodeIndex(int index) { 975 if (index > 0) 976 return index - 1; 977 else if (selectedSegment.way.isClosed()) 978 return selectedSegment.way.getNodesCount() - 2; 979 else 980 return -1; 981 } 982 983 /** 984 * Gets a node from selected way before given index. 985 * @param index index of current node 986 * @return previous node or <code>null</code> if there are no nodes there. 987 */ 988 private Node getPreviousNode(int index) { 989 int indexPrev = getPreviousNodeIndex(index); 990 if (indexPrev >= 0) 991 return selectedSegment.way.getNode(indexPrev); 992 else 993 return null; 994 } 995 996 997 /** 998 * Gets a node index from selected way after given index. 999 * @param index index of current node 1000 * @return index of next node or <code>-1</code> if there are no nodes there. 1001 */ 1002 private int getNextNodeIndex(int index) { 1003 int count = selectedSegment.way.getNodesCount(); 1004 if (index < count - 1) 1005 return index + 1; 1006 else if (selectedSegment.way.isClosed()) 1007 return 1; 1008 else 1009 return -1; 1010 } 1011 1012 /** 1013 * Gets a node from selected way after given index. 1014 * @param index index of current node 1015 * @return next node or <code>null</code> if there are no nodes there. 1016 */ 1017 private Node getNextNode(int index) { 1018 int indexNext = getNextNodeIndex(index); 1019 if (indexNext >= 0) 1020 return selectedSegment.way.getNode(indexNext); 1021 else 1022 return null; 1023 } 1024 1025 // ------------------------------------------------------------------------- 1026 // paint methods 1027 // ------------------------------------------------------------------------- 1028 1029 @Override 1030 public void paint(Graphics2D g, MapView mv, Bounds box) { 1031 Graphics2D g2 = g; 1032 if (mode == Mode.select) { 1033 // Nothing to do 1034 } else { 1035 if (newN1en != null) { 1036 1037 EastNorth p1 = initialN1en; 1038 EastNorth p2 = initialN2en; 1039 EastNorth p3 = newN1en; 1040 EastNorth p4 = newN2en; 1041 1042 Point2D normalUnitVector = activeMoveDirection != null ? getNormalUniVector() : null; 1043 1044 if (mode == Mode.extrude || mode == Mode.create_new) { 1045 g2.setColor(mainColor); 1046 g2.setStroke(mainStroke); 1047 // Draw rectangle around new area. 1048 MapViewPath b = new MapViewPath(mv); 1049 b.moveTo(p1); 1050 b.lineTo(p3); 1051 b.lineTo(p4); 1052 b.lineTo(p2); 1053 b.lineTo(p1); 1054 g2.draw(b); 1055 1056 if (dualAlignActive) { 1057 // Draw reference ways 1058 drawReferenceSegment(g2, mv, dualAlignSegment1); 1059 drawReferenceSegment(g2, mv, dualAlignSegment2); 1060 } else if (activeMoveDirection != null && normalUnitVector != null) { 1061 // Draw reference way 1062 drawReferenceSegment(g2, mv, activeMoveDirection); 1063 1064 // Draw right angle marker on first node position, only when moving at right angle 1065 if (activeMoveDirection.perpendicular) { 1066 // mirror RightAngle marker, so it is inside the extrude 1067 double headingRefWS = activeMoveDirection.p1.heading(activeMoveDirection.p2); 1068 double headingMoveDir = Math.atan2(normalUnitVector.getY(), normalUnitVector.getX()); 1069 double headingDiff = headingRefWS - headingMoveDir; 1070 if (headingDiff < 0) 1071 headingDiff += 2 * Math.PI; 1072 boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5; 1073 Point pr1 = mv.getPoint(activeMoveDirection.p1); 1074 drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA); 1075 } 1076 } 1077 } else if (mode == Mode.translate || mode == Mode.translate_node) { 1078 g2.setColor(mainColor); 1079 if (p1.distance(p2) < 3) { 1080 g2.setStroke(mainStroke); 1081 g2.draw(new MapViewPath(mv).shapeAround(p1, SymbolShape.CIRCLE, symbolSize)); 1082 } else { 1083 g2.setStroke(oldLineStroke); 1084 g2.draw(new MapViewPath(mv).moveTo(p1).lineTo(p2)); 1085 } 1086 1087 if (dualAlignActive) { 1088 // Draw reference ways 1089 drawReferenceSegment(g2, mv, dualAlignSegment1); 1090 drawReferenceSegment(g2, mv, dualAlignSegment2); 1091 } else if (activeMoveDirection != null) { 1092 1093 g2.setColor(helperColor); 1094 g2.setStroke(helperStrokeDash); 1095 // Draw a guideline along the normal. 1096 Point2D centerpoint = mv.getPoint2D(p1.interpolate(p2, .5)); 1097 g2.draw(createSemiInfiniteLine(centerpoint, normalUnitVector, g2)); 1098 // Draw right angle marker on initial position, only when moving at right angle 1099 if (activeMoveDirection.perpendicular) { 1100 // EastNorth units per pixel 1101 g2.setStroke(helperStrokeRA); 1102 g2.setColor(mainColor); 1103 drawAngleSymbol(g2, centerpoint, normalUnitVector, false); 1104 } 1105 } 1106 } 1107 } 1108 g2.setStroke(helperStrokeRA); // restore default stroke to prevent starnge occasional drawings 1109 } 1110 } 1111 1112 private Point2D getNormalUniVector() { 1113 double fac = 1.0 / activeMoveDirection.en.length(); 1114 // mult by factor to get unit vector. 1115 Point2D normalUnitVector = new Point2D.Double(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac); 1116 1117 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector. 1118 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0 1119 if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) { 1120 // If not, use a sign-flipped version of the normalUnitVector. 1121 normalUnitVector = new Point2D.Double(-normalUnitVector.getX(), -normalUnitVector.getY()); 1122 } 1123 1124 //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up. 1125 //This is normally done by MapView.getPoint, but it does not work on vectors. 1126 normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY()); 1127 return normalUnitVector; 1128 } 1129 1130 /** 1131 * Determines if from1-to1 and from2-to2 vectors directions are opposite 1132 * @param from1 vector1 start 1133 * @param to1 vector1 end 1134 * @param from2 vector2 start 1135 * @param to2 vector2 end 1136 * @return true if from1-to1 and from2-to2 vectors directions are opposite 1137 */ 1138 private static boolean isOppositeDirection(EastNorth from1, EastNorth to1, EastNorth from2, EastNorth to2) { 1139 return (from1.getX()-to1.getX())*(from2.getX()-to2.getX()) 1140 +(from1.getY()-to1.getY())*(from2.getY()-to2.getY()) < 0; 1141 } 1142 1143 /** 1144 * Draws right angle symbol at specified position. 1145 * @param g2 the Graphics2D object used to draw on 1146 * @param center center point of angle 1147 * @param normal vector of normal 1148 * @param mirror {@code true} if symbol should be mirrored by the normal 1149 */ 1150 private void drawAngleSymbol(Graphics2D g2, Point2D center, Point2D normal, boolean mirror) { 1151 // EastNorth units per pixel 1152 double factor = 1.0/g2.getTransform().getScaleX(); 1153 double raoffsetx = symbolSize*factor*normal.getX(); 1154 double raoffsety = symbolSize*factor*normal.getY(); 1155 1156 double cx = center.getX(), cy = center.getY(); 1157 double k = mirror ? -1 : 1; 1158 Point2D ra1 = new Point2D.Double(cx + raoffsetx, cy + raoffsety); 1159 Point2D ra3 = new Point2D.Double(cx - raoffsety*k, cy + raoffsetx*k); 1160 Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*k, ra1.getY() + raoffsetx*k); 1161 1162 GeneralPath ra = new GeneralPath(); 1163 ra.moveTo((float) ra1.getX(), (float) ra1.getY()); 1164 ra.lineTo((float) ra2.getX(), (float) ra2.getY()); 1165 ra.lineTo((float) ra3.getX(), (float) ra3.getY()); 1166 g2.setStroke(helperStrokeRA); 1167 g2.draw(ra); 1168 } 1169 1170 /** 1171 * Draws given reference segment. 1172 * @param g2 the Graphics2D object used to draw on 1173 * @param mv map view 1174 * @param seg the reference segment 1175 */ 1176 private void drawReferenceSegment(Graphics2D g2, MapView mv, ReferenceSegment seg) { 1177 g2.setColor(helperColor); 1178 g2.setStroke(helperStrokeDash); 1179 g2.draw(new MapViewPath(mv).moveTo(seg.p1).lineTo(seg.p2)); 1180 } 1181 1182 /** 1183 * Creates a new Line that extends off the edge of the viewport in one direction 1184 * @param start The start point of the line 1185 * @param unitvector A unit vector denoting the direction of the line 1186 * @param g the Graphics2D object it will be used on 1187 * @return created line 1188 */ 1189 private static Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) { 1190 Rectangle bounds = g.getClipBounds(); 1191 try { 1192 AffineTransform invtrans = g.getTransform().createInverse(); 1193 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width, 0), null); 1194 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0, bounds.height), null); 1195 1196 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what 1197 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances. 1198 // This can be used as a safe length of line to generate which will always go off-viewport. 1199 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) 1200 + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY()); 1201 1202 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength), start.getY() 1203 + (unitvector.getY() * linelength))); 1204 } catch (NoninvertibleTransformException e) { 1205 Logging.debug(e); 1206 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10), start.getY() 1207 + (unitvector.getY() * 10))); 1208 } 1209 } 1210}