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;
014import java.util.Optional;
015
016import javax.swing.JComponent;
017
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.ILatLon;
022import org.openstreetmap.josm.data.coor.LatLon;
023import org.openstreetmap.josm.data.osm.Node;
024import org.openstreetmap.josm.data.projection.Projecting;
025import org.openstreetmap.josm.data.projection.Projection;
026import org.openstreetmap.josm.data.projection.ProjectionRegistry;
027import org.openstreetmap.josm.gui.download.DownloadDialog;
028import org.openstreetmap.josm.tools.CheckParameterUtil;
029import org.openstreetmap.josm.tools.Geometry;
030import org.openstreetmap.josm.tools.JosmRuntimeException;
031import org.openstreetmap.josm.tools.bugreport.BugReport;
032
033/**
034 * This class represents a state of the {@link MapView}.
035 * @author Michael Zangl
036 * @since 10343
037 */
038public final class MapViewState implements Serializable {
039
040    private static final long serialVersionUID = 1L;
041
042    /**
043     * A flag indicating that the point is outside to the top of the map view.
044     * @since 10827
045     */
046    public static final int OUTSIDE_TOP = 1;
047
048    /**
049     * A flag indicating that the point is outside to the bottom of the map view.
050     * @since 10827
051     */
052    public static final int OUTSIDE_BOTTOM = 2;
053
054    /**
055     * A flag indicating that the point is outside to the left of the map view.
056     * @since 10827
057     */
058    public static final int OUTSIDE_LEFT = 4;
059
060    /**
061     * A flag indicating that the point is outside to the right of the map view.
062     * @since 10827
063     */
064    public static final int OUTSIDE_RIGHT = 8;
065
066    /**
067     * Additional pixels outside the view for where to start clipping.
068     */
069    private static final int CLIP_BOUNDS = 50;
070
071    private final transient Projecting projecting;
072
073    private final int viewWidth;
074    private final int viewHeight;
075
076    private final double scale;
077
078    /**
079     * Top left {@link EastNorth} coordinate of the view.
080     */
081    private final EastNorth topLeft;
082
083    private final Point topLeftOnScreen;
084    private final Point topLeftInWindow;
085
086    /**
087     * Create a new {@link MapViewState}
088     * @param projection The projection to use.
089     * @param viewWidth The view width
090     * @param viewHeight The view height
091     * @param scale The scale to use
092     * @param topLeft The top left corner in east/north space.
093     * @param topLeftInWindow The top left point in window
094     * @param topLeftOnScreen The top left point on screen
095     */
096    private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft,
097            Point topLeftInWindow, Point topLeftOnScreen) {
098        CheckParameterUtil.ensureParameterNotNull(projection, "projection");
099        CheckParameterUtil.ensureParameterNotNull(topLeft, "topLeft");
100        CheckParameterUtil.ensureParameterNotNull(topLeftInWindow, "topLeftInWindow");
101        CheckParameterUtil.ensureParameterNotNull(topLeftOnScreen, "topLeftOnScreen");
102
103        this.projecting = projection;
104        this.scale = scale;
105        this.topLeft = topLeft;
106
107        this.viewWidth = viewWidth;
108        this.viewHeight = viewHeight;
109        this.topLeftInWindow = topLeftInWindow;
110        this.topLeftOnScreen = topLeftOnScreen;
111    }
112
113    private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) {
114        this(projection, viewWidth, viewHeight, scale, topLeft, new Point(0, 0), new Point(0, 0));
115    }
116
117    private MapViewState(EastNorth topLeft, MapViewState mvs) {
118        this(mvs.projecting, mvs.viewWidth, mvs.viewHeight, mvs.scale, topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen);
119    }
120
121    private MapViewState(double scale, MapViewState mvs) {
122        this(mvs.projecting, mvs.viewWidth, mvs.viewHeight, scale, mvs.topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen);
123    }
124
125    private MapViewState(JComponent position, MapViewState mvs) {
126        this(mvs.projecting, position.getWidth(), position.getHeight(), mvs.scale, mvs.topLeft,
127                findTopLeftInWindow(position), findTopLeftOnScreen(position));
128    }
129
130    private MapViewState(Projecting projecting, MapViewState mvs) {
131        this(projecting, mvs.viewWidth, mvs.viewHeight, mvs.scale, mvs.topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen);
132    }
133
134    /**
135     * This is visible for JMockit.
136     *
137     * @param position The component to get the top left position of its window
138     * @return the top left point in window
139     */
140    static Point findTopLeftInWindow(JComponent position) {
141        Point result = new Point();
142        // better than using swing utils, since this allows us to use the method if no screen is present.
143        Container component = position;
144        while (component != null) {
145            result.x += component.getX();
146            result.y += component.getY();
147            component = component.getParent();
148        }
149        return result;
150    }
151
152    /**
153     * This is visible for JMockit.
154     *
155     * @param position The component to get the top left position of its screen
156     * @return the top left point on screen
157     */
158    static Point findTopLeftOnScreen(JComponent position) {
159        try {
160            return position.getLocationOnScreen();
161        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
162            throw BugReport.intercept(e).put("position", position).put("parent", position::getParent);
163        }
164    }
165
166    @Override
167    public String toString() {
168        return getClass().getName() + " [projecting=" + this.projecting
169            + " viewWidth=" + this.viewWidth
170            + " viewHeight=" + this.viewHeight
171            + " scale=" + this.scale
172            + " topLeft=" + this.topLeft + ']';
173    }
174
175    /**
176     * The scale in east/north units per pixel.
177     * @return The scale.
178     */
179    public double getScale() {
180        return scale;
181    }
182
183    /**
184     * Gets the MapViewPoint representation for a position in view coordinates.
185     * @param x The x coordinate inside the view.
186     * @param y The y coordinate inside the view.
187     * @return The MapViewPoint.
188     */
189    public MapViewPoint getForView(double x, double y) {
190        return new MapViewViewPoint(x, y);
191    }
192
193    /**
194     * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate.
195     * @param eastNorth the position.
196     * @return The point for that position.
197     */
198    public MapViewPoint getPointFor(EastNorth eastNorth) {
199        return new MapViewEastNorthPoint(eastNorth);
200    }
201
202    /**
203     * Gets the {@link MapViewPoint} for the given {@link LatLon} coordinate.
204     * <p>
205     * This method exists to not break binary compatibility with old plugins
206     * @param latlon the position
207     * @return The point for that position.
208     * @since 10651
209     */
210    public MapViewPoint getPointFor(LatLon latlon) {
211        return getPointFor((ILatLon) latlon);
212    }
213
214    /**
215     * Gets the {@link MapViewPoint} for the given {@link LatLon} coordinate.
216     * @param latlon the position
217     * @return The point for that position.
218     * @since 12161
219     */
220    public MapViewPoint getPointFor(ILatLon latlon) {
221        try {
222            return getPointFor(Optional.ofNullable(latlon.getEastNorth(getProjection()))
223                    .orElseThrow(IllegalArgumentException::new));
224        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
225            throw BugReport.intercept(e).put("latlon", latlon);
226        }
227    }
228
229    /**
230     * Gets the {@link MapViewPoint} for the given node.
231     * This is faster than {@link #getPointFor(LatLon)} because it uses the node east/north cache.
232     * @param node The node
233     * @return The position of that node.
234     * @since 10827
235     */
236    public MapViewPoint getPointFor(Node node) {
237        return getPointFor((ILatLon) node);
238    }
239
240    /**
241     * Gets a rectangle representing the whole view area.
242     * @return The rectangle.
243     */
244    public MapViewRectangle getViewArea() {
245        return getForView(0, 0).rectTo(getForView(viewWidth, viewHeight));
246    }
247
248    /**
249     * Gets a rectangle of the view as map view area.
250     * @param rectangle The rectangle to get.
251     * @return The view area.
252     * @since 10827
253     */
254    public MapViewRectangle getViewArea(Rectangle2D rectangle) {
255        return getForView(rectangle.getMinX(), rectangle.getMinY()).rectTo(getForView(rectangle.getMaxX(), rectangle.getMaxY()));
256    }
257
258    /**
259     * Gets the center of the view.
260     * @return The center position.
261     */
262    public MapViewPoint getCenter() {
263        return getForView(viewWidth / 2.0, viewHeight / 2.0);
264    }
265
266    /**
267     * Gets the width of the view on the Screen;
268     * @return The width of the view component in screen pixel.
269     */
270    public double getViewWidth() {
271        return viewWidth;
272    }
273
274    /**
275     * Gets the height of the view on the Screen;
276     * @return The height of the view component in screen pixel.
277     */
278    public double getViewHeight() {
279        return viewHeight;
280    }
281
282    /**
283     * Gets the current projection used for the MapView.
284     * @return The projection.
285     * @see #getProjecting()
286     */
287    public Projection getProjection() {
288        return projecting.getBaseProjection();
289    }
290
291    /**
292     * Gets the current projecting instance that is used to convert between east/north and lat/lon space.
293     * @return The projection.
294     * @since 12161
295     */
296    public Projecting getProjecting() {
297        return projecting;
298    }
299
300    /**
301     * Creates an affine transform that is used to convert the east/north coordinates to view coordinates.
302     * @return The affine transform. It should not be changed.
303     * @since 10375
304     */
305    public AffineTransform getAffineTransform() {
306        return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -topLeft.east() / scale,
307                topLeft.north() / scale);
308    }
309
310    /**
311     * Gets a rectangle that is several pixel bigger than the view. It is used to define the view clipping.
312     * @return The rectangle.
313     */
314    public MapViewRectangle getViewClipRectangle() {
315        return getForView(-CLIP_BOUNDS, -CLIP_BOUNDS).rectTo(getForView(getViewWidth() + CLIP_BOUNDS, getViewHeight() + CLIP_BOUNDS));
316    }
317
318    /**
319     * Returns the area for the given bounds.
320     * @param bounds bounds
321     * @return the area for the given bounds
322     */
323    public Area getArea(Bounds bounds) {
324        Path2D area = new Path2D.Double();
325        getProjection().visitOutline(bounds, en -> {
326            MapViewPoint point = getPointFor(en);
327            if (area.getCurrentPoint() == null) {
328                area.moveTo(point.getInViewX(), point.getInViewY());
329            } else {
330                area.lineTo(point.getInViewX(), point.getInViewY());
331            }
332        });
333        area.closePath();
334        return new Area(area);
335    }
336
337    /**
338     * Creates a new state that is the same as the current state except for that it is using a new center.
339     * @param newCenter The new center coordinate.
340     * @return The new state.
341     * @since 10375
342     */
343    public MapViewState usingCenter(EastNorth newCenter) {
344        return movedTo(getCenter(), newCenter);
345    }
346
347    /**
348     * Creates a new state that is moved to an east/north coordinate.
349     * @param mapViewPoint The reference point.
350     * @param newEastNorthThere The east/north coordinate that should be there.
351     * @return The new state.
352     * @since 10375
353     */
354    public MapViewState movedTo(MapViewPoint mapViewPoint, EastNorth newEastNorthThere) {
355        EastNorth delta = newEastNorthThere.subtract(mapViewPoint.getEastNorth());
356        if (delta.distanceSq(0, 0) < .1e-20) {
357            return this;
358        } else {
359            return new MapViewState(topLeft.add(delta), this);
360        }
361    }
362
363    /**
364     * Creates a new state that is the same as the current state except for that it is using a new scale.
365     * @param newScale The new scale to use.
366     * @return The new state.
367     * @since 10375
368     */
369    public MapViewState usingScale(double newScale) {
370        return new MapViewState(newScale, this);
371    }
372
373    /**
374     * Creates a new state that is the same as the current state except for that it is using the location of the given component.
375     * <p>
376     * The view is moved so that the center is the same as the old center.
377     * @param position The new location to use.
378     * @return The new state.
379     * @since 10375
380     */
381    public MapViewState usingLocation(JComponent position) {
382        EastNorth center = this.getCenter().getEastNorth();
383        return new MapViewState(position, this).usingCenter(center);
384    }
385
386    /**
387     * Creates a state that uses the projection.
388     * @param projection The projection to use.
389     * @return The new state.
390     * @since 10486
391     */
392    public MapViewState usingProjection(Projection projection) {
393        if (projection.equals(this.projecting)) {
394            return this;
395        } else {
396            return new MapViewState(projection, this);
397        }
398    }
399
400    /**
401     * 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
402     * before the view was added to the hierarchy.
403     * @param width The view width
404     * @param height The view height
405     * @return The state
406     * @since 10375
407     */
408    public static MapViewState createDefaultState(int width, int height) {
409        Projection projection = ProjectionRegistry.getProjection();
410        double scale = projection.getDefaultZoomInPPD();
411        MapViewState state = new MapViewState(projection, width, height, scale, new EastNorth(0, 0));
412        EastNorth center = calculateDefaultCenter();
413        return state.movedTo(state.getCenter(), center);
414    }
415
416    private static EastNorth calculateDefaultCenter() {
417        Bounds b = Optional.ofNullable(DownloadDialog.getSavedDownloadBounds()).orElseGet(
418                () -> ProjectionRegistry.getProjection().getWorldBoundsLatLon());
419        return b.getCenter().getEastNorth(ProjectionRegistry.getProjection());
420    }
421
422    /**
423     * Check if this MapViewState equals another one, disregarding the position
424     * of the JOSM window on screen.
425     * @param other the other MapViewState
426     * @return true if the other MapViewState has the same size, scale, position and projection,
427     * false otherwise
428     */
429    public boolean equalsInWindow(MapViewState other) {
430        return other != null &&
431                this.viewWidth == other.viewWidth &&
432                this.viewHeight == other.viewHeight &&
433                this.scale == other.scale &&
434                Objects.equals(this.topLeft, other.topLeft) &&
435                Objects.equals(this.projecting, other.projecting);
436    }
437
438    /**
439     * A class representing a point in the map view. It allows to convert between the different coordinate systems.
440     * @author Michael Zangl
441     */
442    public abstract class MapViewPoint {
443        /**
444         * Gets the map view state this path is used for.
445         * @return The state.
446         * @since 12505
447         */
448        public MapViewState getMapViewState() {
449            return MapViewState.this;
450        }
451
452        /**
453         * Get this point in view coordinates.
454         * @return The point in view coordinates.
455         */
456        public Point2D getInView() {
457            return new Point2D.Double(getInViewX(), getInViewY());
458        }
459
460        /**
461         * Get the x coordinate in view space without creating an intermediate object.
462         * @return The x coordinate
463         * @since 10827
464         */
465        public abstract double getInViewX();
466
467        /**
468         * Get the y coordinate in view space without creating an intermediate object.
469         * @return The y coordinate
470         * @since 10827
471         */
472        public abstract double getInViewY();
473
474        /**
475         * Convert this point to window coordinates.
476         * @return The point in window coordinates.
477         */
478        public Point2D getInWindow() {
479            return getUsingCorner(topLeftInWindow);
480        }
481
482        /**
483         * Convert this point to screen coordinates.
484         * @return The point in screen coordinates.
485         */
486        public Point2D getOnScreen() {
487            return getUsingCorner(topLeftOnScreen);
488        }
489
490        private Double getUsingCorner(Point corner) {
491            return new Point2D.Double(corner.getX() + getInViewX(), corner.getY() + getInViewY());
492        }
493
494        /**
495         * Gets the {@link EastNorth} coordinate of this point.
496         * @return The east/north coordinate.
497         */
498        public EastNorth getEastNorth() {
499            return new EastNorth(topLeft.east() + getInViewX() * scale, topLeft.north() - getInViewY() * scale);
500        }
501
502        /**
503         * Create a rectangle from this to the other point.
504         * @param other The other point. Needs to be of the same {@link MapViewState}
505         * @return A rectangle.
506         */
507        public MapViewRectangle rectTo(MapViewPoint other) {
508            return new MapViewRectangle(this, other);
509        }
510
511        /**
512         * Gets the current position in LatLon coordinates according to the current projection.
513         * @return The position as LatLon.
514         * @see #getLatLonClamped()
515         */
516        public LatLon getLatLon() {
517            return projecting.getBaseProjection().eastNorth2latlon(getEastNorth());
518        }
519
520        /**
521         * Gets the latlon coordinate clamped to the current world area.
522         * @return The lat/lon coordinate
523         * @since 10805
524         */
525        public LatLon getLatLonClamped() {
526            return projecting.eastNorth2latlonClamped(getEastNorth());
527        }
528
529        /**
530         * Add the given offset to this point
531         * @param en The offset in east/north space.
532         * @return The new point
533         * @since 10651
534         */
535        public MapViewPoint add(EastNorth en) {
536            return new MapViewEastNorthPoint(getEastNorth().add(en));
537        }
538
539        /**
540         * Check if this point is inside the view bounds.
541         *
542         * This is the case iff <code>getOutsideRectangleFlags(getViewArea())</code> returns no flags
543         * @return true if it is.
544         * @since 10827
545         */
546        public boolean isInView() {
547            return inRange(getInViewX(), 0, getViewWidth()) && inRange(getInViewY(), 0, getViewHeight());
548        }
549
550        private boolean inRange(double val, int min, double max) {
551            return val >= min && val < max;
552        }
553
554        /**
555         * Gets the direction in which this point is outside of the given view rectangle.
556         * @param rect The rectangle to check against.
557         * @return The direction in which it is outside of the view, as OUTSIDE_... flags.
558         * @since 10827
559         */
560        public int getOutsideRectangleFlags(MapViewRectangle rect) {
561            int flags = 0;
562            double inViewX = getInViewX();
563            if (inViewX < rect.getInViewMinX()) {
564                flags |= OUTSIDE_LEFT;
565            } else if (inViewX > rect.getInViewMaxX()) {
566                flags |= OUTSIDE_RIGHT;
567            }
568            double inViewY = getInViewY();
569            if (inViewY < rect.getInViewMinY()) {
570                flags |= OUTSIDE_TOP;
571            } else if (inViewY > rect.getInViewMaxY()) {
572                flags |= OUTSIDE_BOTTOM;
573            }
574
575            return flags;
576        }
577
578        /**
579         * Gets the sum of the x/y view distances between the points. |x1 - x2| + |y1 - y2|
580         * @param p2 The other point
581         * @return The norm
582         * @since 10827
583         */
584        public double oneNormInView(MapViewPoint p2) {
585            return Math.abs(getInViewX() - p2.getInViewX()) + Math.abs(getInViewY() - p2.getInViewY());
586        }
587
588        /**
589         * Gets the squared distance between this point and an other point.
590         * @param p2 The other point
591         * @return The squared distance.
592         * @since 10827
593         */
594        public double distanceToInViewSq(MapViewPoint p2) {
595            double dx = getInViewX() - p2.getInViewX();
596            double dy = getInViewY() - p2.getInViewY();
597            return dx * dx + dy * dy;
598        }
599
600        /**
601         * Gets the distance between this point and an other point.
602         * @param p2 The other point
603         * @return The distance.
604         * @since 10827
605         */
606        public double distanceToInView(MapViewPoint p2) {
607            return Math.sqrt(distanceToInViewSq(p2));
608        }
609
610        /**
611         * Do a linear interpolation to the other point
612         * @param p1 The other point
613         * @param i The interpolation factor. 0 is at the current point, 1 at the other point.
614         * @return The new point
615         * @since 10874
616         */
617        public MapViewPoint interpolate(MapViewPoint p1, double i) {
618            return new MapViewViewPoint((1 - i) * getInViewX() + i * p1.getInViewX(), (1 - i) * getInViewY() + i * p1.getInViewY());
619        }
620    }
621
622    private class MapViewViewPoint extends MapViewPoint {
623        private final double x;
624        private final double y;
625
626        MapViewViewPoint(double x, double y) {
627            this.x = x;
628            this.y = y;
629        }
630
631        @Override
632        public double getInViewX() {
633            return x;
634        }
635
636        @Override
637        public double getInViewY() {
638            return y;
639        }
640
641        @Override
642        public String toString() {
643            return "MapViewViewPoint [x=" + x + ", y=" + y + ']';
644        }
645    }
646
647    private class MapViewEastNorthPoint extends MapViewPoint {
648
649        private final EastNorth eastNorth;
650
651        MapViewEastNorthPoint(EastNorth eastNorth) {
652            this.eastNorth = Objects.requireNonNull(eastNorth, "eastNorth");
653        }
654
655        @Override
656        public double getInViewX() {
657            return (eastNorth.east() - topLeft.east()) / scale;
658        }
659
660        @Override
661        public double getInViewY() {
662            return (topLeft.north() - eastNorth.north()) / scale;
663        }
664
665        @Override
666        public EastNorth getEastNorth() {
667            return eastNorth;
668        }
669
670        @Override
671        public String toString() {
672            return "MapViewEastNorthPoint [eastNorth=" + eastNorth + ']';
673        }
674    }
675
676    /**
677     * A rectangle on the MapView. It is rectangular in screen / EastNorth space.
678     * @author Michael Zangl
679     */
680    public class MapViewRectangle {
681        private final MapViewPoint p1;
682        private final MapViewPoint p2;
683
684        /**
685         * Create a new MapViewRectangle
686         * @param p1 The first point to use
687         * @param p2 The second point to use.
688         */
689        MapViewRectangle(MapViewPoint p1, MapViewPoint p2) {
690            this.p1 = p1;
691            this.p2 = p2;
692        }
693
694        /**
695         * Gets the projection bounds for this rectangle.
696         * @return The projection bounds.
697         */
698        public ProjectionBounds getProjectionBounds() {
699            ProjectionBounds b = new ProjectionBounds(p1.getEastNorth());
700            b.extend(p2.getEastNorth());
701            return b;
702        }
703
704        /**
705         * Gets a rough estimate of the bounds by assuming lat/lon are parallel to x/y.
706         * @return The bounds computed by converting the corners of this rectangle.
707         * @see #getLatLonBoundsBox()
708         */
709        public Bounds getCornerBounds() {
710            Bounds b = new Bounds(p1.getLatLon());
711            b.extend(p2.getLatLon());
712            return b;
713        }
714
715        /**
716         * Gets the real bounds that enclose this rectangle.
717         * This is computed respecting that the borders of this rectangle may not be a straight line in latlon coordinates.
718         * @return The bounds.
719         * @since 10458
720         */
721        public Bounds getLatLonBoundsBox() {
722            // TODO @michael2402: Use hillclimb.
723            return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds());
724        }
725
726        /**
727         * Gets this rectangle on the screen.
728         * @return The rectangle.
729         * @since 10651
730         */
731        public Rectangle2D getInView() {
732            double x1 = p1.getInViewX();
733            double y1 = p1.getInViewY();
734            double x2 = p2.getInViewX();
735            double y2 = p2.getInViewY();
736            return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2));
737        }
738
739        double getInViewMinX() {
740            return Math.min(p1.getInViewX(), p2.getInViewX());
741        }
742
743        double getInViewMaxX() {
744            return Math.max(p1.getInViewX(), p2.getInViewX());
745        }
746
747        double getInViewMinY() {
748            return Math.min(p1.getInViewY(), p2.getInViewY());
749        }
750
751        double getInViewMaxY() {
752            return Math.max(p1.getInViewY(), p2.getInViewY());
753        }
754
755        /**
756         * Check if the rectangle intersects the map view area.
757         * @return <code>true</code> if it intersects.
758         * @since 10827
759         */
760        public boolean isInView() {
761            return getInView().intersects(getViewArea().getInView());
762        }
763
764        /**
765         * Gets the entry point at which a line between start and end enters the current view.
766         * @param start The start
767         * @param end The end
768         * @return The entry point or <code>null</code> if the line does not intersect this view.
769         */
770        public MapViewPoint getLineEntry(MapViewPoint start, MapViewPoint end) {
771            ProjectionBounds bounds = getProjectionBounds();
772            EastNorth enStart = start.getEastNorth();
773            if (bounds.contains(enStart)) {
774                return start;
775            }
776
777            EastNorth enEnd = end.getEastNorth();
778            double dx = enEnd.east() - enStart.east();
779            double boundX = dx > 0 ? bounds.minEast : bounds.maxEast;
780            EastNorth borderIntersection = Geometry.getSegmentSegmentIntersection(enStart, enEnd,
781                    new EastNorth(boundX, bounds.minNorth),
782                    new EastNorth(boundX, bounds.maxNorth));
783            if (borderIntersection != null) {
784                return getPointFor(borderIntersection);
785            }
786
787            double dy = enEnd.north() - enStart.north();
788            double boundY = dy > 0 ? bounds.minNorth : bounds.maxNorth;
789            borderIntersection = Geometry.getSegmentSegmentIntersection(enStart, enEnd,
790                    new EastNorth(bounds.minEast, boundY),
791                    new EastNorth(bounds.maxEast, boundY));
792            if (borderIntersection != null) {
793                return getPointFor(borderIntersection);
794            }
795
796            return null;
797        }
798
799        @Override
800        public String toString() {
801            return "MapViewRectangle [p1=" + p1 + ", p2=" + p2 + ']';
802        }
803    }
804}