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 // Do nothing 353 } 354 355 @Override 356 public void doKeyReleased(KeyEvent e) { 357 if (!dualAlignShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e))) 358 return; 359 if (ignoreNextKeyRelease) { 360 ignoreNextKeyRelease = false; 361 } else { 362 toggleDualAlign(); 363 } 364 } 365 366 /** 367 * Toggles dual alignment mode. 368 */ 369 private void toggleDualAlign() { 370 dualAlignEnabled = !dualAlignEnabled; 371 dualAlignCheckboxMenuItem.setState(dualAlignEnabled); 372 updateStatusLine(); 373 } 374 375 /** 376 * If the left mouse button is pressed over a segment or a node, switches 377 * to appropriate {@link #mode}, depending on Ctrl/Alt/Shift modifiers and 378 * {@link #dualAlignEnabled}. 379 * @param e current mouse event 380 */ 381 @Override 382 public void mousePressed(MouseEvent e) { 383 if (!Main.map.mapView.isActiveLayerVisible()) 384 return; 385 if (!(Boolean) this.getValue("active")) 386 return; 387 if (e.getButton() != MouseEvent.BUTTON1) 388 return; 389 390 requestFocusInMapView(); 391 updateKeyModifiers(e); 392 393 selectedNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate); 394 selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 395 396 // If nothing gets caught, stay in select mode 397 if (selectedSegment == null && selectedNode == null) return; 398 399 if (selectedNode != null) { 400 if (ctrl || nodeDragWithoutCtrl) { 401 movingNodeList = new ArrayList<>(); 402 movingNodeList.add(selectedNode); 403 calculatePossibleDirectionsByNode(); 404 if (possibleMoveDirections.isEmpty()) { 405 // if no directions fould, do not enter dragging mode 406 return; 407 } 408 mode = Mode.translate_node; 409 dualAlignActive = false; 410 } 411 } else { 412 // Otherwise switch to another mode 413 if (dualAlignEnabled && checkDualAlignConditions()) { 414 dualAlignActive = true; 415 calculatePossibleDirectionsForDualAlign(); 416 dualAlignSegmentCollapsed = false; 417 } else { 418 dualAlignActive = false; 419 calculatePossibleDirectionsBySegment(); 420 } 421 if (ctrl) { 422 mode = Mode.translate; 423 movingNodeList = new ArrayList<>(); 424 movingNodeList.add(selectedSegment.getFirstNode()); 425 movingNodeList.add(selectedSegment.getSecondNode()); 426 } else if (alt) { 427 mode = Mode.create_new; 428 // create a new segment and then select and extrude the new segment 429 getCurrentDataSet().setSelected(selectedSegment.way); 430 alwaysCreateNodes = true; 431 } else { 432 mode = Mode.extrude; 433 getCurrentDataSet().setSelected(selectedSegment.way); 434 alwaysCreateNodes = shift; 435 } 436 } 437 438 // Signifies that nothing has happened yet 439 newN1en = null; 440 newN2en = null; 441 moveCommand = null; 442 moveCommand2 = null; 443 444 Main.map.mapView.addTemporaryLayer(this); 445 446 updateStatusLine(); 447 Main.map.mapView.repaint(); 448 449 // Make note of time pressed 450 mouseDownTime = System.currentTimeMillis(); 451 452 // Make note of mouse position 453 initialMousePos = e.getPoint(); 454 } 455 456 /** 457 * Performs action depending on what {@link #mode} we're in. 458 * @param e current mouse event 459 */ 460 @Override 461 public void mouseDragged(MouseEvent e) { 462 if (!Main.map.mapView.isActiveLayerVisible()) 463 return; 464 465 // do not count anything as a drag if it lasts less than 100 milliseconds. 466 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) 467 return; 468 469 if (mode == Mode.select) { 470 // Just sit tight and wait for mouse to be released. 471 } else { 472 //move, create new and extrude mode - move the selected segment 473 474 EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y); 475 EastNorth bestMovement = calculateBestMovementAndNewNodes(mouseEn); 476 477 Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this); 478 479 if (dualAlignActive) { 480 if (mode == Mode.extrude || mode == Mode.create_new) { 481 // nothing here 482 } else if (mode == Mode.translate) { 483 EastNorth movement1 = newN1en.subtract(initialN1en); 484 EastNorth movement2 = newN2en.subtract(initialN2en); 485 // move nodes to new position 486 if (moveCommand == null || moveCommand2 == null) { 487 // make a new move commands 488 moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY()); 489 moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY()); 490 Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2); 491 Main.main.undoRedo.add(c); 492 } else { 493 // reuse existing move commands 494 moveCommand.moveAgainTo(movement1.getX(), movement1.getY()); 495 moveCommand2.moveAgainTo(movement2.getX(), movement2.getY()); 496 } 497 } 498 } else { 499 if (mode == Mode.extrude || mode == Mode.create_new) { 500 //nothing here 501 } else if (mode == Mode.translate_node || mode == Mode.translate) { 502 //move nodes to new position 503 if (moveCommand == null) { 504 //make a new move command 505 moveCommand = new MoveCommand(new ArrayList<OsmPrimitive>(movingNodeList), bestMovement); 506 Main.main.undoRedo.add(moveCommand); 507 } else { 508 //reuse existing move command 509 moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY()); 510 } 511 } 512 } 513 514 Main.map.mapView.repaint(); 515 } 516 } 517 518 /** 519 * Does anything that needs to be done, then switches back to select mode. 520 * @param e current mouse event 521 */ 522 @Override 523 public void mouseReleased(MouseEvent e) { 524 525 if (!Main.map.mapView.isActiveLayerVisible()) 526 return; 527 528 if (mode == Mode.select) { 529 // Nothing to be done 530 } else { 531 if (mode == Mode.create_new) { 532 if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null) { 533 createNewRectangle(); 534 } 535 } else if (mode == Mode.extrude) { 536 if (e.getClickCount() == 2 && e.getPoint().equals(initialMousePos)) { 537 // double click adds a new node 538 addNewNode(e); 539 } else if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null && selectedSegment != null) { 540 // main extrusion commands 541 performExtrusion(); 542 } 543 } else if (mode == Mode.translate || mode == Mode.translate_node) { 544 //Commit translate 545 //the move command is already committed in mouseDragged 546 joinNodesIfCollapsed(movingNodeList); 547 } 548 549 updateKeyModifiers(e); 550 // Switch back into select mode 551 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this); 552 Main.map.mapView.removeTemporaryLayer(this); 553 selectedSegment = null; 554 moveCommand = null; 555 mode = Mode.select; 556 dualAlignSegmentCollapsed = false; 557 updateStatusLine(); 558 Main.map.mapView.repaint(); 559 } 560 } 561 562 // ------------------------------------------------------------------------- 563 // Custom methods 564 // ------------------------------------------------------------------------- 565 566 /** 567 * Inserts node into nearby segment. 568 * @param e current mouse point 569 */ 570 private static void addNewNode(MouseEvent e) { 571 // Should maybe do the same as in DrawAction and fetch all nearby segments? 572 WaySegment ws = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 573 if (ws != null) { 574 Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY())); 575 EastNorth a = ws.getFirstNode().getEastNorth(); 576 EastNorth b = ws.getSecondNode().getEastNorth(); 577 n.setEastNorth(Geometry.closestPointToSegment(a, b, n.getEastNorth())); 578 Way wnew = new Way(ws.way); 579 wnew.addNode(ws.lowerIndex+1, n); 580 SequenceCommand cmds = new SequenceCommand(tr("Add a new node to an existing way"), 581 new AddCommand(n), new ChangeCommand(ws.way, wnew)); 582 Main.main.undoRedo.add(cmds); 583 } 584 } 585 586 /** 587 * Creates a new way that shares segment with selected way. 588 */ 589 private void createNewRectangle() { 590 if (selectedSegment == null) return; 591 // crete a new rectangle 592 Collection<Command> cmds = new LinkedList<>(); 593 Node third = new Node(newN2en); 594 Node fourth = new Node(newN1en); 595 Way wnew = new Way(); 596 wnew.addNode(selectedSegment.getFirstNode()); 597 wnew.addNode(selectedSegment.getSecondNode()); 598 wnew.addNode(third); 599 if (!dualAlignSegmentCollapsed) { 600 // rectangle can degrade to triangle for dual alignment after collapsing 601 wnew.addNode(fourth); 602 } 603 // ... and close the way 604 wnew.addNode(selectedSegment.getFirstNode()); 605 // undo support 606 cmds.add(new AddCommand(third)); 607 if (!dualAlignSegmentCollapsed) { 608 cmds.add(new AddCommand(fourth)); 609 } 610 cmds.add(new AddCommand(wnew)); 611 Command c = new SequenceCommand(tr("Extrude Way"), cmds); 612 Main.main.undoRedo.add(c); 613 getCurrentDataSet().setSelected(wnew); 614 } 615 616 /** 617 * Does actual extrusion of {@link #selectedSegment}. 618 * Uses {@link #initialN1en}, {@link #initialN2en} saved in calculatePossibleDirections* call 619 * Uses {@link #newN1en}, {@link #newN2en} calculated by {@link #calculateBestMovementAndNewNodes} 620 */ 621 private void performExtrusion() { 622 // create extrusion 623 Collection<Command> cmds = new LinkedList<>(); 624 Way wnew = new Way(selectedSegment.way); 625 boolean wayWasModified = false; 626 boolean wayWasSingleSegment = wnew.getNodesCount() == 2; 627 int insertionPoint = selectedSegment.lowerIndex + 1; 628 629 //find if the new points overlap existing segments (in case of 90 degree angles) 630 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 631 boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en); 632 // segmentAngleZero marks subset of nodeOverlapsSegment. 633 // nodeOverlapsSegment is true if angle between segments is 0 or PI, segmentAngleZero only if angle is 0 634 boolean segmentAngleZero = prevNode != null && Math.abs(Geometry.getCornerAngle(prevNode.getEastNorth(), initialN1en, newN1en)) < 1e-5; 635 boolean hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way); 636 List<Node> changedNodes = new ArrayList<>(); 637 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { 638 //move existing node 639 Node n1Old = selectedSegment.getFirstNode(); 640 cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en))); 641 changedNodes.add(n1Old); 642 } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) { 643 // replace shared node with new one 644 Node n1Old = selectedSegment.getFirstNode(); 645 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en)); 646 wnew.addNode(insertionPoint, n1New); 647 wnew.removeNode(n1Old); 648 wayWasModified = true; 649 cmds.add(new AddCommand(n1New)); 650 changedNodes.add(n1New); 651 } else { 652 //introduce new node 653 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en)); 654 wnew.addNode(insertionPoint, n1New); 655 wayWasModified = true; 656 insertionPoint++; 657 cmds.add(new AddCommand(n1New)); 658 changedNodes.add(n1New); 659 } 660 661 //find if the new points overlap existing segments (in case of 90 degree angles) 662 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 663 nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en); 664 segmentAngleZero = nextNode != null && Math.abs(Geometry.getCornerAngle(nextNode.getEastNorth(), initialN2en, newN2en)) < 1e-5; 665 hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way); 666 667 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { 668 //move existing node 669 Node n2Old = selectedSegment.getSecondNode(); 670 cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en))); 671 changedNodes.add(n2Old); 672 } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) { 673 // replace shared node with new one 674 Node n2Old = selectedSegment.getSecondNode(); 675 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en)); 676 wnew.addNode(insertionPoint, n2New); 677 wnew.removeNode(n2Old); 678 wayWasModified = true; 679 cmds.add(new AddCommand(n2New)); 680 changedNodes.add(n2New); 681 } else { 682 //introduce new node 683 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en)); 684 wnew.addNode(insertionPoint, n2New); 685 wayWasModified = true; 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 * This method calculates offset amount by which to move the given segment 768 * perpendicularly for it to be in line with mouse position. 769 * @param segmentP1 segment's first point 770 * @param segmentP2 segment's second point 771 * @param moveDirection direction of movement 772 * @param targetPos mouse position 773 * @return offset amount of P1 and P2. 774 */ 775 private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection, 776 EastNorth targetPos) { 777 EastNorth intersectionPoint; 778 if (segmentP1.distanceSq(segmentP2) > 1e-7) { 779 intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, targetPos.add(moveDirection)); 780 } else { 781 intersectionPoint = Geometry.closestPointToLine(targetPos, targetPos.add(moveDirection), segmentP1); 782 } 783 784 if (intersectionPoint == null) 785 return null; 786 else 787 //return distance form base to target position 788 return targetPos.subtract(intersectionPoint); 789 } 790 791 /** 792 * Gathers possible move directions - perpendicular to the selected segment 793 * and parallel to neighboring segments. 794 */ 795 private void calculatePossibleDirectionsBySegment() { 796 // remember initial positions for segment nodes. 797 initialN1en = selectedSegment.getFirstNode().getEastNorth(); 798 initialN2en = selectedSegment.getSecondNode().getEastNorth(); 799 800 //add direction perpendicular to the selected segment 801 possibleMoveDirections = new ArrayList<>(); 802 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 803 initialN1en.getY() - initialN2en.getY(), 804 initialN2en.getX() - initialN1en.getX() 805 ), initialN1en, initialN2en, true)); 806 807 808 //add directions parallel to neighbor segments 809 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 810 if (prevNode != null) { 811 EastNorth en = prevNode.getEastNorth(); 812 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 813 initialN1en.getX() - en.getX(), 814 initialN1en.getY() - en.getY() 815 ), initialN1en, en, false)); 816 } 817 818 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 819 if (nextNode != null) { 820 EastNorth en = nextNode.getEastNorth(); 821 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 822 initialN2en.getX() - en.getX(), 823 initialN2en.getY() - en.getY() 824 ), initialN2en, en, false)); 825 } 826 } 827 828 /** 829 * Gathers possible move directions - along all adjacent segments. 830 */ 831 private void calculatePossibleDirectionsByNode() { 832 // remember initial positions for segment nodes. 833 initialN1en = selectedNode.getEastNorth(); 834 initialN2en = initialN1en; 835 possibleMoveDirections = new ArrayList<>(); 836 for (OsmPrimitive p: selectedNode.getReferrers()) { 837 if (p instanceof Way && p.isUsable()) { 838 for (Node neighbor: ((Way) p).getNeighbours(selectedNode)) { 839 EastNorth en = neighbor.getEastNorth(); 840 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 841 initialN1en.getX() - en.getX(), 842 initialN1en.getY() - en.getY() 843 ), initialN1en, en, false)); 844 } 845 } 846 } 847 } 848 849 /** 850 * Checks dual alignment conditions: 851 * 1. selected segment has both neighboring segments, 852 * 2. selected segment is not parallel with neighboring segments. 853 * @return {@code true} if dual alignment conditions are satisfied 854 */ 855 private boolean checkDualAlignConditions() { 856 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 857 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 858 if (prevNode == null || nextNode == null) { 859 return false; 860 } 861 862 EastNorth n1en = selectedSegment.getFirstNode().getEastNorth(); 863 EastNorth n2en = selectedSegment.getSecondNode().getEastNorth(); 864 if (n1en.distance(prevNode.getEastNorth()) < 1e-4 || 865 n2en.distance(nextNode.getEastNorth()) < 1e-4) { 866 return false; 867 } 868 869 boolean prevSegmentParallel = Geometry.segmentsParallel(n1en, prevNode.getEastNorth(), n1en, n2en); 870 boolean nextSegmentParallel = Geometry.segmentsParallel(n2en, nextNode.getEastNorth(), n1en, n2en); 871 if (prevSegmentParallel || nextSegmentParallel) { 872 return false; 873 } 874 875 return true; 876 } 877 878 /** 879 * Gathers possible move directions - perpendicular to the selected segment only. 880 * Neighboring segments go to {@link #dualAlignSegment1} and {@link #dualAlignSegment2}. 881 */ 882 private void calculatePossibleDirectionsForDualAlign() { 883 // remember initial positions for segment nodes. 884 initialN1en = selectedSegment.getFirstNode().getEastNorth(); 885 initialN2en = selectedSegment.getSecondNode().getEastNorth(); 886 887 // add direction perpendicular to the selected segment 888 possibleMoveDirections = new ArrayList<>(); 889 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 890 initialN1en.getY() - initialN2en.getY(), 891 initialN2en.getX() - initialN1en.getX() 892 ), initialN1en, initialN2en, true)); 893 894 // set neighboring segments 895 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 896 EastNorth prevNodeEn = prevNode.getEastNorth(); 897 dualAlignSegment1 = new ReferenceSegment(new EastNorth( 898 initialN1en.getX() - prevNodeEn.getX(), 899 initialN1en.getY() - prevNodeEn.getY() 900 ), initialN1en, prevNodeEn, false); 901 902 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 903 EastNorth nextNodeEn = nextNode.getEastNorth(); 904 dualAlignSegment2 = new ReferenceSegment(new EastNorth( 905 initialN2en.getX() - nextNodeEn.getX(), 906 initialN2en.getY() - nextNodeEn.getY() 907 ), initialN2en, nextNodeEn, false); 908 } 909 910 /** 911 * Calculate newN1en, newN2en best suitable for given mouse coordinates 912 * For dual align, calculates positions of new nodes, aligning them to neighboring segments. 913 * Elsewhere, just adds the vetor returned by calculateBestMovement to {@link #initialN1en}, {@link #initialN2en}. 914 * @param mouseEn mouse coordinates 915 * @return best movement vector 916 */ 917 private EastNorth calculateBestMovementAndNewNodes(EastNorth mouseEn) { 918 EastNorth bestMovement = calculateBestMovement(mouseEn); 919 EastNorth n1movedEn = initialN1en.add(bestMovement), n2movedEn; 920 921 // find out the movement distance, in metres 922 double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance( 923 Main.getProjection().eastNorth2latlon(n1movedEn)); 924 Main.map.statusLine.setDist(distance); 925 updateStatusLine(); 926 927 if (dualAlignActive) { 928 // new positions of selected segment's nodes, without applying dual alignment 929 n1movedEn = initialN1en.add(bestMovement); 930 n2movedEn = initialN2en.add(bestMovement); 931 932 // calculate intersections of parallel shifted segment and the adjacent lines 933 newN1en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment1.p1, dualAlignSegment1.p2); 934 newN2en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment2.p1, dualAlignSegment2.p2); 935 if (newN1en == null || newN2en == null) return bestMovement; 936 if (keepSegmentDirection && isOppositeDirection(newN1en, newN2en, initialN1en, initialN2en)) { 937 EastNorth collapsedSegmentPosition = Geometry.getLineLineIntersection(dualAlignSegment1.p1, dualAlignSegment1.p2, 938 dualAlignSegment2.p1, dualAlignSegment2.p2); 939 newN1en = collapsedSegmentPosition; 940 newN2en = collapsedSegmentPosition; 941 dualAlignSegmentCollapsed = true; 942 } else { 943 dualAlignSegmentCollapsed = false; 944 } 945 } else { 946 newN1en = n1movedEn; 947 newN2en = initialN2en.add(bestMovement); 948 } 949 return bestMovement; 950 } 951 952 /** 953 * Gets a node index from selected way before given index. 954 * @param index index of current node 955 * @return index of previous node or <code>-1</code> if there are no nodes there. 956 */ 957 private int getPreviousNodeIndex(int index) { 958 if (index > 0) 959 return index - 1; 960 else if (selectedSegment.way.isClosed()) 961 return selectedSegment.way.getNodesCount() - 2; 962 else 963 return -1; 964 } 965 966 /** 967 * Gets a node from selected way before given index. 968 * @param index index of current node 969 * @return previous node or <code>null</code> if there are no nodes there. 970 */ 971 private Node getPreviousNode(int index) { 972 int indexPrev = getPreviousNodeIndex(index); 973 if (indexPrev >= 0) 974 return selectedSegment.way.getNode(indexPrev); 975 else 976 return null; 977 } 978 979 980 /** 981 * Gets a node index from selected way after given index. 982 * @param index index of current node 983 * @return index of next node or <code>-1</code> if there are no nodes there. 984 */ 985 private int getNextNodeIndex(int index) { 986 int count = selectedSegment.way.getNodesCount(); 987 if (index < count - 1) 988 return index + 1; 989 else if (selectedSegment.way.isClosed()) 990 return 1; 991 else 992 return -1; 993 } 994 995 /** 996 * Gets a node from selected way after given index. 997 * @param index index of current node 998 * @return next node or <code>null</code> if there are no nodes there. 999 */ 1000 private Node getNextNode(int index) { 1001 int indexNext = getNextNodeIndex(index); 1002 if (indexNext >= 0) 1003 return selectedSegment.way.getNode(indexNext); 1004 else 1005 return null; 1006 } 1007 1008 // ------------------------------------------------------------------------- 1009 // paint methods 1010 // ------------------------------------------------------------------------- 1011 1012 @Override 1013 public void paint(Graphics2D g, MapView mv, Bounds box) { 1014 Graphics2D g2 = g; 1015 if (mode == Mode.select) { 1016 // Nothing to do 1017 } else { 1018 if (newN1en != null) { 1019 1020 Point p1 = mv.getPoint(initialN1en); 1021 Point p2 = mv.getPoint(initialN2en); 1022 Point p3 = mv.getPoint(newN1en); 1023 Point p4 = mv.getPoint(newN2en); 1024 1025 Point2D normalUnitVector = activeMoveDirection != null ? getNormalUniVector() : null; 1026 1027 if (mode == Mode.extrude || mode == Mode.create_new) { 1028 g2.setColor(mainColor); 1029 g2.setStroke(mainStroke); 1030 // Draw rectangle around new area. 1031 GeneralPath b = new GeneralPath(); 1032 b.moveTo(p1.x, p1.y); 1033 b.lineTo(p3.x, p3.y); 1034 b.lineTo(p4.x, p4.y); 1035 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 && normalUnitVector != 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) 1054 headingDiff += 2 * Math.PI; 1055 boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5; 1056 Point pr1 = mv.getPoint(activeMoveDirection.p1); 1057 drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA); 1058 } 1059 } 1060 } else if (mode == Mode.translate || mode == Mode.translate_node) { 1061 g2.setColor(mainColor); 1062 if (p1.distance(p2) < 3) { 1063 g2.setStroke(mainStroke); 1064 g2.drawOval((int) (p1.x-symbolSize/2), (int) (p1.y-symbolSize/2), 1065 (int) (symbolSize), (int) (symbolSize)); 1066 } else { 1067 Line2D oldline = new Line2D.Double(p1, p2); 1068 g2.setStroke(oldLineStroke); 1069 g2.draw(oldline); 1070 } 1071 1072 if (dualAlignActive) { 1073 // Draw reference ways 1074 drawReferenceSegment(g2, mv, dualAlignSegment1); 1075 drawReferenceSegment(g2, mv, dualAlignSegment2); 1076 } else if (activeMoveDirection != null) { 1077 1078 g2.setColor(helperColor); 1079 g2.setStroke(helperStrokeDash); 1080 // Draw a guideline along the normal. 1081 Line2D normline; 1082 Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5); 1083 normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2); 1084 g2.draw(normline); 1085 // Draw right angle marker on initial position, only when moving at right angle 1086 if (activeMoveDirection.perpendicular) { 1087 // EastNorth units per pixel 1088 g2.setStroke(helperStrokeRA); 1089 g2.setColor(mainColor); 1090 drawAngleSymbol(g2, centerpoint, normalUnitVector, false); 1091 } 1092 } 1093 } 1094 } 1095 g2.setStroke(helperStrokeRA); // restore default stroke to prevent starnge occasional drawings 1096 } 1097 } 1098 1099 private Point2D getNormalUniVector() { 1100 double fac = 1.0 / activeMoveDirection.en.length(); 1101 // mult by factor to get unit vector. 1102 Point2D normalUnitVector = new Point2D.Double(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac); 1103 1104 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector. 1105 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0 1106 if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) { 1107 // If not, use a sign-flipped version of the normalUnitVector. 1108 normalUnitVector = new Point2D.Double(-normalUnitVector.getX(), -normalUnitVector.getY()); 1109 } 1110 1111 //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up. 1112 //This is normally done by MapView.getPoint, but it does not work on vectors. 1113 normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY()); 1114 return normalUnitVector; 1115 } 1116 1117 /** 1118 * Determines if from1-to1 and from2-to2 vectors directions are opposite 1119 * @param from1 vector1 start 1120 * @param to1 vector1 end 1121 * @param from2 vector2 start 1122 * @param to2 vector2 end 1123 * @return true if from1-to1 and from2-to2 vectors directions are opposite 1124 */ 1125 private static boolean isOppositeDirection(EastNorth from1, EastNorth to1, EastNorth from2, EastNorth to2) { 1126 return (from1.getX()-to1.getX())*(from2.getX()-to2.getX()) 1127 +(from1.getY()-to1.getY())*(from2.getY()-to2.getY()) < 0; 1128 } 1129 1130 /** 1131 * Draws right angle symbol at specified position. 1132 * @param g2 the Graphics2D object used to draw on 1133 * @param center center point of angle 1134 * @param normal vector of normal 1135 * @param mirror {@code true} if symbol should be mirrored by the normal 1136 */ 1137 private void drawAngleSymbol(Graphics2D g2, Point2D center, Point2D normal, boolean mirror) { 1138 // EastNorth units per pixel 1139 double factor = 1.0/g2.getTransform().getScaleX(); 1140 double raoffsetx = symbolSize*factor*normal.getX(); 1141 double raoffsety = symbolSize*factor*normal.getY(); 1142 1143 double cx = center.getX(), cy = center.getY(); 1144 double k = mirror ? -1 : 1; 1145 Point2D ra1 = new Point2D.Double(cx + raoffsetx, cy + raoffsety); 1146 Point2D ra3 = new Point2D.Double(cx - raoffsety*k, cy + raoffsetx*k); 1147 Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*k, ra1.getY() + raoffsetx*k); 1148 1149 GeneralPath ra = new GeneralPath(); 1150 ra.moveTo((float) ra1.getX(), (float) ra1.getY()); 1151 ra.lineTo((float) ra2.getX(), (float) ra2.getY()); 1152 ra.lineTo((float) ra3.getX(), (float) ra3.getY()); 1153 g2.setStroke(helperStrokeRA); 1154 g2.draw(ra); 1155 } 1156 1157 /** 1158 * Draws given reference segment. 1159 * @param g2 the Graphics2D object used to draw on 1160 * @param mv map view 1161 * @param seg the reference segment 1162 */ 1163 private void drawReferenceSegment(Graphics2D g2, MapView mv, ReferenceSegment seg) { 1164 Point p1 = mv.getPoint(seg.p1); 1165 Point p2 = mv.getPoint(seg.p2); 1166 GeneralPath b = new GeneralPath(); 1167 b.moveTo(p1.x, p1.y); 1168 b.lineTo(p2.x, p2.y); 1169 g2.setColor(helperColor); 1170 g2.setStroke(helperStrokeDash); 1171 g2.draw(b); 1172 } 1173 1174 /** 1175 * Creates a new Line that extends off the edge of the viewport in one direction 1176 * @param start The start point of the line 1177 * @param unitvector A unit vector denoting the direction of the line 1178 * @param g the Graphics2D object it will be used on 1179 * @return created line 1180 */ 1181 private static Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) { 1182 Rectangle bounds = g.getDeviceConfiguration().getBounds(); 1183 try { 1184 AffineTransform invtrans = g.getTransform().createInverse(); 1185 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width, 0), null); 1186 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0, bounds.height), null); 1187 1188 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what 1189 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances. 1190 // This can be used as a safe length of line to generate which will always go off-viewport. 1191 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) 1192 + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY()); 1193 1194 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength), start.getY() 1195 + (unitvector.getY() * linelength))); 1196 } catch (NoninvertibleTransformException e) { 1197 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10), start.getY() 1198 + (unitvector.getY() * 10))); 1199 } 1200 } 1201}