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