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