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

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

see #13537 - refactor constructors and add parameters validation to detect root cause

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