001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import java.awt.Container; 005import java.awt.Point; 006import java.awt.geom.AffineTransform; 007import java.awt.geom.Area; 008import java.awt.geom.Path2D; 009import java.awt.geom.Point2D; 010import java.awt.geom.Point2D.Double; 011import java.awt.geom.Rectangle2D; 012import java.io.Serializable; 013import java.util.Objects; 014import java.util.Optional; 015 016import javax.swing.JComponent; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.data.Bounds; 020import org.openstreetmap.josm.data.ProjectionBounds; 021import org.openstreetmap.josm.data.coor.EastNorth; 022import org.openstreetmap.josm.data.coor.ILatLon; 023import org.openstreetmap.josm.data.coor.LatLon; 024import org.openstreetmap.josm.data.osm.Node; 025import org.openstreetmap.josm.data.projection.Projecting; 026import org.openstreetmap.josm.data.projection.Projection; 027import org.openstreetmap.josm.gui.download.DownloadDialog; 028import org.openstreetmap.josm.tools.CheckParameterUtil; 029import org.openstreetmap.josm.tools.Geometry; 030import org.openstreetmap.josm.tools.JosmRuntimeException; 031import org.openstreetmap.josm.tools.bugreport.BugReport; 032 033/** 034 * This class represents a state of the {@link MapView}. 035 * @author Michael Zangl 036 * @since 10343 037 */ 038public final class MapViewState implements Serializable { 039 040 private static final long serialVersionUID = 1L; 041 042 /** 043 * A flag indicating that the point is outside to the top of the map view. 044 * @since 10827 045 */ 046 public static final int OUTSIDE_TOP = 1; 047 048 /** 049 * A flag indicating that the point is outside to the bottom of the map view. 050 * @since 10827 051 */ 052 public static final int OUTSIDE_BOTTOM = 2; 053 054 /** 055 * A flag indicating that the point is outside to the left of the map view. 056 * @since 10827 057 */ 058 public static final int OUTSIDE_LEFT = 4; 059 060 /** 061 * A flag indicating that the point is outside to the right of the map view. 062 * @since 10827 063 */ 064 public static final int OUTSIDE_RIGHT = 8; 065 066 /** 067 * Additional pixels outside the view for where to start clipping. 068 */ 069 private static final int CLIP_BOUNDS = 50; 070 071 private final transient Projecting projecting; 072 073 private final int viewWidth; 074 private final int viewHeight; 075 076 private final double scale; 077 078 /** 079 * Top left {@link EastNorth} coordinate of the view. 080 */ 081 private final EastNorth topLeft; 082 083 private final Point topLeftOnScreen; 084 private final Point topLeftInWindow; 085 086 /** 087 * Create a new {@link MapViewState} 088 * @param projection The projection to use. 089 * @param viewWidth The view width 090 * @param viewHeight The view height 091 * @param scale The scale to use 092 * @param topLeft The top left corner in east/north space. 093 * @param topLeftInWindow The top left point in window 094 * @param topLeftOnScreen The top left point on screen 095 */ 096 private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft, 097 Point topLeftInWindow, Point topLeftOnScreen) { 098 CheckParameterUtil.ensureParameterNotNull(projection, "projection"); 099 CheckParameterUtil.ensureParameterNotNull(topLeft, "topLeft"); 100 CheckParameterUtil.ensureParameterNotNull(topLeftInWindow, "topLeftInWindow"); 101 CheckParameterUtil.ensureParameterNotNull(topLeftOnScreen, "topLeftOnScreen"); 102 103 this.projecting = projection; 104 this.scale = scale; 105 this.topLeft = topLeft; 106 107 this.viewWidth = viewWidth; 108 this.viewHeight = viewHeight; 109 this.topLeftInWindow = topLeftInWindow; 110 this.topLeftOnScreen = topLeftOnScreen; 111 } 112 113 private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) { 114 this(projection, viewWidth, viewHeight, scale, topLeft, new Point(0, 0), new Point(0, 0)); 115 } 116 117 private MapViewState(EastNorth topLeft, MapViewState mvs) { 118 this(mvs.projecting, mvs.viewWidth, mvs.viewHeight, mvs.scale, topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen); 119 } 120 121 private MapViewState(double scale, MapViewState mvs) { 122 this(mvs.projecting, mvs.viewWidth, mvs.viewHeight, scale, mvs.topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen); 123 } 124 125 private MapViewState(JComponent position, MapViewState mvs) { 126 this(mvs.projecting, position.getWidth(), position.getHeight(), mvs.scale, mvs.topLeft, 127 findTopLeftInWindow(position), findTopLeftOnScreen(position)); 128 } 129 130 private MapViewState(Projecting projecting, MapViewState mvs) { 131 this(projecting, mvs.viewWidth, mvs.viewHeight, mvs.scale, mvs.topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen); 132 } 133 134 private static Point findTopLeftInWindow(JComponent position) { 135 Point result = new Point(); 136 // better than using swing utils, since this allows us to use the method if no screen is present. 137 Container component = position; 138 while (component != null) { 139 result.x += component.getX(); 140 result.y += component.getY(); 141 component = component.getParent(); 142 } 143 return result; 144 } 145 146 private static Point findTopLeftOnScreen(JComponent position) { 147 try { 148 return position.getLocationOnScreen(); 149 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 150 throw BugReport.intercept(e).put("position", position).put("parent", position::getParent); 151 } 152 } 153 154 @Override 155 public String toString() { 156 return getClass().getName() + " [projecting=" + this.projecting 157 + " viewWidth=" + this.viewWidth 158 + " viewHeight=" + this.viewHeight 159 + " scale=" + this.scale 160 + " topLeft=" + this.topLeft + ']'; 161 } 162 163 /** 164 * The scale in east/north units per pixel. 165 * @return The scale. 166 */ 167 public double getScale() { 168 return scale; 169 } 170 171 /** 172 * Gets the MapViewPoint representation for a position in view coordinates. 173 * @param x The x coordinate inside the view. 174 * @param y The y coordinate inside the view. 175 * @return The MapViewPoint. 176 */ 177 public MapViewPoint getForView(double x, double y) { 178 return new MapViewViewPoint(x, y); 179 } 180 181 /** 182 * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate. 183 * @param eastNorth the position. 184 * @return The point for that position. 185 */ 186 public MapViewPoint getPointFor(EastNorth eastNorth) { 187 return new MapViewEastNorthPoint(eastNorth); 188 } 189 190 /** 191 * Gets the {@link MapViewPoint} for the given {@link LatLon} coordinate. 192 * <p> 193 * This method exists to not break binary compatibility with old plugins 194 * @param latlon the position 195 * @return The point for that position. 196 * @since 10651 197 */ 198 public MapViewPoint getPointFor(LatLon latlon) { 199 return getPointFor((ILatLon) latlon); 200 } 201 202 /** 203 * Gets the {@link MapViewPoint} for the given {@link LatLon} coordinate. 204 * @param latlon the position 205 * @return The point for that position. 206 * @since 12161 207 */ 208 public MapViewPoint getPointFor(ILatLon latlon) { 209 try { 210 return getPointFor(Optional.ofNullable(latlon.getEastNorth(getProjection())) 211 .orElseThrow(IllegalArgumentException::new)); 212 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 213 throw BugReport.intercept(e).put("latlon", latlon); 214 } 215 } 216 217 /** 218 * Gets the {@link MapViewPoint} for the given node. 219 * This is faster than {@link #getPointFor(LatLon)} because it uses the node east/north cache. 220 * @param node The node 221 * @return The position of that node. 222 * @since 10827 223 */ 224 public MapViewPoint getPointFor(Node node) { 225 return getPointFor((ILatLon) node); 226 } 227 228 /** 229 * Gets a rectangle representing the whole view area. 230 * @return The rectangle. 231 */ 232 public MapViewRectangle getViewArea() { 233 return getForView(0, 0).rectTo(getForView(viewWidth, viewHeight)); 234 } 235 236 /** 237 * Gets a rectangle of the view as map view area. 238 * @param rectangle The rectangle to get. 239 * @return The view area. 240 * @since 10827 241 */ 242 public MapViewRectangle getViewArea(Rectangle2D rectangle) { 243 return getForView(rectangle.getMinX(), rectangle.getMinY()).rectTo(getForView(rectangle.getMaxX(), rectangle.getMaxY())); 244 } 245 246 /** 247 * Gets the center of the view. 248 * @return The center position. 249 */ 250 public MapViewPoint getCenter() { 251 return getForView(viewWidth / 2.0, viewHeight / 2.0); 252 } 253 254 /** 255 * Gets the width of the view on the Screen; 256 * @return The width of the view component in screen pixel. 257 */ 258 public double getViewWidth() { 259 return viewWidth; 260 } 261 262 /** 263 * Gets the height of the view on the Screen; 264 * @return The height of the view component in screen pixel. 265 */ 266 public double getViewHeight() { 267 return viewHeight; 268 } 269 270 /** 271 * Gets the current projection used for the MapView. 272 * @return The projection. 273 * @see #getProjecting() 274 */ 275 public Projection getProjection() { 276 return projecting.getBaseProjection(); 277 } 278 279 /** 280 * Gets the current projecting instance that is used to convert between east/north and lat/lon space. 281 * @return The projection. 282 * @since 12161 283 */ 284 public Projecting getProjecting() { 285 return projecting; 286 } 287 288 /** 289 * Creates an affine transform that is used to convert the east/north coordinates to view coordinates. 290 * @return The affine transform. It should not be changed. 291 * @since 10375 292 */ 293 public AffineTransform getAffineTransform() { 294 return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -topLeft.east() / scale, 295 topLeft.north() / scale); 296 } 297 298 /** 299 * Gets a rectangle that is several pixel bigger than the view. It is used to define the view clipping. 300 * @return The rectangle. 301 */ 302 public MapViewRectangle getViewClipRectangle() { 303 return getForView(-CLIP_BOUNDS, -CLIP_BOUNDS).rectTo(getForView(getViewWidth() + CLIP_BOUNDS, getViewHeight() + CLIP_BOUNDS)); 304 } 305 306 /** 307 * Returns the area for the given bounds. 308 * @param bounds bounds 309 * @return the area for the given bounds 310 */ 311 public Area getArea(Bounds bounds) { 312 Path2D area = new Path2D.Double(); 313 getProjection().visitOutline(bounds, en -> { 314 MapViewPoint point = getPointFor(en); 315 if (area.getCurrentPoint() == null) { 316 area.moveTo(point.getInViewX(), point.getInViewY()); 317 } else { 318 area.lineTo(point.getInViewX(), point.getInViewY()); 319 } 320 }); 321 area.closePath(); 322 return new Area(area); 323 } 324 325 /** 326 * Creates a new state that is the same as the current state except for that it is using a new center. 327 * @param newCenter The new center coordinate. 328 * @return The new state. 329 * @since 10375 330 */ 331 public MapViewState usingCenter(EastNorth newCenter) { 332 return movedTo(getCenter(), newCenter); 333 } 334 335 /** 336 * @param mapViewPoint The reference point. 337 * @param newEastNorthThere The east/north coordinate that should be there. 338 * @return The new state. 339 * @since 10375 340 */ 341 public MapViewState movedTo(MapViewPoint mapViewPoint, EastNorth newEastNorthThere) { 342 EastNorth delta = newEastNorthThere.subtract(mapViewPoint.getEastNorth()); 343 if (delta.distanceSq(0, 0) < .1e-20) { 344 return this; 345 } else { 346 return new MapViewState(topLeft.add(delta), this); 347 } 348 } 349 350 /** 351 * Creates a new state that is the same as the current state except for that it is using a new scale. 352 * @param newScale The new scale to use. 353 * @return The new state. 354 * @since 10375 355 */ 356 public MapViewState usingScale(double newScale) { 357 return new MapViewState(newScale, this); 358 } 359 360 /** 361 * Creates a new state that is the same as the current state except for that it is using the location of the given component. 362 * <p> 363 * The view is moved so that the center is the same as the old center. 364 * @param positon The new location to use. 365 * @return The new state. 366 * @since 10375 367 */ 368 public MapViewState usingLocation(JComponent positon) { 369 EastNorth center = this.getCenter().getEastNorth(); 370 return new MapViewState(positon, this).usingCenter(center); 371 } 372 373 /** 374 * Creates a state that uses the projection. 375 * @param projection The projection to use. 376 * @return The new state. 377 * @since 10486 378 */ 379 public MapViewState usingProjection(Projection projection) { 380 if (projection.equals(this.projecting)) { 381 return this; 382 } else { 383 return new MapViewState(projection, this); 384 } 385 } 386 387 /** 388 * Create the default {@link MapViewState} object for the given map view. The screen position won't be set so that this method can be used 389 * before the view was added to the hierarchy. 390 * @param width The view width 391 * @param height The view height 392 * @return The state 393 * @since 10375 394 */ 395 public static MapViewState createDefaultState(int width, int height) { 396 Projection projection = Main.getProjection(); 397 double scale = projection.getDefaultZoomInPPD(); 398 MapViewState state = new MapViewState(projection, width, height, scale, new EastNorth(0, 0)); 399 EastNorth center = calculateDefaultCenter(); 400 return state.movedTo(state.getCenter(), center); 401 } 402 403 private static EastNorth calculateDefaultCenter() { 404 Bounds b = Optional.ofNullable(DownloadDialog.getSavedDownloadBounds()).orElseGet( 405 () -> Main.getProjection().getWorldBoundsLatLon()); 406 return b.getCenter().getEastNorth(Main.getProjection()); 407 } 408 409 /** 410 * Check if this MapViewState equals another one, disregarding the position 411 * of the JOSM window on screen. 412 * @param other the other MapViewState 413 * @return true if the other MapViewState has the same size, scale, position and projection, 414 * false otherwise 415 */ 416 public boolean equalsInWindow(MapViewState other) { 417 return other != null && 418 this.viewWidth == other.viewWidth && 419 this.viewHeight == other.viewHeight && 420 this.scale == other.scale && 421 Objects.equals(this.topLeft, other.topLeft) && 422 Objects.equals(this.projecting, other.projecting); 423 } 424 425 /** 426 * A class representing a point in the map view. It allows to convert between the different coordinate systems. 427 * @author Michael Zangl 428 */ 429 public abstract class MapViewPoint { 430 /** 431 * Gets the map view state this path is used for. 432 * @return The state. 433 * @since 12505 434 */ 435 public MapViewState getMapViewState() { 436 return MapViewState.this; 437 } 438 439 /** 440 * Get this point in view coordinates. 441 * @return The point in view coordinates. 442 */ 443 public Point2D getInView() { 444 return new Point2D.Double(getInViewX(), getInViewY()); 445 } 446 447 /** 448 * Get the x coordinate in view space without creating an intermediate object. 449 * @return The x coordinate 450 * @since 10827 451 */ 452 public abstract double getInViewX(); 453 454 /** 455 * Get the y coordinate in view space without creating an intermediate object. 456 * @return The y coordinate 457 * @since 10827 458 */ 459 public abstract double getInViewY(); 460 461 /** 462 * Convert this point to window coordinates. 463 * @return The point in window coordinates. 464 */ 465 public Point2D getInWindow() { 466 return getUsingCorner(topLeftInWindow); 467 } 468 469 /** 470 * Convert this point to screen coordinates. 471 * @return The point in screen coordinates. 472 */ 473 public Point2D getOnScreen() { 474 return getUsingCorner(topLeftOnScreen); 475 } 476 477 private Double getUsingCorner(Point corner) { 478 return new Point2D.Double(corner.getX() + getInViewX(), corner.getY() + getInViewY()); 479 } 480 481 /** 482 * Gets the {@link EastNorth} coordinate of this point. 483 * @return The east/north coordinate. 484 */ 485 public EastNorth getEastNorth() { 486 return new EastNorth(topLeft.east() + getInViewX() * scale, topLeft.north() - getInViewY() * scale); 487 } 488 489 /** 490 * Create a rectangle from this to the other point. 491 * @param other The other point. Needs to be of the same {@link MapViewState} 492 * @return A rectangle. 493 */ 494 public MapViewRectangle rectTo(MapViewPoint other) { 495 return new MapViewRectangle(this, other); 496 } 497 498 /** 499 * Gets the current position in LatLon coordinates according to the current projection. 500 * @return The positon as LatLon. 501 * @see #getLatLonClamped() 502 */ 503 public LatLon getLatLon() { 504 return projecting.getBaseProjection().eastNorth2latlon(getEastNorth()); 505 } 506 507 /** 508 * Gets the latlon coordinate clamped to the current world area. 509 * @return The lat/lon coordinate 510 * @since 10805 511 */ 512 public LatLon getLatLonClamped() { 513 return projecting.eastNorth2latlonClamped(getEastNorth()); 514 } 515 516 /** 517 * Add the given offset to this point 518 * @param en The offset in east/north space. 519 * @return The new point 520 * @since 10651 521 */ 522 public MapViewPoint add(EastNorth en) { 523 return new MapViewEastNorthPoint(getEastNorth().add(en)); 524 } 525 526 /** 527 * Check if this point is inside the view bounds. 528 * 529 * This is the case iff <code>getOutsideRectangleFlags(getViewArea())</code> returns no flags 530 * @return true if it is. 531 * @since 10827 532 */ 533 public boolean isInView() { 534 return inRange(getInViewX(), 0, getViewWidth()) && inRange(getInViewY(), 0, getViewHeight()); 535 } 536 537 private boolean inRange(double val, int min, double max) { 538 return val >= min && val < max; 539 } 540 541 /** 542 * Gets the direction in which this point is outside of the given view rectangle. 543 * @param rect The rectangle to check agains. 544 * @return The direction in which it is outside of the view, as OUTSIDE_... flags. 545 * @since 10827 546 */ 547 public int getOutsideRectangleFlags(MapViewRectangle rect) { 548 Rectangle2D bounds = rect.getInView(); 549 int flags = 0; 550 if (getInViewX() < bounds.getMinX()) { 551 flags |= OUTSIDE_LEFT; 552 } else if (getInViewX() > bounds.getMaxX()) { 553 flags |= OUTSIDE_RIGHT; 554 } 555 if (getInViewY() < bounds.getMinY()) { 556 flags |= OUTSIDE_TOP; 557 } else if (getInViewY() > bounds.getMaxY()) { 558 flags |= OUTSIDE_BOTTOM; 559 } 560 561 return flags; 562 } 563 564 /** 565 * Gets the sum of the x/y view distances between the points. |x1 - x2| + |y1 - y2| 566 * @param p2 The other point 567 * @return The norm 568 * @since 10827 569 */ 570 public double oneNormInView(MapViewPoint p2) { 571 return Math.abs(getInViewX() - p2.getInViewX()) + Math.abs(getInViewY() - p2.getInViewY()); 572 } 573 574 /** 575 * Gets the squared distance between this point and an other point. 576 * @param p2 The other point 577 * @return The squared distance. 578 * @since 10827 579 */ 580 public double distanceToInViewSq(MapViewPoint p2) { 581 double dx = getInViewX() - p2.getInViewX(); 582 double dy = getInViewY() - p2.getInViewY(); 583 return dx * dx + dy * dy; 584 } 585 586 /** 587 * Gets the distance between this point and an other point. 588 * @param p2 The other point 589 * @return The distance. 590 * @since 10827 591 */ 592 public double distanceToInView(MapViewPoint p2) { 593 return Math.sqrt(distanceToInViewSq(p2)); 594 } 595 596 /** 597 * Do a linear interpolation to the other point 598 * @param p1 The other point 599 * @param i The interpolation factor. 0 is at the current point, 1 at the other point. 600 * @return The new point 601 * @since 10874 602 */ 603 public MapViewPoint interpolate(MapViewPoint p1, double i) { 604 return new MapViewViewPoint((1 - i) * getInViewX() + i * p1.getInViewX(), (1 - i) * getInViewY() + i * p1.getInViewY()); 605 } 606 } 607 608 private class MapViewViewPoint extends MapViewPoint { 609 private final double x; 610 private final double y; 611 612 MapViewViewPoint(double x, double y) { 613 this.x = x; 614 this.y = y; 615 } 616 617 @Override 618 public double getInViewX() { 619 return x; 620 } 621 622 @Override 623 public double getInViewY() { 624 return y; 625 } 626 627 @Override 628 public String toString() { 629 return "MapViewViewPoint [x=" + x + ", y=" + y + ']'; 630 } 631 } 632 633 private class MapViewEastNorthPoint extends MapViewPoint { 634 635 private final EastNorth eastNorth; 636 637 MapViewEastNorthPoint(EastNorth eastNorth) { 638 this.eastNorth = Objects.requireNonNull(eastNorth, "eastNorth"); 639 } 640 641 @Override 642 public double getInViewX() { 643 return (eastNorth.east() - topLeft.east()) / scale; 644 } 645 646 @Override 647 public double getInViewY() { 648 return (topLeft.north() - eastNorth.north()) / scale; 649 } 650 651 @Override 652 public EastNorth getEastNorth() { 653 return eastNorth; 654 } 655 656 @Override 657 public String toString() { 658 return "MapViewEastNorthPoint [eastNorth=" + eastNorth + ']'; 659 } 660 } 661 662 /** 663 * A rectangle on the MapView. It is rectangular in screen / EastNorth space. 664 * @author Michael Zangl 665 */ 666 public class MapViewRectangle { 667 private final MapViewPoint p1; 668 private final MapViewPoint p2; 669 670 /** 671 * Create a new MapViewRectangle 672 * @param p1 The first point to use 673 * @param p2 The second point to use. 674 */ 675 MapViewRectangle(MapViewPoint p1, MapViewPoint p2) { 676 this.p1 = p1; 677 this.p2 = p2; 678 } 679 680 /** 681 * Gets the projection bounds for this rectangle. 682 * @return The projection bounds. 683 */ 684 public ProjectionBounds getProjectionBounds() { 685 ProjectionBounds b = new ProjectionBounds(p1.getEastNorth()); 686 b.extend(p2.getEastNorth()); 687 return b; 688 } 689 690 /** 691 * Gets a rough estimate of the bounds by assuming lat/lon are parallel to x/y. 692 * @return The bounds computed by converting the corners of this rectangle. 693 * @see #getLatLonBoundsBox() 694 */ 695 public Bounds getCornerBounds() { 696 Bounds b = new Bounds(p1.getLatLon()); 697 b.extend(p2.getLatLon()); 698 return b; 699 } 700 701 /** 702 * Gets the real bounds that enclose this rectangle. 703 * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates. 704 * @return The bounds. 705 * @since 10458 706 */ 707 public Bounds getLatLonBoundsBox() { 708 // TODO @michael2402: Use hillclimb. 709 return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds()); 710 } 711 712 /** 713 * Gets this rectangle on the screen. 714 * @return The rectangle. 715 * @since 10651 716 */ 717 public Rectangle2D getInView() { 718 double x1 = p1.getInViewX(); 719 double y1 = p1.getInViewY(); 720 double x2 = p2.getInViewX(); 721 double y2 = p2.getInViewY(); 722 return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2)); 723 } 724 725 /** 726 * Check if the rectangle intersects the map view area. 727 * @return <code>true</code> if it intersects. 728 * @since 10827 729 */ 730 public boolean isInView() { 731 return getInView().intersects(getViewArea().getInView()); 732 } 733 734 /** 735 * Gets the entry point at which a line between start and end enters the current view. 736 * @param start The start 737 * @param end The end 738 * @return The entry point or <code>null</code> if the line does not intersect this view. 739 */ 740 public MapViewPoint getLineEntry(MapViewPoint start, MapViewPoint end) { 741 ProjectionBounds bounds = getProjectionBounds(); 742 if (bounds.contains(start.getEastNorth())) { 743 return start; 744 } 745 746 double dx = end.getEastNorth().east() - start.getEastNorth().east(); 747 double boundX = dx > 0 ? bounds.minEast : bounds.maxEast; 748 EastNorth borderIntersection = Geometry.getSegmentSegmentIntersection(start.getEastNorth(), end.getEastNorth(), 749 new EastNorth(boundX, bounds.minNorth), 750 new EastNorth(boundX, bounds.maxNorth)); 751 if (borderIntersection != null) { 752 return getPointFor(borderIntersection); 753 } 754 755 double dy = end.getEastNorth().north() - start.getEastNorth().north(); 756 double boundY = dy > 0 ? bounds.minNorth : bounds.maxNorth; 757 borderIntersection = Geometry.getSegmentSegmentIntersection(start.getEastNorth(), end.getEastNorth(), 758 new EastNorth(bounds.minEast, boundY), 759 new EastNorth(bounds.maxEast, boundY)); 760 if (borderIntersection != null) { 761 return getPointFor(borderIntersection); 762 } 763 764 return null; 765 } 766 767 @Override 768 public String toString() { 769 return "MapViewRectangle [p1=" + p1 + ", p2=" + p2 + ']'; 770 } 771 } 772}