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.Rectangle; 007import java.awt.geom.AffineTransform; 008import java.awt.geom.Point2D; 009import java.awt.geom.Point2D.Double; 010 011import javax.swing.JComponent; 012 013import org.openstreetmap.josm.Main; 014import org.openstreetmap.josm.data.Bounds; 015import org.openstreetmap.josm.data.ProjectionBounds; 016import org.openstreetmap.josm.data.coor.EastNorth; 017import org.openstreetmap.josm.data.coor.LatLon; 018import org.openstreetmap.josm.data.projection.Projection; 019import org.openstreetmap.josm.gui.download.DownloadDialog; 020 021/** 022 * This class represents a state of the {@link MapView}. 023 * @author Michael Zangl 024 * @since 10343 025 */ 026public final class MapViewState { 027 028 private final Projection projection; 029 030 private final int viewWidth; 031 private final int viewHeight; 032 033 private final double scale; 034 035 /** 036 * Top left {@link EastNorth} coordinate of the view. 037 */ 038 private final EastNorth topLeft; 039 040 private final Point topLeftOnScreen; 041 private final Point topLeftInWindow; 042 043 /** 044 * Create a new {@link MapViewState} 045 * @param projection The projection to use. 046 * @param viewWidth The view width 047 * @param viewHeight The view height 048 * @param scale The scale to use 049 * @param topLeft The top left corner in east/north space. 050 */ 051 private MapViewState(Projection projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) { 052 this.projection = projection; 053 this.scale = scale; 054 this.topLeft = topLeft; 055 056 this.viewWidth = viewWidth; 057 this.viewHeight = viewHeight; 058 topLeftInWindow = new Point(0, 0); 059 topLeftOnScreen = new Point(0, 0); 060 } 061 062 private MapViewState(EastNorth topLeft, MapViewState mapViewState) { 063 this.projection = mapViewState.projection; 064 this.scale = mapViewState.scale; 065 this.topLeft = topLeft; 066 067 viewWidth = mapViewState.viewWidth; 068 viewHeight = mapViewState.viewHeight; 069 topLeftInWindow = mapViewState.topLeftInWindow; 070 topLeftOnScreen = mapViewState.topLeftOnScreen; 071 } 072 073 private MapViewState(double scale, MapViewState mapViewState) { 074 this.projection = mapViewState.projection; 075 this.scale = scale; 076 this.topLeft = mapViewState.topLeft; 077 078 viewWidth = mapViewState.viewWidth; 079 viewHeight = mapViewState.viewHeight; 080 topLeftInWindow = mapViewState.topLeftInWindow; 081 topLeftOnScreen = mapViewState.topLeftOnScreen; 082 } 083 084 private MapViewState(JComponent position, MapViewState mapViewState) { 085 this.projection = mapViewState.projection; 086 this.scale = mapViewState.scale; 087 this.topLeft = mapViewState.topLeft; 088 089 viewWidth = position.getWidth(); 090 viewHeight = position.getHeight(); 091 topLeftInWindow = new Point(); 092 // better than using swing utils, since this allows us to use the mehtod if no screen is present. 093 Container component = position; 094 while (component != null) { 095 topLeftInWindow.x += component.getX(); 096 topLeftInWindow.y += component.getY(); 097 component = component.getParent(); 098 } 099 topLeftOnScreen = position.getLocationOnScreen(); 100 } 101 102 private MapViewState(Projection projection, MapViewState mapViewState) { 103 this.projection = projection; 104 this.scale = mapViewState.scale; 105 this.topLeft = mapViewState.topLeft; 106 107 viewWidth = mapViewState.viewWidth; 108 viewHeight = mapViewState.viewHeight; 109 topLeftInWindow = mapViewState.topLeftInWindow; 110 topLeftOnScreen = mapViewState.topLeftOnScreen; 111 } 112 113 /** 114 * The scale in east/north units per pixel. 115 * @return The scale. 116 */ 117 public double getScale() { 118 return scale; 119 } 120 121 /** 122 * Gets the MapViewPoint representation for a position in view coordinates. 123 * @param x The x coordinate inside the view. 124 * @param y The y coordinate inside the view. 125 * @return The MapViewPoint. 126 */ 127 public MapViewPoint getForView(double x, double y) { 128 return new MapViewViewPoint(x, y); 129 } 130 131 /** 132 * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate. 133 * @param eastNorth the position. 134 * @return The point for that position. 135 */ 136 public MapViewPoint getPointFor(EastNorth eastNorth) { 137 return new MapViewEastNorthPoint(eastNorth); 138 } 139 140 /** 141 * Gets a rectangle representing the whole view area. 142 * @return The rectangle. 143 */ 144 public MapViewRectangle getViewArea() { 145 return getForView(0, 0).rectTo(getForView(viewWidth, viewHeight)); 146 } 147 148 /** 149 * Gets a rectangle of the view as map view area. 150 * @param rectangle The rectangle to get. 151 * @return The view area. 152 * @since 10458 153 */ 154 public MapViewRectangle getViewArea(Rectangle rectangle) { 155 return getForView(rectangle.getMinX(), rectangle.getMinY()).rectTo(getForView(rectangle.getMaxX(), rectangle.getMaxY())); 156 } 157 158 /** 159 * Gets the center of the view. 160 * @return The center position. 161 */ 162 public MapViewPoint getCenter() { 163 return getForView(viewWidth / 2.0, viewHeight / 2.0); 164 } 165 166 /** 167 * Gets the width of the view on the Screen; 168 * @return The width of the view component in screen pixel. 169 */ 170 public double getViewWidth() { 171 return viewWidth; 172 } 173 174 /** 175 * Gets the height of the view on the Screen; 176 * @return The height of the view component in screen pixel. 177 */ 178 public double getViewHeight() { 179 return viewHeight; 180 } 181 182 /** 183 * Gets the current projection used for the MapView. 184 * @return The projection. 185 */ 186 public Projection getProjection() { 187 return projection; 188 } 189 190 /** 191 * Creates an affine transform that is used to convert the east/north coordinates to view coordinates. 192 * @return The affine transform. It should not be changed. 193 * @since 10375 194 */ 195 public AffineTransform getAffineTransform() { 196 return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -topLeft.east() / scale, 197 topLeft.north() / scale); 198 } 199 200 /** 201 * Creates a new state that is the same as the current state except for that it is using a new center. 202 * @param newCenter The new center coordinate. 203 * @return The new state. 204 * @since 10375 205 */ 206 public MapViewState usingCenter(EastNorth newCenter) { 207 return movedTo(getCenter(), newCenter); 208 } 209 210 /** 211 * @param mapViewPoint The reference point. 212 * @param newEastNorthThere The east/north coordinate that should be there. 213 * @return The new state. 214 * @since 10375 215 */ 216 public MapViewState movedTo(MapViewPoint mapViewPoint, EastNorth newEastNorthThere) { 217 EastNorth delta = newEastNorthThere.subtract(mapViewPoint.getEastNorth()); 218 if (delta.distanceSq(0, 0) < .1e-20) { 219 return this; 220 } else { 221 return new MapViewState(topLeft.add(delta), this); 222 } 223 } 224 225 /** 226 * Creates a new state that is the same as the current state except for that it is using a new scale. 227 * @param newScale The new scale to use. 228 * @return The new state. 229 * @since 10375 230 */ 231 public MapViewState usingScale(double newScale) { 232 return new MapViewState(newScale, this); 233 } 234 235 /** 236 * Creates a new state that is the same as the current state except for that it is using the location of the given component. 237 * <p> 238 * The view is moved so that the center is the same as the old center. 239 * @param positon The new location to use. 240 * @return The new state. 241 * @since 10375 242 */ 243 public MapViewState usingLocation(JComponent positon) { 244 EastNorth center = this.getCenter().getEastNorth(); 245 return new MapViewState(positon, this).usingCenter(center); 246 } 247 248 /** 249 * Creates a state that uses the projection. 250 * @param projection The projection to use. 251 * @return The new state. 252 * @since 10486 253 */ 254 public MapViewState usingProjection(Projection projection) { 255 if (projection.equals(this.projection)) { 256 return this; 257 } else { 258 return new MapViewState(projection, this); 259 } 260 } 261 262 /** 263 * 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 264 * before the view was added to the hirarchy. 265 * @param width The view width 266 * @param height The view height 267 * @return The state 268 * @since 10375 269 */ 270 public static MapViewState createDefaultState(int width, int height) { 271 Projection projection = Main.getProjection(); 272 double scale = projection.getDefaultZoomInPPD(); 273 MapViewState state = new MapViewState(projection, width, height, scale, new EastNorth(0, 0)); 274 EastNorth center = calculateDefaultCenter(); 275 return state.movedTo(state.getCenter(), center); 276 } 277 278 private static EastNorth calculateDefaultCenter() { 279 Bounds b = DownloadDialog.getSavedDownloadBounds(); 280 if (b == null) { 281 b = Main.getProjection().getWorldBoundsLatLon(); 282 } 283 return Main.getProjection().latlon2eastNorth(b.getCenter()); 284 } 285 286 /** 287 * A class representing a point in the map view. It allows to convert between the different coordinate systems. 288 * @author Michael Zangl 289 */ 290 public abstract class MapViewPoint { 291 292 /** 293 * Get this point in view coordinates. 294 * @return The point in view coordinates. 295 */ 296 public Point2D getInView() { 297 return new Point2D.Double(getInViewX(), getInViewY()); 298 } 299 300 protected abstract double getInViewX(); 301 302 protected abstract double getInViewY(); 303 304 /** 305 * Convert this point to window coordinates. 306 * @return The point in window coordinates. 307 */ 308 public Point2D getInWindow() { 309 return getUsingCorner(topLeftInWindow); 310 } 311 312 /** 313 * Convert this point to screen coordinates. 314 * @return The point in screen coordinates. 315 */ 316 public Point2D getOnScreen() { 317 return getUsingCorner(topLeftOnScreen); 318 } 319 320 private Double getUsingCorner(Point corner) { 321 return new Point2D.Double(corner.getX() + getInViewX(), corner.getY() + getInViewY()); 322 } 323 324 /** 325 * Gets the {@link EastNorth} coordinate of this point. 326 * @return The east/north coordinate. 327 */ 328 public EastNorth getEastNorth() { 329 return new EastNorth(topLeft.east() + getInViewX() * scale, topLeft.north() - getInViewY() * scale); 330 } 331 332 /** 333 * Create a rectangle from this to the other point. 334 * @param other The other point. Needs to be of the same {@link MapViewState} 335 * @return A rectangle. 336 */ 337 public MapViewRectangle rectTo(MapViewPoint other) { 338 return new MapViewRectangle(this, other); 339 } 340 341 /** 342 * Gets the current position in LatLon coordinates according to the current projection. 343 * @return The positon as LatLon. 344 */ 345 public LatLon getLatLon() { 346 return projection.eastNorth2latlon(getEastNorth()); 347 } 348 } 349 350 private class MapViewViewPoint extends MapViewPoint { 351 private final double x; 352 private final double y; 353 354 MapViewViewPoint(double x, double y) { 355 this.x = x; 356 this.y = y; 357 } 358 359 @Override 360 protected double getInViewX() { 361 return x; 362 } 363 364 @Override 365 protected double getInViewY() { 366 return y; 367 } 368 369 @Override 370 public String toString() { 371 return "MapViewViewPoint [x=" + x + ", y=" + y + ']'; 372 } 373 } 374 375 private class MapViewEastNorthPoint extends MapViewPoint { 376 377 private final EastNorth eastNorth; 378 379 MapViewEastNorthPoint(EastNorth eastNorth) { 380 this.eastNorth = eastNorth; 381 } 382 383 @Override 384 protected double getInViewX() { 385 return (eastNorth.east() - topLeft.east()) / scale; 386 } 387 388 @Override 389 protected double getInViewY() { 390 return (topLeft.north() - eastNorth.north()) / scale; 391 } 392 393 @Override 394 public EastNorth getEastNorth() { 395 return eastNorth; 396 } 397 398 @Override 399 public String toString() { 400 return "MapViewEastNorthPoint [eastNorth=" + eastNorth + ']'; 401 } 402 } 403 404 /** 405 * A rectangle on the MapView. It is rectangular in screen / EastNorth space. 406 * @author Michael Zangl 407 */ 408 public class MapViewRectangle { 409 private final MapViewPoint p1; 410 private final MapViewPoint p2; 411 412 /** 413 * Create a new MapViewRectangle 414 * @param p1 The first point to use 415 * @param p2 The second point to use. 416 */ 417 MapViewRectangle(MapViewPoint p1, MapViewPoint p2) { 418 this.p1 = p1; 419 this.p2 = p2; 420 } 421 422 /** 423 * Gets the projection bounds for this rectangle. 424 * @return The projection bounds. 425 */ 426 public ProjectionBounds getProjectionBounds() { 427 ProjectionBounds b = new ProjectionBounds(p1.getEastNorth()); 428 b.extend(p2.getEastNorth()); 429 return b; 430 } 431 432 /** 433 * Gets a rough estimate of the bounds by assuming lat/lon are parallel to x/y. 434 * @return The bounds computed by converting the corners of this rectangle. 435 * @see #getLatLonBoundsBox() 436 */ 437 public Bounds getCornerBounds() { 438 Bounds b = new Bounds(p1.getLatLon()); 439 b.extend(p2.getLatLon()); 440 return b; 441 } 442 443 /** 444 * Gets the real bounds that enclose this rectangle. 445 * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates. 446 * @return The bounds. 447 * @since 10458 448 */ 449 public Bounds getLatLonBoundsBox() { 450 return projection.getLatLonBoundsBox(getProjectionBounds()); 451 } 452 } 453 454}