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