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

Last change on this file since 11114 was 11026, checked in by michael2402, 8 years ago

See #13636: Add automated clipping to MapViewPath.

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