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