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

Last change on this file since 11279 was 11144, checked in by michael2402, 7 years ago

Fix #13793: Clip paths in a way that let's the dashes stay in place.

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