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