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