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