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

Last change on this file since 11710 was 11553, checked in by Don-vip, 7 years ago

refactor handling of null values - use Java 8 Optional where possible

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