001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import java.awt.Container;
005import java.awt.Point;
006import java.awt.geom.AffineTransform;
007import java.awt.geom.Area;
008import java.awt.geom.Path2D;
009import java.awt.geom.Point2D;
010import java.awt.geom.Point2D.Double;
011import java.awt.geom.Rectangle2D;
012import java.io.Serializable;
013import java.util.Objects;
014
015import javax.swing.JComponent;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.Bounds;
019import org.openstreetmap.josm.data.ProjectionBounds;
020import org.openstreetmap.josm.data.coor.EastNorth;
021import org.openstreetmap.josm.data.coor.LatLon;
022import org.openstreetmap.josm.data.osm.Node;
023import org.openstreetmap.josm.data.projection.Projecting;
024import org.openstreetmap.josm.data.projection.Projection;
025import org.openstreetmap.josm.gui.download.DownloadDialog;
026import org.openstreetmap.josm.tools.CheckParameterUtil;
027import org.openstreetmap.josm.tools.Geometry;
028import org.openstreetmap.josm.tools.bugreport.BugReport;
029
030/**
031 * This class represents a state of the {@link MapView}.
032 * @author Michael Zangl
033 * @since 10343
034 */
035public final class MapViewState implements Serializable {
036
037    private static final long serialVersionUID = 1L;
038
039    /**
040     * A flag indicating that the point is outside to the top of the map view.
041     * @since 10827
042     */
043    public static final int OUTSIDE_TOP = 1;
044
045    /**
046     * A flag indicating that the point is outside to the bottom of the map view.
047     * @since 10827
048     */
049    public static final int OUTSIDE_BOTTOM = 2;
050
051    /**
052     * A flag indicating that the point is outside to the left of the map view.
053     * @since 10827
054     */
055    public static final int OUTSIDE_LEFT = 4;
056
057    /**
058     * A flag indicating that the point is outside to the right of the map view.
059     * @since 10827
060     */
061    public static final int OUTSIDE_RIGHT = 8;
062
063    /**
064     * Additional pixels outside the view for where to start clipping.
065     */
066    private static final int CLIP_BOUNDS = 50;
067
068    private final transient Projecting projecting;
069
070    private final int viewWidth;
071    private final int viewHeight;
072
073    private final double scale;
074
075    /**
076     * Top left {@link EastNorth} coordinate of the view.
077     */
078    private final EastNorth topLeft;
079
080    private final Point topLeftOnScreen;
081    private final Point topLeftInWindow;
082
083    /**
084     * Create a new {@link MapViewState}
085     * @param projection The projection to use.
086     * @param viewWidth The view width
087     * @param viewHeight The view height
088     * @param scale The scale to use
089     * @param topLeft The top left corner in east/north space.
090     * @param topLeftInWindow The top left point in window
091     * @param topLeftOnScreen The top left point on screen
092     */
093    private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft,
094            Point topLeftInWindow, Point topLeftOnScreen) {
095        CheckParameterUtil.ensureParameterNotNull(projection, "projection");
096        CheckParameterUtil.ensureParameterNotNull(topLeft, "topLeft");
097        CheckParameterUtil.ensureParameterNotNull(topLeftInWindow, "topLeftInWindow");
098        CheckParameterUtil.ensureParameterNotNull(topLeftOnScreen, "topLeftOnScreen");
099
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}