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.Rectangle;
007import java.awt.geom.AffineTransform;
008import java.awt.geom.Point2D;
009import java.awt.geom.Point2D.Double;
010
011import javax.swing.JComponent;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.data.Bounds;
015import org.openstreetmap.josm.data.ProjectionBounds;
016import org.openstreetmap.josm.data.coor.EastNorth;
017import org.openstreetmap.josm.data.coor.LatLon;
018import org.openstreetmap.josm.data.projection.Projection;
019import org.openstreetmap.josm.gui.download.DownloadDialog;
020
021/**
022 * This class represents a state of the {@link MapView}.
023 * @author Michael Zangl
024 * @since 10343
025 */
026public final class MapViewState {
027
028    private final Projection projection;
029
030    private final int viewWidth;
031    private final int viewHeight;
032
033    private final double scale;
034
035    /**
036     * Top left {@link EastNorth} coordinate of the view.
037     */
038    private final EastNorth topLeft;
039
040    private final Point topLeftOnScreen;
041    private final Point topLeftInWindow;
042
043    /**
044     * Create a new {@link MapViewState}
045     * @param projection The projection to use.
046     * @param viewWidth The view width
047     * @param viewHeight The view height
048     * @param scale The scale to use
049     * @param topLeft The top left corner in east/north space.
050     */
051    private MapViewState(Projection projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) {
052        this.projection = projection;
053        this.scale = scale;
054        this.topLeft = topLeft;
055
056        this.viewWidth = viewWidth;
057        this.viewHeight = viewHeight;
058        topLeftInWindow = new Point(0, 0);
059        topLeftOnScreen = new Point(0, 0);
060    }
061
062    private MapViewState(EastNorth topLeft, MapViewState mapViewState) {
063        this.projection = mapViewState.projection;
064        this.scale = mapViewState.scale;
065        this.topLeft = topLeft;
066
067        viewWidth = mapViewState.viewWidth;
068        viewHeight = mapViewState.viewHeight;
069        topLeftInWindow = mapViewState.topLeftInWindow;
070        topLeftOnScreen = mapViewState.topLeftOnScreen;
071    }
072
073    private MapViewState(double scale, MapViewState mapViewState) {
074        this.projection = mapViewState.projection;
075        this.scale = scale;
076        this.topLeft = mapViewState.topLeft;
077
078        viewWidth = mapViewState.viewWidth;
079        viewHeight = mapViewState.viewHeight;
080        topLeftInWindow = mapViewState.topLeftInWindow;
081        topLeftOnScreen = mapViewState.topLeftOnScreen;
082    }
083
084    private MapViewState(JComponent position, MapViewState mapViewState) {
085        this.projection = mapViewState.projection;
086        this.scale = mapViewState.scale;
087        this.topLeft = mapViewState.topLeft;
088
089        viewWidth = position.getWidth();
090        viewHeight = position.getHeight();
091        topLeftInWindow = new Point();
092        // better than using swing utils, since this allows us to use the mehtod if no screen is present.
093        Container component = position;
094        while (component != null) {
095            topLeftInWindow.x += component.getX();
096            topLeftInWindow.y += component.getY();
097            component = component.getParent();
098        }
099        topLeftOnScreen = position.getLocationOnScreen();
100    }
101
102    private MapViewState(Projection projection, MapViewState mapViewState) {
103        this.projection = projection;
104        this.scale = mapViewState.scale;
105        this.topLeft = mapViewState.topLeft;
106
107        viewWidth = mapViewState.viewWidth;
108        viewHeight = mapViewState.viewHeight;
109        topLeftInWindow = mapViewState.topLeftInWindow;
110        topLeftOnScreen = mapViewState.topLeftOnScreen;
111    }
112
113    /**
114     * The scale in east/north units per pixel.
115     * @return The scale.
116     */
117    public double getScale() {
118        return scale;
119    }
120
121    /**
122     * Gets the MapViewPoint representation for a position in view coordinates.
123     * @param x The x coordinate inside the view.
124     * @param y The y coordinate inside the view.
125     * @return The MapViewPoint.
126     */
127    public MapViewPoint getForView(double x, double y) {
128        return new MapViewViewPoint(x, y);
129    }
130
131    /**
132     * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate.
133     * @param eastNorth the position.
134     * @return The point for that position.
135     */
136    public MapViewPoint getPointFor(EastNorth eastNorth) {
137        return new MapViewEastNorthPoint(eastNorth);
138    }
139
140    /**
141     * Gets a rectangle representing the whole view area.
142     * @return The rectangle.
143     */
144    public MapViewRectangle getViewArea() {
145        return getForView(0, 0).rectTo(getForView(viewWidth, viewHeight));
146    }
147
148    /**
149     * Gets a rectangle of the view as map view area.
150     * @param rectangle The rectangle to get.
151     * @return The view area.
152     * @since 10458
153     */
154    public MapViewRectangle getViewArea(Rectangle rectangle) {
155        return getForView(rectangle.getMinX(), rectangle.getMinY()).rectTo(getForView(rectangle.getMaxX(), rectangle.getMaxY()));
156    }
157
158    /**
159     * Gets the center of the view.
160     * @return The center position.
161     */
162    public MapViewPoint getCenter() {
163        return getForView(viewWidth / 2.0, viewHeight / 2.0);
164    }
165
166    /**
167     * Gets the width of the view on the Screen;
168     * @return The width of the view component in screen pixel.
169     */
170    public double getViewWidth() {
171        return viewWidth;
172    }
173
174    /**
175     * Gets the height of the view on the Screen;
176     * @return The height of the view component in screen pixel.
177     */
178    public double getViewHeight() {
179        return viewHeight;
180    }
181
182    /**
183     * Gets the current projection used for the MapView.
184     * @return The projection.
185     */
186    public Projection getProjection() {
187        return projection;
188    }
189
190    /**
191     * Creates an affine transform that is used to convert the east/north coordinates to view coordinates.
192     * @return The affine transform. It should not be changed.
193     * @since 10375
194     */
195    public AffineTransform getAffineTransform() {
196        return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -topLeft.east() / scale,
197                topLeft.north() / scale);
198    }
199
200    /**
201     * Creates a new state that is the same as the current state except for that it is using a new center.
202     * @param newCenter The new center coordinate.
203     * @return The new state.
204     * @since 10375
205     */
206    public MapViewState usingCenter(EastNorth newCenter) {
207        return movedTo(getCenter(), newCenter);
208    }
209
210    /**
211     * @param mapViewPoint The reference point.
212     * @param newEastNorthThere The east/north coordinate that should be there.
213     * @return The new state.
214     * @since 10375
215     */
216    public MapViewState movedTo(MapViewPoint mapViewPoint, EastNorth newEastNorthThere) {
217        EastNorth delta = newEastNorthThere.subtract(mapViewPoint.getEastNorth());
218        if (delta.distanceSq(0, 0) < .1e-20) {
219            return this;
220        } else {
221            return new MapViewState(topLeft.add(delta), this);
222        }
223    }
224
225    /**
226     * Creates a new state that is the same as the current state except for that it is using a new scale.
227     * @param newScale The new scale to use.
228     * @return The new state.
229     * @since 10375
230     */
231    public MapViewState usingScale(double newScale) {
232        return new MapViewState(newScale, this);
233    }
234
235    /**
236     * Creates a new state that is the same as the current state except for that it is using the location of the given component.
237     * <p>
238     * The view is moved so that the center is the same as the old center.
239     * @param positon The new location to use.
240     * @return The new state.
241     * @since 10375
242     */
243    public MapViewState usingLocation(JComponent positon) {
244        EastNorth center = this.getCenter().getEastNorth();
245        return new MapViewState(positon, this).usingCenter(center);
246    }
247
248    /**
249     * Creates a state that uses the projection.
250     * @param projection The projection to use.
251     * @return The new state.
252     * @since 10486
253     */
254    public MapViewState usingProjection(Projection projection) {
255        if (projection.equals(this.projection)) {
256            return this;
257        } else {
258            return new MapViewState(projection, this);
259        }
260    }
261
262    /**
263     * 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
264     * before the view was added to the hirarchy.
265     * @param width The view width
266     * @param height The view height
267     * @return The state
268     * @since 10375
269     */
270    public static MapViewState createDefaultState(int width, int height) {
271        Projection projection = Main.getProjection();
272        double scale = projection.getDefaultZoomInPPD();
273        MapViewState state = new MapViewState(projection, width, height, scale, new EastNorth(0, 0));
274        EastNorth center = calculateDefaultCenter();
275        return state.movedTo(state.getCenter(), center);
276    }
277
278    private static EastNorth calculateDefaultCenter() {
279        Bounds b = DownloadDialog.getSavedDownloadBounds();
280        if (b == null) {
281            b = Main.getProjection().getWorldBoundsLatLon();
282        }
283        return Main.getProjection().latlon2eastNorth(b.getCenter());
284    }
285
286    /**
287     * A class representing a point in the map view. It allows to convert between the different coordinate systems.
288     * @author Michael Zangl
289     */
290    public abstract class MapViewPoint {
291
292        /**
293         * Get this point in view coordinates.
294         * @return The point in view coordinates.
295         */
296        public Point2D getInView() {
297            return new Point2D.Double(getInViewX(), getInViewY());
298        }
299
300        protected abstract double getInViewX();
301
302        protected abstract double getInViewY();
303
304        /**
305         * Convert this point to window coordinates.
306         * @return The point in window coordinates.
307         */
308        public Point2D getInWindow() {
309            return getUsingCorner(topLeftInWindow);
310        }
311
312        /**
313         * Convert this point to screen coordinates.
314         * @return The point in screen coordinates.
315         */
316        public Point2D getOnScreen() {
317            return getUsingCorner(topLeftOnScreen);
318        }
319
320        private Double getUsingCorner(Point corner) {
321            return new Point2D.Double(corner.getX() + getInViewX(), corner.getY() + getInViewY());
322        }
323
324        /**
325         * Gets the {@link EastNorth} coordinate of this point.
326         * @return The east/north coordinate.
327         */
328        public EastNorth getEastNorth() {
329            return new EastNorth(topLeft.east() + getInViewX() * scale, topLeft.north() - getInViewY() * scale);
330        }
331
332        /**
333         * Create a rectangle from this to the other point.
334         * @param other The other point. Needs to be of the same {@link MapViewState}
335         * @return A rectangle.
336         */
337        public MapViewRectangle rectTo(MapViewPoint other) {
338            return new MapViewRectangle(this, other);
339        }
340
341        /**
342         * Gets the current position in LatLon coordinates according to the current projection.
343         * @return The positon as LatLon.
344         */
345        public LatLon getLatLon() {
346            return projection.eastNorth2latlon(getEastNorth());
347        }
348    }
349
350    private class MapViewViewPoint extends MapViewPoint {
351        private final double x;
352        private final double y;
353
354        MapViewViewPoint(double x, double y) {
355            this.x = x;
356            this.y = y;
357        }
358
359        @Override
360        protected double getInViewX() {
361            return x;
362        }
363
364        @Override
365        protected double getInViewY() {
366            return y;
367        }
368
369        @Override
370        public String toString() {
371            return "MapViewViewPoint [x=" + x + ", y=" + y + ']';
372        }
373    }
374
375    private class MapViewEastNorthPoint extends MapViewPoint {
376
377        private final EastNorth eastNorth;
378
379        MapViewEastNorthPoint(EastNorth eastNorth) {
380            this.eastNorth = eastNorth;
381        }
382
383        @Override
384        protected double getInViewX() {
385            return (eastNorth.east() - topLeft.east()) / scale;
386        }
387
388        @Override
389        protected double getInViewY() {
390            return (topLeft.north() - eastNorth.north()) / scale;
391        }
392
393        @Override
394        public EastNorth getEastNorth() {
395            return eastNorth;
396        }
397
398        @Override
399        public String toString() {
400            return "MapViewEastNorthPoint [eastNorth=" + eastNorth + ']';
401        }
402    }
403
404    /**
405     * A rectangle on the MapView. It is rectangular in screen / EastNorth space.
406     * @author Michael Zangl
407     */
408    public class MapViewRectangle {
409        private final MapViewPoint p1;
410        private final MapViewPoint p2;
411
412        /**
413         * Create a new MapViewRectangle
414         * @param p1 The first point to use
415         * @param p2 The second point to use.
416         */
417        MapViewRectangle(MapViewPoint p1, MapViewPoint p2) {
418            this.p1 = p1;
419            this.p2 = p2;
420        }
421
422        /**
423         * Gets the projection bounds for this rectangle.
424         * @return The projection bounds.
425         */
426        public ProjectionBounds getProjectionBounds() {
427            ProjectionBounds b = new ProjectionBounds(p1.getEastNorth());
428            b.extend(p2.getEastNorth());
429            return b;
430        }
431
432        /**
433         * Gets a rough estimate of the bounds by assuming lat/lon are parallel to x/y.
434         * @return The bounds computed by converting the corners of this rectangle.
435         * @see #getLatLonBoundsBox()
436         */
437        public Bounds getCornerBounds() {
438            Bounds b = new Bounds(p1.getLatLon());
439            b.extend(p2.getLatLon());
440            return b;
441        }
442
443        /**
444         * Gets the real bounds that enclose this rectangle.
445         * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates.
446         * @return The bounds.
447         * @since 10458
448         */
449        public Bounds getLatLonBoundsBox() {
450            return projection.getLatLonBoundsBox(getProjectionBounds());
451        }
452    }
453
454}