source: josm/trunk/src/org/openstreetmap/josm/gui/MapViewState.java@ 10943

Last change on this file since 10943 was 10910, checked in by Don-vip, 8 years ago

improve javadoc, unit tests, reduce visibility of some public fields

  • Property svn:eol-style set to native
File size: 21.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui;
3
4import java.awt.Container;
5import java.awt.Point;
6import java.awt.geom.AffineTransform;
7import java.awt.geom.Area;
8import java.awt.geom.Path2D;
9import java.awt.geom.Point2D;
10import java.awt.geom.Point2D.Double;
11import java.awt.geom.Rectangle2D;
12import java.io.Serializable;
13
14import javax.swing.JComponent;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.data.Bounds;
18import org.openstreetmap.josm.data.ProjectionBounds;
19import org.openstreetmap.josm.data.coor.EastNorth;
20import org.openstreetmap.josm.data.coor.LatLon;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.projection.Projecting;
23import org.openstreetmap.josm.data.projection.Projection;
24import org.openstreetmap.josm.gui.download.DownloadDialog;
25import org.openstreetmap.josm.tools.bugreport.BugReport;
26
27/**
28 * This class represents a state of the {@link MapView}.
29 * @author Michael Zangl
30 * @since 10343
31 */
32public final class MapViewState implements Serializable {
33
34 private static final long serialVersionUID = 1L;
35
36 /**
37 * A flag indicating that the point is outside to the top of the map view.
38 * @since 10827
39 */
40 public static final int OUTSIDE_TOP = 1;
41
42 /**
43 * A flag indicating that the point is outside to the bottom of the map view.
44 * @since 10827
45 */
46 public static final int OUTSIDE_BOTTOM = 2;
47
48 /**
49 * A flag indicating that the point is outside to the left of the map view.
50 * @since 10827
51 */
52 public static final int OUTSIDE_LEFT = 4;
53
54 /**
55 * A flag indicating that the point is outside to the right of the map view.
56 * @since 10827
57 */
58 public static final int OUTSIDE_RIGHT = 8;
59
60 private final transient Projecting projecting;
61
62 private final int viewWidth;
63 private final int viewHeight;
64
65 private final double scale;
66
67 /**
68 * Top left {@link EastNorth} coordinate of the view.
69 */
70 private final EastNorth topLeft;
71
72 private final Point topLeftOnScreen;
73 private final Point topLeftInWindow;
74
75 /**
76 * Create a new {@link MapViewState}
77 * @param projection The projection to use.
78 * @param viewWidth The view width
79 * @param viewHeight The view height
80 * @param scale The scale to use
81 * @param topLeft The top left corner in east/north space.
82 */
83 private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) {
84 this.projecting = projection;
85 this.scale = scale;
86 this.topLeft = topLeft;
87
88 this.viewWidth = viewWidth;
89 this.viewHeight = viewHeight;
90 topLeftInWindow = new Point(0, 0);
91 topLeftOnScreen = new Point(0, 0);
92 }
93
94 private MapViewState(EastNorth topLeft, MapViewState mapViewState) {
95 this.projecting = mapViewState.projecting;
96 this.scale = mapViewState.scale;
97 this.topLeft = topLeft;
98
99 viewWidth = mapViewState.viewWidth;
100 viewHeight = mapViewState.viewHeight;
101 topLeftInWindow = mapViewState.topLeftInWindow;
102 topLeftOnScreen = mapViewState.topLeftOnScreen;
103 }
104
105 private MapViewState(double scale, MapViewState mapViewState) {
106 this.projecting = mapViewState.projecting;
107 this.scale = scale;
108 this.topLeft = mapViewState.topLeft;
109
110 viewWidth = mapViewState.viewWidth;
111 viewHeight = mapViewState.viewHeight;
112 topLeftInWindow = mapViewState.topLeftInWindow;
113 topLeftOnScreen = mapViewState.topLeftOnScreen;
114 }
115
116 private MapViewState(JComponent position, MapViewState mapViewState) {
117 this.projecting = mapViewState.projecting;
118 this.scale = mapViewState.scale;
119 this.topLeft = mapViewState.topLeft;
120
121 viewWidth = position.getWidth();
122 viewHeight = position.getHeight();
123 topLeftInWindow = new Point();
124 // better than using swing utils, since this allows us to use the mehtod if no screen is present.
125 Container component = position;
126 while (component != null) {
127 topLeftInWindow.x += component.getX();
128 topLeftInWindow.y += component.getY();
129 component = component.getParent();
130 }
131 try {
132 topLeftOnScreen = position.getLocationOnScreen();
133 } catch (RuntimeException e) {
134 throw BugReport.intercept(e).put("position", position).put("parent", position::getParent);
135 }
136 }
137
138 private MapViewState(Projecting projecting, MapViewState mapViewState) {
139 this.projecting = projecting;
140 this.scale = mapViewState.scale;
141 this.topLeft = mapViewState.topLeft;
142
143 viewWidth = mapViewState.viewWidth;
144 viewHeight = mapViewState.viewHeight;
145 topLeftInWindow = mapViewState.topLeftInWindow;
146 topLeftOnScreen = mapViewState.topLeftOnScreen;
147 }
148
149 /**
150 * The scale in east/north units per pixel.
151 * @return The scale.
152 */
153 public double getScale() {
154 return scale;
155 }
156
157 /**
158 * Gets the MapViewPoint representation for a position in view coordinates.
159 * @param x The x coordinate inside the view.
160 * @param y The y coordinate inside the view.
161 * @return The MapViewPoint.
162 */
163 public MapViewPoint getForView(double x, double y) {
164 return new MapViewViewPoint(x, y);
165 }
166
167 /**
168 * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate.
169 * @param eastNorth the position.
170 * @return The point for that position.
171 */
172 public MapViewPoint getPointFor(EastNorth eastNorth) {
173 return new MapViewEastNorthPoint(eastNorth);
174 }
175
176 /**
177 * Gets the {@link MapViewPoint} for the given {@link LatLon} coordinate.
178 * @param latlon the position
179 * @return The point for that position.
180 * @since 10651
181 */
182 public MapViewPoint getPointFor(LatLon latlon) {
183 return getPointFor(getProjection().latlon2eastNorth(latlon));
184 }
185
186 /**
187 * Gets the {@link MapViewPoint} for the given node. This is faster than {@link #getPointFor(LatLon)} because it uses the node east/north
188 * cache.
189 * @param node The node
190 * @return The position of that node.
191 * @since 10827
192 */
193 public MapViewPoint getPointFor(Node node) {
194 return getPointFor(node.getEastNorth(getProjection()));
195 }
196
197 /**
198 * Gets a rectangle representing the whole view area.
199 * @return The rectangle.
200 */
201 public MapViewRectangle getViewArea() {
202 return getForView(0, 0).rectTo(getForView(viewWidth, viewHeight));
203 }
204
205 /**
206 * Gets a rectangle of the view as map view area.
207 * @param rectangle The rectangle to get.
208 * @return The view area.
209 * @since 10827
210 */
211 public MapViewRectangle getViewArea(Rectangle2D rectangle) {
212 return getForView(rectangle.getMinX(), rectangle.getMinY()).rectTo(getForView(rectangle.getMaxX(), rectangle.getMaxY()));
213 }
214
215 /**
216 * Gets the center of the view.
217 * @return The center position.
218 */
219 public MapViewPoint getCenter() {
220 return getForView(viewWidth / 2.0, viewHeight / 2.0);
221 }
222
223 /**
224 * Gets the center of the view, rounded to a pixel coordinate
225 * @return The center position.
226 * @since 10856
227 */
228 public MapViewPoint getCenterAtPixel() {
229 return getForView(viewWidth / 2, viewHeight / 2);
230 }
231
232 /**
233 * Gets the width of the view on the Screen;
234 * @return The width of the view component in screen pixel.
235 */
236 public double getViewWidth() {
237 return viewWidth;
238 }
239
240 /**
241 * Gets the height of the view on the Screen;
242 * @return The height of the view component in screen pixel.
243 */
244 public double getViewHeight() {
245 return viewHeight;
246 }
247
248 /**
249 * Gets the current projection used for the MapView.
250 * @return The projection.
251 */
252 public Projection getProjection() {
253 return projecting.getBaseProjection();
254 }
255
256 /**
257 * Creates an affine transform that is used to convert the east/north coordinates to view coordinates.
258 * @return The affine transform. It should not be changed.
259 * @since 10375
260 */
261 public AffineTransform getAffineTransform() {
262 return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -topLeft.east() / scale,
263 topLeft.north() / scale);
264 }
265
266 /**
267 * Returns the area for the given bounds.
268 * @param bounds bounds
269 * @return the area for the given bounds
270 */
271 public Area getArea(Bounds bounds) {
272 Path2D area = new Path2D.Double();
273 bounds.visitEdge(getProjection(), latlon -> {
274 MapViewPoint point = getPointFor(latlon);
275 if (area.getCurrentPoint() == null) {
276 area.moveTo(point.getInViewX(), point.getInViewY());
277 } else {
278 area.lineTo(point.getInViewX(), point.getInViewY());
279 }
280 });
281 area.closePath();
282 return new Area(area);
283 }
284
285 /**
286 * Creates a new state that is the same as the current state except for that it is using a new center.
287 * @param newCenter The new center coordinate.
288 * @return The new state.
289 * @since 10375
290 */
291 public MapViewState usingCenter(EastNorth newCenter) {
292 return movedTo(getCenter(), newCenter);
293 }
294
295 /**
296 * @param mapViewPoint The reference point.
297 * @param newEastNorthThere The east/north coordinate that should be there.
298 * @return The new state.
299 * @since 10375
300 */
301 public MapViewState movedTo(MapViewPoint mapViewPoint, EastNorth newEastNorthThere) {
302 EastNorth delta = newEastNorthThere.subtract(mapViewPoint.getEastNorth());
303 if (delta.distanceSq(0, 0) < .1e-20) {
304 return this;
305 } else {
306 return new MapViewState(topLeft.add(delta), this);
307 }
308 }
309
310 /**
311 * Creates a new state that is the same as the current state except for that it is using a new scale.
312 * @param newScale The new scale to use.
313 * @return The new state.
314 * @since 10375
315 */
316 public MapViewState usingScale(double newScale) {
317 return new MapViewState(newScale, this);
318 }
319
320 /**
321 * Creates a new state that is the same as the current state except for that it is using the location of the given component.
322 * <p>
323 * The view is moved so that the center is the same as the old center.
324 * @param positon The new location to use.
325 * @return The new state.
326 * @since 10375
327 */
328 public MapViewState usingLocation(JComponent positon) {
329 EastNorth center = this.getCenter().getEastNorth();
330 return new MapViewState(positon, this).usingCenter(center);
331 }
332
333 /**
334 * Creates a state that uses the projection.
335 * @param projection The projection to use.
336 * @return The new state.
337 * @since 10486
338 */
339 public MapViewState usingProjection(Projection projection) {
340 if (projection.equals(this.projecting)) {
341 return this;
342 } else {
343 return new MapViewState(projection, this);
344 }
345 }
346
347 /**
348 * 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
349 * before the view was added to the hirarchy.
350 * @param width The view width
351 * @param height The view height
352 * @return The state
353 * @since 10375
354 */
355 public static MapViewState createDefaultState(int width, int height) {
356 Projection projection = Main.getProjection();
357 double scale = projection.getDefaultZoomInPPD();
358 MapViewState state = new MapViewState(projection, width, height, scale, new EastNorth(0, 0));
359 EastNorth center = calculateDefaultCenter();
360 return state.movedTo(state.getCenter(), center);
361 }
362
363 private static EastNorth calculateDefaultCenter() {
364 Bounds b = DownloadDialog.getSavedDownloadBounds();
365 if (b == null) {
366 b = Main.getProjection().getWorldBoundsLatLon();
367 }
368 return Main.getProjection().latlon2eastNorth(b.getCenter());
369 }
370
371 /**
372 * A class representing a point in the map view. It allows to convert between the different coordinate systems.
373 * @author Michael Zangl
374 */
375 public abstract class MapViewPoint {
376
377 /**
378 * Get this point in view coordinates.
379 * @return The point in view coordinates.
380 */
381 public Point2D getInView() {
382 return new Point2D.Double(getInViewX(), getInViewY());
383 }
384
385 /**
386 * Get the x coordinate in view space without creating an intermediate object.
387 * @return The x coordinate
388 * @since 10827
389 */
390 public abstract double getInViewX();
391
392 /**
393 * Get the y coordinate in view space without creating an intermediate object.
394 * @return The y coordinate
395 * @since 10827
396 */
397 public abstract double getInViewY();
398
399 /**
400 * Convert this point to window coordinates.
401 * @return The point in window coordinates.
402 */
403 public Point2D getInWindow() {
404 return getUsingCorner(topLeftInWindow);
405 }
406
407 /**
408 * Convert this point to screen coordinates.
409 * @return The point in screen coordinates.
410 */
411 public Point2D getOnScreen() {
412 return getUsingCorner(topLeftOnScreen);
413 }
414
415 private Double getUsingCorner(Point corner) {
416 return new Point2D.Double(corner.getX() + getInViewX(), corner.getY() + getInViewY());
417 }
418
419 /**
420 * Gets the {@link EastNorth} coordinate of this point.
421 * @return The east/north coordinate.
422 */
423 public EastNorth getEastNorth() {
424 return new EastNorth(topLeft.east() + getInViewX() * scale, topLeft.north() - getInViewY() * scale);
425 }
426
427 /**
428 * Create a rectangle from this to the other point.
429 * @param other The other point. Needs to be of the same {@link MapViewState}
430 * @return A rectangle.
431 */
432 public MapViewRectangle rectTo(MapViewPoint other) {
433 return new MapViewRectangle(this, other);
434 }
435
436 /**
437 * Gets the current position in LatLon coordinates according to the current projection.
438 * @return The positon as LatLon.
439 * @see #getLatLonClamped()
440 */
441 public LatLon getLatLon() {
442 return projecting.getBaseProjection().eastNorth2latlon(getEastNorth());
443 }
444
445 /**
446 * Gets the latlon coordinate clamped to the current world area.
447 * @return The lat/lon coordinate
448 * @since 10805
449 */
450 public LatLon getLatLonClamped() {
451 return projecting.eastNorth2latlonClamped(getEastNorth());
452 }
453
454 /**
455 * Add the given offset to this point
456 * @param en The offset in east/north space.
457 * @return The new point
458 * @since 10651
459 */
460 public MapViewPoint add(EastNorth en) {
461 return new MapViewEastNorthPoint(getEastNorth().add(en));
462 }
463
464 /**
465 * Check if this point is inside the view bounds.
466 *
467 * This is the case iff <code>getOutsideRectangleFlags(getViewArea())</code> returns no flags
468 * @return true if it is.
469 * @since 10827
470 */
471 public boolean isInView() {
472 return inRange(getInViewX(), 0, getViewWidth()) && inRange(getInViewY(), 0, getViewHeight());
473 }
474
475 private boolean inRange(double val, int min, double max) {
476 return val >= min && val < max;
477 }
478
479 /**
480 * Gets the direction in which this point is outside of the given view rectangle.
481 * @param rect The rectangle to check agains.
482 * @return The direction in which it is outside of the view, as OUTSIDE_... flags.
483 * @since 10827
484 */
485 public int getOutsideRectangleFlags(MapViewRectangle rect) {
486 Rectangle2D bounds = rect.getInView();
487 int flags = 0;
488 if (getInViewX() < bounds.getMinX()) {
489 flags |= OUTSIDE_LEFT;
490 } else if (getInViewX() > bounds.getMaxX()) {
491 flags |= OUTSIDE_RIGHT;
492 }
493 if (getInViewY() < bounds.getMinY()) {
494 flags |= OUTSIDE_TOP;
495 } else if (getInViewY() > bounds.getMaxY()) {
496 flags |= OUTSIDE_BOTTOM;
497 }
498
499 return flags;
500 }
501
502 /**
503 * Gets the sum of the x/y view distances between the points. |x1 - x2| + |y1 - y2|
504 * @param p2 The other point
505 * @return The norm
506 * @since 10827
507 */
508 public double oneNormInView(MapViewPoint p2) {
509 return Math.abs(getInViewX() - p2.getInViewX()) + Math.abs(getInViewY() - p2.getInViewY());
510 }
511
512 /**
513 * Gets the squared distance between this point and an other point.
514 * @param p2 The other point
515 * @return The squared distance.
516 * @since 10827
517 */
518 public double distanceToInViewSq(MapViewPoint p2) {
519 double dx = getInViewX() - p2.getInViewX();
520 double dy = getInViewY() - p2.getInViewY();
521 return dx * dx + dy * dy;
522 }
523
524 /**
525 * Gets the distance between this point and an other point.
526 * @param p2 The other point
527 * @return The distance.
528 * @since 10827
529 */
530 public double distanceToInView(MapViewPoint p2) {
531 return Math.sqrt(distanceToInViewSq(p2));
532 }
533
534 /**
535 * Do a linear interpolation to the other point
536 * @param p1 The other point
537 * @param i The interpolation factor. 0 is at the current point, 1 at the other point.
538 * @return The new point
539 * @since 10874
540 */
541 public MapViewPoint interpolate(MapViewPoint p1, int i) {
542 return new MapViewViewPoint((1 - i) * getInViewX() + i * p1.getInViewX(), (1 - i) * getInViewY() + i * p1.getInViewY());
543 }
544 }
545
546 private class MapViewViewPoint extends MapViewPoint {
547 private final double x;
548 private final double y;
549
550 MapViewViewPoint(double x, double y) {
551 this.x = x;
552 this.y = y;
553 }
554
555 @Override
556 public double getInViewX() {
557 return x;
558 }
559
560 @Override
561 public double getInViewY() {
562 return y;
563 }
564
565 @Override
566 public String toString() {
567 return "MapViewViewPoint [x=" + x + ", y=" + y + ']';
568 }
569 }
570
571 private class MapViewEastNorthPoint extends MapViewPoint {
572
573 private final EastNorth eastNorth;
574
575 MapViewEastNorthPoint(EastNorth eastNorth) {
576 this.eastNorth = eastNorth;
577 }
578
579 @Override
580 public double getInViewX() {
581 return (eastNorth.east() - topLeft.east()) / scale;
582 }
583
584 @Override
585 public double getInViewY() {
586 return (topLeft.north() - eastNorth.north()) / scale;
587 }
588
589 @Override
590 public EastNorth getEastNorth() {
591 return eastNorth;
592 }
593
594 @Override
595 public String toString() {
596 return "MapViewEastNorthPoint [eastNorth=" + eastNorth + ']';
597 }
598 }
599
600 /**
601 * A rectangle on the MapView. It is rectangular in screen / EastNorth space.
602 * @author Michael Zangl
603 */
604 public class MapViewRectangle {
605 private final MapViewPoint p1;
606 private final MapViewPoint p2;
607
608 /**
609 * Create a new MapViewRectangle
610 * @param p1 The first point to use
611 * @param p2 The second point to use.
612 */
613 MapViewRectangle(MapViewPoint p1, MapViewPoint p2) {
614 this.p1 = p1;
615 this.p2 = p2;
616 }
617
618 /**
619 * Gets the projection bounds for this rectangle.
620 * @return The projection bounds.
621 */
622 public ProjectionBounds getProjectionBounds() {
623 ProjectionBounds b = new ProjectionBounds(p1.getEastNorth());
624 b.extend(p2.getEastNorth());
625 return b;
626 }
627
628 /**
629 * Gets a rough estimate of the bounds by assuming lat/lon are parallel to x/y.
630 * @return The bounds computed by converting the corners of this rectangle.
631 * @see #getLatLonBoundsBox()
632 */
633 public Bounds getCornerBounds() {
634 Bounds b = new Bounds(p1.getLatLon());
635 b.extend(p2.getLatLon());
636 return b;
637 }
638
639 /**
640 * Gets the real bounds that enclose this rectangle.
641 * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates.
642 * @return The bounds.
643 * @since 10458
644 */
645 public Bounds getLatLonBoundsBox() {
646 // TODO @michael2402: Use hillclimb.
647 return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds());
648 }
649
650 /**
651 * Gets this rectangle on the screen.
652 * @return The rectangle.
653 * @since 10651
654 */
655 public Rectangle2D getInView() {
656 double x1 = p1.getInViewX();
657 double y1 = p1.getInViewY();
658 double x2 = p2.getInViewX();
659 double y2 = p2.getInViewY();
660 return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2));
661 }
662
663 /**
664 * Check if the rectangle intersects the map view area.
665 * @return <code>true</code> if it intersects.
666 * @since 10827
667 */
668 public boolean isInView() {
669 return getInView().intersects(getViewArea().getInView());
670 }
671 }
672
673}
Note: See TracBrowser for help on using the repository browser.