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

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

fixed #14740 - Current displayed data position changes when you press TAB

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