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