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

Last change on this file since 11482 was 11470, checked in by bastiK, 8 years ago

see #13124 - better cache invalidation

Fixes a problem when zoomed out, so all gpx data is in the center of the mapview.
Then moving the map did not update the gpx layer.

  • Property svn:eol-style set to native
File size: 24.6 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;
13import java.util.Objects;
14
15import javax.swing.JComponent;
16
17import org.openstreetmap.josm.Main;
18import org.openstreetmap.josm.data.Bounds;
19import org.openstreetmap.josm.data.ProjectionBounds;
20import org.openstreetmap.josm.data.coor.EastNorth;
21import org.openstreetmap.josm.data.coor.LatLon;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.projection.Projecting;
24import org.openstreetmap.josm.data.projection.Projection;
25import org.openstreetmap.josm.gui.download.DownloadDialog;
26import org.openstreetmap.josm.tools.CheckParameterUtil;
27import org.openstreetmap.josm.tools.Geometry;
28import org.openstreetmap.josm.tools.bugreport.BugReport;
29
30/**
31 * This class represents a state of the {@link MapView}.
32 * @author Michael Zangl
33 * @since 10343
34 */
35public final class MapViewState implements Serializable {
36
37 private static final long serialVersionUID = 1L;
38
39 /**
40 * A flag indicating that the point is outside to the top of the map view.
41 * @since 10827
42 */
43 public static final int OUTSIDE_TOP = 1;
44
45 /**
46 * A flag indicating that the point is outside to the bottom of the map view.
47 * @since 10827
48 */
49 public static final int OUTSIDE_BOTTOM = 2;
50
51 /**
52 * A flag indicating that the point is outside to the left of the map view.
53 * @since 10827
54 */
55 public static final int OUTSIDE_LEFT = 4;
56
57 /**
58 * A flag indicating that the point is outside to the right of the map view.
59 * @since 10827
60 */
61 public static final int OUTSIDE_RIGHT = 8;
62
63 /**
64 * Additional pixels outside the view for where to start clipping.
65 */
66 private static final int CLIP_BOUNDS = 50;
67
68 private final transient Projecting projecting;
69
70 private final int viewWidth;
71 private final int viewHeight;
72
73 private final double scale;
74
75 /**
76 * Top left {@link EastNorth} coordinate of the view.
77 */
78 private final EastNorth topLeft;
79
80 private final Point topLeftOnScreen;
81 private final Point topLeftInWindow;
82
83 /**
84 * Create a new {@link MapViewState}
85 * @param projection The projection to use.
86 * @param viewWidth The view width
87 * @param viewHeight The view height
88 * @param scale The scale to use
89 * @param topLeft The top left corner in east/north space.
90 * @param topLeftInWindow The top left point in window
91 * @param topLeftOnScreen The top left point on screen
92 */
93 private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft,
94 Point topLeftInWindow, Point topLeftOnScreen) {
95 CheckParameterUtil.ensureParameterNotNull(projection, "projection");
96 CheckParameterUtil.ensureParameterNotNull(topLeft, "topLeft");
97 CheckParameterUtil.ensureParameterNotNull(topLeftInWindow, "topLeftInWindow");
98 CheckParameterUtil.ensureParameterNotNull(topLeftOnScreen, "topLeftOnScreen");
99
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 * Check if this MapViewState equals another one, disregarding the position
387 * of the JOSM window on screen.
388 * @param other the other MapViewState
389 * @return true if the other MapViewState has the same size, scale, position and projection,
390 * false otherwise
391 */
392 public boolean equalsInWindow(MapViewState other) {
393 return other != null &&
394 this.viewWidth == other.viewWidth &&
395 this.viewHeight == other.viewHeight &&
396 this.scale == other.scale &&
397 Objects.equals(this.topLeft, other.topLeft) &&
398 Objects.equals(this.projecting, other.projecting);
399 }
400
401 /**
402 * A class representing a point in the map view. It allows to convert between the different coordinate systems.
403 * @author Michael Zangl
404 */
405 public abstract class MapViewPoint {
406
407 /**
408 * Get this point in view coordinates.
409 * @return The point in view coordinates.
410 */
411 public Point2D getInView() {
412 return new Point2D.Double(getInViewX(), getInViewY());
413 }
414
415 /**
416 * Get the x coordinate in view space without creating an intermediate object.
417 * @return The x coordinate
418 * @since 10827
419 */
420 public abstract double getInViewX();
421
422 /**
423 * Get the y coordinate in view space without creating an intermediate object.
424 * @return The y coordinate
425 * @since 10827
426 */
427 public abstract double getInViewY();
428
429 /**
430 * Convert this point to window coordinates.
431 * @return The point in window coordinates.
432 */
433 public Point2D getInWindow() {
434 return getUsingCorner(topLeftInWindow);
435 }
436
437 /**
438 * Convert this point to screen coordinates.
439 * @return The point in screen coordinates.
440 */
441 public Point2D getOnScreen() {
442 return getUsingCorner(topLeftOnScreen);
443 }
444
445 private Double getUsingCorner(Point corner) {
446 return new Point2D.Double(corner.getX() + getInViewX(), corner.getY() + getInViewY());
447 }
448
449 /**
450 * Gets the {@link EastNorth} coordinate of this point.
451 * @return The east/north coordinate.
452 */
453 public EastNorth getEastNorth() {
454 return new EastNorth(topLeft.east() + getInViewX() * scale, topLeft.north() - getInViewY() * scale);
455 }
456
457 /**
458 * Create a rectangle from this to the other point.
459 * @param other The other point. Needs to be of the same {@link MapViewState}
460 * @return A rectangle.
461 */
462 public MapViewRectangle rectTo(MapViewPoint other) {
463 return new MapViewRectangle(this, other);
464 }
465
466 /**
467 * Gets the current position in LatLon coordinates according to the current projection.
468 * @return The positon as LatLon.
469 * @see #getLatLonClamped()
470 */
471 public LatLon getLatLon() {
472 return projecting.getBaseProjection().eastNorth2latlon(getEastNorth());
473 }
474
475 /**
476 * Gets the latlon coordinate clamped to the current world area.
477 * @return The lat/lon coordinate
478 * @since 10805
479 */
480 public LatLon getLatLonClamped() {
481 return projecting.eastNorth2latlonClamped(getEastNorth());
482 }
483
484 /**
485 * Add the given offset to this point
486 * @param en The offset in east/north space.
487 * @return The new point
488 * @since 10651
489 */
490 public MapViewPoint add(EastNorth en) {
491 return new MapViewEastNorthPoint(getEastNorth().add(en));
492 }
493
494 /**
495 * Check if this point is inside the view bounds.
496 *
497 * This is the case iff <code>getOutsideRectangleFlags(getViewArea())</code> returns no flags
498 * @return true if it is.
499 * @since 10827
500 */
501 public boolean isInView() {
502 return inRange(getInViewX(), 0, getViewWidth()) && inRange(getInViewY(), 0, getViewHeight());
503 }
504
505 private boolean inRange(double val, int min, double max) {
506 return val >= min && val < max;
507 }
508
509 /**
510 * Gets the direction in which this point is outside of the given view rectangle.
511 * @param rect The rectangle to check agains.
512 * @return The direction in which it is outside of the view, as OUTSIDE_... flags.
513 * @since 10827
514 */
515 public int getOutsideRectangleFlags(MapViewRectangle rect) {
516 Rectangle2D bounds = rect.getInView();
517 int flags = 0;
518 if (getInViewX() < bounds.getMinX()) {
519 flags |= OUTSIDE_LEFT;
520 } else if (getInViewX() > bounds.getMaxX()) {
521 flags |= OUTSIDE_RIGHT;
522 }
523 if (getInViewY() < bounds.getMinY()) {
524 flags |= OUTSIDE_TOP;
525 } else if (getInViewY() > bounds.getMaxY()) {
526 flags |= OUTSIDE_BOTTOM;
527 }
528
529 return flags;
530 }
531
532 /**
533 * Gets the sum of the x/y view distances between the points. |x1 - x2| + |y1 - y2|
534 * @param p2 The other point
535 * @return The norm
536 * @since 10827
537 */
538 public double oneNormInView(MapViewPoint p2) {
539 return Math.abs(getInViewX() - p2.getInViewX()) + Math.abs(getInViewY() - p2.getInViewY());
540 }
541
542 /**
543 * Gets the squared distance between this point and an other point.
544 * @param p2 The other point
545 * @return The squared distance.
546 * @since 10827
547 */
548 public double distanceToInViewSq(MapViewPoint p2) {
549 double dx = getInViewX() - p2.getInViewX();
550 double dy = getInViewY() - p2.getInViewY();
551 return dx * dx + dy * dy;
552 }
553
554 /**
555 * Gets the distance between this point and an other point.
556 * @param p2 The other point
557 * @return The distance.
558 * @since 10827
559 */
560 public double distanceToInView(MapViewPoint p2) {
561 return Math.sqrt(distanceToInViewSq(p2));
562 }
563
564 /**
565 * Do a linear interpolation to the other point
566 * @param p1 The other point
567 * @param i The interpolation factor. 0 is at the current point, 1 at the other point.
568 * @return The new point
569 * @since 10874
570 */
571 public MapViewPoint interpolate(MapViewPoint p1, double i) {
572 return new MapViewViewPoint((1 - i) * getInViewX() + i * p1.getInViewX(), (1 - i) * getInViewY() + i * p1.getInViewY());
573 }
574 }
575
576 private class MapViewViewPoint extends MapViewPoint {
577 private final double x;
578 private final double y;
579
580 MapViewViewPoint(double x, double y) {
581 this.x = x;
582 this.y = y;
583 }
584
585 @Override
586 public double getInViewX() {
587 return x;
588 }
589
590 @Override
591 public double getInViewY() {
592 return y;
593 }
594
595 @Override
596 public String toString() {
597 return "MapViewViewPoint [x=" + x + ", y=" + y + ']';
598 }
599 }
600
601 private class MapViewEastNorthPoint extends MapViewPoint {
602
603 private final EastNorth eastNorth;
604
605 MapViewEastNorthPoint(EastNorth eastNorth) {
606 this.eastNorth = Objects.requireNonNull(eastNorth, "eastNorth");
607 }
608
609 @Override
610 public double getInViewX() {
611 return (eastNorth.east() - topLeft.east()) / scale;
612 }
613
614 @Override
615 public double getInViewY() {
616 return (topLeft.north() - eastNorth.north()) / scale;
617 }
618
619 @Override
620 public EastNorth getEastNorth() {
621 return eastNorth;
622 }
623
624 @Override
625 public String toString() {
626 return "MapViewEastNorthPoint [eastNorth=" + eastNorth + ']';
627 }
628 }
629
630 /**
631 * A rectangle on the MapView. It is rectangular in screen / EastNorth space.
632 * @author Michael Zangl
633 */
634 public class MapViewRectangle {
635 private final MapViewPoint p1;
636 private final MapViewPoint p2;
637
638 /**
639 * Create a new MapViewRectangle
640 * @param p1 The first point to use
641 * @param p2 The second point to use.
642 */
643 MapViewRectangle(MapViewPoint p1, MapViewPoint p2) {
644 this.p1 = p1;
645 this.p2 = p2;
646 }
647
648 /**
649 * Gets the projection bounds for this rectangle.
650 * @return The projection bounds.
651 */
652 public ProjectionBounds getProjectionBounds() {
653 ProjectionBounds b = new ProjectionBounds(p1.getEastNorth());
654 b.extend(p2.getEastNorth());
655 return b;
656 }
657
658 /**
659 * Gets a rough estimate of the bounds by assuming lat/lon are parallel to x/y.
660 * @return The bounds computed by converting the corners of this rectangle.
661 * @see #getLatLonBoundsBox()
662 */
663 public Bounds getCornerBounds() {
664 Bounds b = new Bounds(p1.getLatLon());
665 b.extend(p2.getLatLon());
666 return b;
667 }
668
669 /**
670 * Gets the real bounds that enclose this rectangle.
671 * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates.
672 * @return The bounds.
673 * @since 10458
674 */
675 public Bounds getLatLonBoundsBox() {
676 // TODO @michael2402: Use hillclimb.
677 return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds());
678 }
679
680 /**
681 * Gets this rectangle on the screen.
682 * @return The rectangle.
683 * @since 10651
684 */
685 public Rectangle2D getInView() {
686 double x1 = p1.getInViewX();
687 double y1 = p1.getInViewY();
688 double x2 = p2.getInViewX();
689 double y2 = p2.getInViewY();
690 return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2));
691 }
692
693 /**
694 * Check if the rectangle intersects the map view area.
695 * @return <code>true</code> if it intersects.
696 * @since 10827
697 */
698 public boolean isInView() {
699 return getInView().intersects(getViewArea().getInView());
700 }
701
702 /**
703 * Gets the entry point at which a line between start and end enters the current view.
704 * @param start The start
705 * @param end The end
706 * @return The entry point or <code>null</code> if the line does not intersect this view.
707 */
708 public MapViewPoint getLineEntry(MapViewPoint start, MapViewPoint end) {
709 ProjectionBounds bounds = getProjectionBounds();
710 if (bounds.contains(start.getEastNorth())) {
711 return start;
712 }
713
714 double dx = end.getEastNorth().east() - start.getEastNorth().east();
715 double boundX = dx > 0 ? bounds.minEast : bounds.maxEast;
716 EastNorth borderIntersection = Geometry.getSegmentSegmentIntersection(start.getEastNorth(), end.getEastNorth(),
717 new EastNorth(boundX, bounds.minNorth),
718 new EastNorth(boundX, bounds.maxNorth));
719 if (borderIntersection != null) {
720 return getPointFor(borderIntersection);
721 }
722
723 double dy = end.getEastNorth().north() - start.getEastNorth().north();
724 double boundY = dy > 0 ? bounds.minNorth : bounds.maxNorth;
725 borderIntersection = Geometry.getSegmentSegmentIntersection(start.getEastNorth(), end.getEastNorth(),
726 new EastNorth(bounds.minEast, boundY),
727 new EastNorth(bounds.maxEast, boundY));
728 if (borderIntersection != null) {
729 return getPointFor(borderIntersection);
730 }
731
732 return null;
733 }
734 }
735
736}
Note: See TracBrowser for help on using the repository browser.