001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.actions.mapmode; 003 004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005 import static org.openstreetmap.josm.tools.I18n.tr; 006 007 import java.awt.AWTEvent; 008 import java.awt.BasicStroke; 009 import java.awt.Color; 010 import java.awt.Cursor; 011 import java.awt.Graphics2D; 012 import java.awt.Point; 013 import java.awt.Rectangle; 014 import java.awt.Toolkit; 015 import java.awt.event.AWTEventListener; 016 import java.awt.event.ActionEvent; 017 import java.awt.event.InputEvent; 018 import java.awt.event.KeyEvent; 019 import java.awt.event.MouseEvent; 020 import java.awt.geom.AffineTransform; 021 import java.awt.geom.GeneralPath; 022 import java.awt.geom.Line2D; 023 import java.awt.geom.NoninvertibleTransformException; 024 import java.awt.geom.Point2D; 025 import java.util.ArrayList; 026 import java.util.Collection; 027 import java.util.LinkedList; 028 import java.util.List; 029 030 import org.openstreetmap.josm.Main; 031 import org.openstreetmap.josm.command.AddCommand; 032 import org.openstreetmap.josm.command.ChangeCommand; 033 import org.openstreetmap.josm.command.Command; 034 import org.openstreetmap.josm.command.MoveCommand; 035 import org.openstreetmap.josm.command.SequenceCommand; 036 import org.openstreetmap.josm.data.Bounds; 037 import org.openstreetmap.josm.data.coor.EastNorth; 038 import org.openstreetmap.josm.data.osm.Node; 039 import org.openstreetmap.josm.data.osm.OsmPrimitive; 040 import org.openstreetmap.josm.data.osm.Way; 041 import org.openstreetmap.josm.data.osm.WaySegment; 042 import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 043 import org.openstreetmap.josm.gui.MapFrame; 044 import org.openstreetmap.josm.gui.MapView; 045 import org.openstreetmap.josm.gui.layer.Layer; 046 import org.openstreetmap.josm.gui.layer.MapViewPaintable; 047 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 048 import org.openstreetmap.josm.tools.Geometry; 049 import org.openstreetmap.josm.tools.ImageProvider; 050 import org.openstreetmap.josm.tools.Shortcut; 051 052 /** 053 * Makes a rectangle from a line, or modifies a rectangle. 054 */ 055 public class ExtrudeAction extends MapMode implements MapViewPaintable { 056 057 enum Mode { extrude, translate, select, create_new } 058 059 private Mode mode = Mode.select; 060 061 /** 062 * If true, when extruding create new node even if segments parallel. 063 */ 064 private boolean alwaysCreateNodes = false; 065 private long mouseDownTime = 0; 066 private WaySegment selectedSegment = null; 067 private Color selectedColor; 068 069 /** 070 * Possible directions to move to. 071 */ 072 private List<EastNorth> possibleMoveDirections; 073 074 /** 075 * The direction that is currently active. 076 */ 077 private EastNorth activeMoveDirection; 078 079 /** 080 * The position of the mouse cursor when the drag action was initiated. 081 */ 082 private Point initialMousePos; 083 /** 084 * The time which needs to pass between click and release before something 085 * counts as a move, in milliseconds 086 */ 087 private int initialMoveDelay = 200; 088 /** 089 * The initial EastNorths of node1 and node2 090 */ 091 private EastNorth initialN1en; 092 private EastNorth initialN2en; 093 /** 094 * The new EastNorths of node1 and node2 095 */ 096 private EastNorth newN1en; 097 private EastNorth newN2en; 098 099 /** 100 * the command that performed last move. 101 */ 102 private MoveCommand moveCommand; 103 104 /** The cursor for the 'create_new' mode. */ 105 private final Cursor cursorCreateNew; 106 107 /** 108 * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed. 109 */ 110 private final AWTEventListener altKeyListener = new AWTEventListener() { 111 @Override 112 public void eventDispatched(AWTEvent e) { 113 if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable()) 114 return; 115 InputEvent ie = (InputEvent) e; 116 boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; 117 if(mode == Mode.select) { 118 Main.map.mapView.setNewCursor(alt ? cursorCreateNew : cursor, this); 119 } 120 } 121 }; 122 123 /** 124 * Create a new SelectAction 125 * @param mapFrame The MapFrame this action belongs to. 126 */ 127 public ExtrudeAction(MapFrame mapFrame) { 128 super(tr("Extrude"), "extrude/extrude", tr("Create areas"), 129 Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT), 130 mapFrame, 131 ImageProvider.getCursor("normal", "rectangle")); 132 putValue("help", ht("/Action/Extrude")); 133 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200); 134 selectedColor = PaintColors.SELECTED.get(); 135 cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus"); 136 } 137 138 @Override public String getModeHelpText() { 139 if (mode == Mode.translate) 140 return tr("Move a segment along its normal, then release the mouse button."); 141 else if (mode == Mode.extrude) 142 return tr("Draw a rectangle of the desired size, then release the mouse button."); 143 else if (mode == Mode.create_new) 144 return tr("Draw a rectangle of the desired size, then release the mouse button."); 145 else 146 return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " + 147 "Alt-drag to create a new rectangle, double click to add a new node."); 148 } 149 150 @Override public boolean layerIsSupported(Layer l) { 151 return l instanceof OsmDataLayer; 152 } 153 154 @Override public void enterMode() { 155 super.enterMode(); 156 Main.map.mapView.addMouseListener(this); 157 Main.map.mapView.addMouseMotionListener(this); 158 try { 159 Toolkit.getDefaultToolkit().addAWTEventListener(altKeyListener, AWTEvent.KEY_EVENT_MASK); 160 } catch (SecurityException ex) { 161 } 162 } 163 164 @Override public void exitMode() { 165 Main.map.mapView.removeMouseListener(this); 166 Main.map.mapView.removeMouseMotionListener(this); 167 Main.map.mapView.removeTemporaryLayer(this); 168 try { 169 Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener); 170 } catch (SecurityException ex) { 171 } 172 super.exitMode(); 173 } 174 175 /** 176 * If the left mouse button is pressed over a segment, switch 177 * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held. 178 */ 179 @Override public void mousePressed(MouseEvent e) { 180 if(!Main.map.mapView.isActiveLayerVisible()) 181 return; 182 if (!(Boolean)this.getValue("active")) 183 return; 184 if (e.getButton() != MouseEvent.BUTTON1) 185 return; 186 187 updateKeyModifiers(e); 188 189 selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 190 191 if (selectedSegment == null) { 192 // If nothing gets caught, stay in select mode 193 } else { 194 // Otherwise switch to another mode 195 196 if (ctrl) { 197 mode = Mode.translate; 198 } else if (alt) { 199 mode = Mode.create_new; 200 // create a new segment and then select and extrude the new segment 201 getCurrentDataSet().setSelected(selectedSegment.way); 202 alwaysCreateNodes = true; 203 } else { 204 mode = Mode.extrude; 205 getCurrentDataSet().setSelected(selectedSegment.way); 206 alwaysCreateNodes = shift; 207 } 208 209 // remember initial positions for segment nodes. 210 initialN1en = selectedSegment.getFirstNode().getEastNorth(); 211 initialN2en = selectedSegment.getSecondNode().getEastNorth(); 212 213 //gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments 214 possibleMoveDirections = new ArrayList<EastNorth>(); 215 possibleMoveDirections.add(new EastNorth( 216 initialN1en.getY() - initialN2en.getY(), 217 initialN2en.getX() - initialN1en.getX())); 218 219 //add directions parallel to neighbor segments 220 221 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 222 if (prevNode != null) { 223 EastNorth en = prevNode.getEastNorth(); 224 possibleMoveDirections.add(new EastNorth( 225 initialN1en.getX() - en.getX(), 226 initialN1en.getY() - en.getY())); 227 } 228 229 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 230 if (nextNode != null) { 231 EastNorth en = nextNode.getEastNorth(); 232 possibleMoveDirections.add(new EastNorth( 233 initialN2en.getX() - en.getX(), 234 initialN2en.getY() - en.getY())); 235 } 236 237 // Signifies that nothing has happened yet 238 newN1en = null; 239 newN2en = null; 240 moveCommand = null; 241 242 Main.map.mapView.addTemporaryLayer(this); 243 244 updateStatusLine(); 245 Main.map.mapView.repaint(); 246 247 // Make note of time pressed 248 mouseDownTime = System.currentTimeMillis(); 249 250 // Make note of mouse position 251 initialMousePos = e.getPoint(); 252 } 253 } 254 255 /** 256 * Perform action depending on what mode we're in. 257 */ 258 @Override public void mouseDragged(MouseEvent e) { 259 if(!Main.map.mapView.isActiveLayerVisible()) 260 return; 261 262 // do not count anything as a drag if it lasts less than 100 milliseconds. 263 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) 264 return; 265 266 if (mode == Mode.select) { 267 // Just sit tight and wait for mouse to be released. 268 } else { 269 //move, create new and extrude mode - move the selected segment 270 271 EastNorth initialMouseEn = Main.map.mapView.getEastNorth(initialMousePos.x, initialMousePos.y); 272 EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y); 273 EastNorth mouseMovement = new EastNorth(mouseEn.getX() - initialMouseEn.getX(), mouseEn.getY() - initialMouseEn.getY()); 274 275 double bestDistance = Double.POSITIVE_INFINITY; 276 EastNorth bestMovement = null; 277 activeMoveDirection = null; 278 279 //find the best movement direction and vector 280 for (EastNorth direction: possibleMoveDirections) { 281 EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction , mouseEn); 282 if (movement == null) { 283 //if direction parallel to segment. 284 continue; 285 } 286 287 double distanceFromMouseMovement = movement.distance(mouseMovement); 288 if (bestDistance > distanceFromMouseMovement) { 289 bestDistance = distanceFromMouseMovement; 290 activeMoveDirection = direction; 291 bestMovement = movement; 292 } 293 } 294 295 newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY()); 296 newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY()); 297 298 // find out the movement distance, in metres 299 double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en)); 300 Main.map.statusLine.setDist(distance); 301 updateStatusLine(); 302 303 Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this); 304 305 if (mode == Mode.extrude || mode == Mode.create_new) { 306 //nothing here 307 } else if (mode == Mode.translate) { 308 //move nodes to new position 309 if (moveCommand == null) { 310 //make a new move command 311 Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>(); 312 nodelist.add(selectedSegment.getFirstNode()); 313 nodelist.add(selectedSegment.getSecondNode()); 314 moveCommand = new MoveCommand(nodelist, bestMovement.getX(), bestMovement.getY()); 315 Main.main.undoRedo.add(moveCommand); 316 } else { 317 //reuse existing move command 318 moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY()); 319 } 320 } 321 322 Main.map.mapView.repaint(); 323 } 324 } 325 326 /** 327 * Do anything that needs to be done, then switch back to select mode 328 */ 329 @Override public void mouseReleased(MouseEvent e) { 330 331 if(!Main.map.mapView.isActiveLayerVisible()) 332 return; 333 334 if (mode == Mode.select) { 335 // Nothing to be done 336 } else { 337 if (mode == Mode.create_new) { 338 if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) { 339 // crete a new rectangle 340 Collection<Command> cmds = new LinkedList<Command>(); 341 Node third = new Node(newN2en); 342 Node fourth = new Node(newN1en); 343 Way wnew = new Way(); 344 wnew.addNode(selectedSegment.getFirstNode()); 345 wnew.addNode(selectedSegment.getSecondNode()); 346 wnew.addNode(third); 347 wnew.addNode(fourth); 348 // ... and close the way 349 wnew.addNode(selectedSegment.getFirstNode()); 350 // undo support 351 cmds.add(new AddCommand(third)); 352 cmds.add(new AddCommand(fourth)); 353 cmds.add(new AddCommand(wnew)); 354 Command c = new SequenceCommand(tr("Extrude Way"), cmds); 355 Main.main.undoRedo.add(c); 356 getCurrentDataSet().setSelected(wnew); 357 } 358 } else if (mode == Mode.extrude) { 359 if( e.getClickCount() == 2 && e.getPoint().equals(initialMousePos) ) { 360 // double click add a new node 361 // Should maybe do the same as in DrawAction and fetch all nearby segments? 362 WaySegment ws = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 363 if (ws != null) { 364 Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY())); 365 EastNorth A = ws.getFirstNode().getEastNorth(); 366 EastNorth B = ws.getSecondNode().getEastNorth(); 367 n.setEastNorth(Geometry.closestPointToSegment(A, B, n.getEastNorth())); 368 Way wnew = new Way(ws.way); 369 wnew.addNode(ws.lowerIndex+1, n); 370 SequenceCommand cmds = new SequenceCommand(tr("Add a new node to an existing way"), 371 new AddCommand(n), new ChangeCommand(ws.way, wnew)); 372 Main.main.undoRedo.add(cmds); 373 } 374 } 375 else if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null && selectedSegment != null) { 376 // create extrusion 377 378 Collection<Command> cmds = new LinkedList<Command>(); 379 Way wnew = new Way(selectedSegment.way); 380 int insertionPoint = selectedSegment.lowerIndex + 1; 381 382 //find if the new points overlap existing segments (in case of 90 degree angles) 383 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 384 boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en); 385 boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way); 386 387 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { 388 //move existing node 389 Node n1Old = selectedSegment.getFirstNode(); 390 cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en))); 391 } else { 392 //introduce new node 393 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en)); 394 wnew.addNode(insertionPoint, n1New); 395 insertionPoint ++; 396 cmds.add(new AddCommand(n1New)); 397 } 398 399 //find if the new points overlap existing segments (in case of 90 degree angles) 400 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 401 nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en); 402 hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way); 403 404 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { 405 //move existing node 406 Node n2Old = selectedSegment.getSecondNode(); 407 cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en))); 408 } else { 409 //introduce new node 410 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en)); 411 wnew.addNode(insertionPoint, n2New); 412 insertionPoint ++; 413 cmds.add(new AddCommand(n2New)); 414 } 415 416 //the way was a single segment, close the way 417 if (wnew.getNodesCount() == 4) { 418 wnew.addNode(selectedSegment.getFirstNode()); 419 } 420 421 cmds.add(new ChangeCommand(selectedSegment.way, wnew)); 422 Command c = new SequenceCommand(tr("Extrude Way"), cmds); 423 Main.main.undoRedo.add(c); 424 } 425 } else if (mode == Mode.translate) { 426 //Commit translate 427 //the move command is already committed in mouseDragged 428 } 429 430 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; 431 // Switch back into select mode 432 Main.map.mapView.setNewCursor(alt ? cursorCreateNew : cursor, this); 433 Main.map.mapView.removeTemporaryLayer(this); 434 selectedSegment = null; 435 moveCommand = null; 436 mode = Mode.select; 437 438 updateStatusLine(); 439 Main.map.mapView.repaint(); 440 } 441 } 442 443 /** 444 * This method tests if a node has other ways apart from the given one. 445 * @param node 446 * @param myWay 447 * @return true of node belongs only to myWay, false if there are more ways. 448 */ 449 private boolean hasNodeOtherWays(Node node, Way myWay) { 450 for (OsmPrimitive p : node.getReferrers()) { 451 if (p instanceof Way && p.isUsable() && p != myWay) 452 return true; 453 } 454 return false; 455 } 456 457 /*** 458 * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position. 459 * @param segmentP1 460 * @param segmentP2 461 * @param targetPos 462 * @return offset amount of P1 and P2. 463 */ 464 private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection, 465 EastNorth targetPos) { 466 EastNorth intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, 467 new EastNorth(targetPos.getX() + moveDirection.getX(), targetPos.getY() + moveDirection.getY())); 468 469 if (intersectionPoint == null) 470 return null; 471 else 472 //return distance form base to target position 473 return new EastNorth(targetPos.getX() - intersectionPoint.getX(), 474 targetPos.getY() - intersectionPoint.getY()); 475 } 476 477 478 /** 479 * Gets a node from selected way before given index. 480 * @param index index of current node 481 * @return previous node or null if there are no nodes there. 482 */ 483 private Node getPreviousNode(int index) { 484 if (index > 0) 485 return selectedSegment.way.getNode(index - 1); 486 else if (selectedSegment.way.isClosed()) 487 return selectedSegment.way.getNode(selectedSegment.way.getNodesCount() - 2); 488 else 489 return null; 490 } 491 492 /** 493 * Gets a node from selected way before given index. 494 * @param index index of current node 495 * @return next node or null if there are no nodes there. 496 */ 497 private Node getNextNode(int index) { 498 int count = selectedSegment.way.getNodesCount(); 499 if (index < count - 1) 500 return selectedSegment.way.getNode(index + 1); 501 else if (selectedSegment.way.isClosed()) 502 return selectedSegment.way.getNode(1); 503 else 504 return null; 505 } 506 507 public void paint(Graphics2D g, MapView mv, Bounds box) { 508 if (mode == Mode.select) { 509 // Nothing to do 510 } else { 511 if (newN1en != null) { 512 Graphics2D g2 = g; 513 g2.setColor(selectedColor); 514 g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); 515 516 Point p1 = mv.getPoint(initialN1en); 517 Point p2 = mv.getPoint(initialN2en); 518 Point p3 = mv.getPoint(newN1en); 519 Point p4 = mv.getPoint(newN2en); 520 521 if (mode == Mode.extrude || mode == Mode.create_new) { 522 // Draw rectangle around new area. 523 GeneralPath b = new GeneralPath(); 524 b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y); 525 b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y); 526 b.lineTo(p1.x, p1.y); 527 g2.draw(b); 528 g2.setStroke(new BasicStroke(1)); 529 } else if (mode == Mode.translate) { 530 // Highlight the new and old segments. 531 Line2D newline = new Line2D.Double(p3, p4); 532 g2.draw(newline); 533 g2.setStroke(new BasicStroke(1)); 534 Line2D oldline = new Line2D.Double(p1, p2); 535 g2.draw(oldline); 536 537 if (activeMoveDirection != null) { 538 539 double fac = 1.0 / activeMoveDirection.distance(0,0); 540 // mult by factor to get unit vector. 541 EastNorth normalUnitVector = new EastNorth(activeMoveDirection.getX() * fac, activeMoveDirection.getY() * fac); 542 543 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector. 544 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0 545 if (newN1en != null && (newN1en.getX() > initialN1en.getX() != normalUnitVector.getX() > -0.0)) { 546 // If not, use a sign-flipped version of the normalUnitVector. 547 normalUnitVector = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY()); 548 } 549 550 //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up. 551 //This is normally done by MapView.getPoint, but it does not work on vectors. 552 normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY()); 553 554 // Draw a guideline along the normal. 555 Line2D normline; 556 Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5); 557 normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2); 558 g2.draw(normline); 559 560 // Draw right angle marker on initial position, only when moving at right angle 561 if (activeMoveDirection == possibleMoveDirections.get(0)) { 562 // EastNorth units per pixel 563 double factor = 1.0/g2.getTransform().getScaleX(); 564 565 double raoffsetx = 8.0*factor*normalUnitVector.getX(); 566 double raoffsety = 8.0*factor*normalUnitVector.getY(); 567 Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety); 568 Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx); 569 Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx); 570 GeneralPath ra = new GeneralPath(); 571 ra.moveTo((float)ra1.getX(), (float)ra1.getY()); 572 ra.lineTo((float)ra2.getX(), (float)ra2.getY()); 573 ra.lineTo((float)ra3.getX(), (float)ra3.getY()); 574 g2.draw(ra); 575 } 576 } 577 } 578 } 579 } 580 } 581 582 /** 583 * Create a new Line that extends off the edge of the viewport in one direction 584 * @param start The start point of the line 585 * @param unitvector A unit vector denoting the direction of the line 586 * @param g the Graphics2D object it will be used on 587 */ 588 static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) { 589 Rectangle bounds = g.getDeviceConfiguration().getBounds(); 590 try { 591 AffineTransform invtrans = g.getTransform().createInverse(); 592 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null); 593 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null); 594 595 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what 596 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances. 597 // This can be used as a safe length of line to generate which will always go off-viewport. 598 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY()); 599 600 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength))); 601 } 602 catch (NoninvertibleTransformException e) { 603 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10))); 604 } 605 } 606 }