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