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

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

PMD - Strict Exceptions

  • Property svn:eol-style set to native
File size: 24.8 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. This is faster than {@link #getPointFor(LatLon)} because it uses the node east/north
192 * 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(node.getEastNorth(getProjection()));
200 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
201 throw BugReport.intercept(e).put("node", node);
202 }
203 }
204
205 /**
206 * Gets a rectangle representing the whole view area.
207 * @return The rectangle.
208 */
209 public MapViewRectangle getViewArea() {
210 return getForView(0, 0).rectTo(getForView(viewWidth, viewHeight));
211 }
212
213 /**
214 * Gets a rectangle of the view as map view area.
215 * @param rectangle The rectangle to get.
216 * @return The view area.
217 * @since 10827
218 */
219 public MapViewRectangle getViewArea(Rectangle2D rectangle) {
220 return getForView(rectangle.getMinX(), rectangle.getMinY()).rectTo(getForView(rectangle.getMaxX(), rectangle.getMaxY()));
221 }
222
223 /**
224 * Gets the center of the view.
225 * @return The center position.
226 */
227 public MapViewPoint getCenter() {
228 return getForView(viewWidth / 2.0, viewHeight / 2.0);
229 }
230
231 /**
232 * Gets the center of the view, rounded to a pixel coordinate
233 * @return The center position.
234 * @since 10856
235 */
236 public MapViewPoint getCenterAtPixel() {
237 return getForView(viewWidth / 2, viewHeight / 2);
238 }
239
240 /**
241 * Gets the width of the view on the Screen;
242 * @return The width of the view component in screen pixel.
243 */
244 public double getViewWidth() {
245 return viewWidth;
246 }
247
248 /**
249 * Gets the height of the view on the Screen;
250 * @return The height of the view component in screen pixel.
251 */
252 public double getViewHeight() {
253 return viewHeight;
254 }
255
256 /**
257 * Gets the current projection used for the MapView.
258 * @return The projection.
259 */
260 public Projection getProjection() {
261 return projecting.getBaseProjection();
262 }
263
264 /**
265 * Creates an affine transform that is used to convert the east/north coordinates to view coordinates.
266 * @return The affine transform. It should not be changed.
267 * @since 10375
268 */
269 public AffineTransform getAffineTransform() {
270 return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -topLeft.east() / scale,
271 topLeft.north() / scale);
272 }
273
274 /**
275 * Gets a rectangle that is several pixel bigger than the view. It is used to define the view clipping.
276 * @return The rectangle.
277 */
278 public MapViewRectangle getViewClipRectangle() {
279 return getForView(-CLIP_BOUNDS, -CLIP_BOUNDS).rectTo(getForView(getViewWidth() + CLIP_BOUNDS, getViewHeight() + CLIP_BOUNDS));
280 }
281
282 /**
283 * Returns the area for the given bounds.
284 * @param bounds bounds
285 * @return the area for the given bounds
286 */
287 public Area getArea(Bounds bounds) {
288 Path2D area = new Path2D.Double();
289 bounds.visitEdge(getProjection(), latlon -> {
290 MapViewPoint point = getPointFor(latlon);
291 if (area.getCurrentPoint() == null) {
292 area.moveTo(point.getInViewX(), point.getInViewY());
293 } else {
294 area.lineTo(point.getInViewX(), point.getInViewY());
295 }
296 });
297 area.closePath();
298 return new Area(area);
299 }
300
301 /**
302 * Creates a new state that is the same as the current state except for that it is using a new center.
303 * @param newCenter The new center coordinate.
304 * @return The new state.
305 * @since 10375
306 */
307 public MapViewState usingCenter(EastNorth newCenter) {
308 return movedTo(getCenter(), newCenter);
309 }
310
311 /**
312 * @param mapViewPoint The reference point.
313 * @param newEastNorthThere The east/north coordinate that should be there.
314 * @return The new state.
315 * @since 10375
316 */
317 public MapViewState movedTo(MapViewPoint mapViewPoint, EastNorth newEastNorthThere) {
318 EastNorth delta = newEastNorthThere.subtract(mapViewPoint.getEastNorth());
319 if (delta.distanceSq(0, 0) < .1e-20) {
320 return this;
321 } else {
322 return new MapViewState(topLeft.add(delta), this);
323 }
324 }
325
326 /**
327 * Creates a new state that is the same as the current state except for that it is using a new scale.
328 * @param newScale The new scale to use.
329 * @return The new state.
330 * @since 10375
331 */
332 public MapViewState usingScale(double newScale) {
333 return new MapViewState(newScale, this);
334 }
335
336 /**
337 * Creates a new state that is the same as the current state except for that it is using the location of the given component.
338 * <p>
339 * The view is moved so that the center is the same as the old center.
340 * @param positon The new location to use.
341 * @return The new state.
342 * @since 10375
343 */
344 public MapViewState usingLocation(JComponent positon) {
345 EastNorth center = this.getCenter().getEastNorth();
346 return new MapViewState(positon, this).usingCenter(center);
347 }
348
349 /**
350 * Creates a state that uses the projection.
351 * @param projection The projection to use.
352 * @return The new state.
353 * @since 10486
354 */
355 public MapViewState usingProjection(Projection projection) {
356 if (projection.equals(this.projecting)) {
357 return this;
358 } else {
359 return new MapViewState(projection, this);
360 }
361 }
362
363 /**
364 * 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
365 * before the view was added to the hirarchy.
366 * @param width The view width
367 * @param height The view height
368 * @return The state
369 * @since 10375
370 */
371 public static MapViewState createDefaultState(int width, int height) {
372 Projection projection = Main.getProjection();
373 double scale = projection.getDefaultZoomInPPD();
374 MapViewState state = new MapViewState(projection, width, height, scale, new EastNorth(0, 0));
375 EastNorth center = calculateDefaultCenter();
376 return state.movedTo(state.getCenter(), center);
377 }
378
379 private static EastNorth calculateDefaultCenter() {
380 Bounds b = Optional.ofNullable(DownloadDialog.getSavedDownloadBounds()).orElseGet(
381 () -> Main.getProjection().getWorldBoundsLatLon());
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.