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