// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui; import java.awt.Container; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Point2D.Double; import java.awt.geom.Rectangle2D; import javax.swing.JComponent; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.ProjectionBounds; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.projection.Projecting; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.gui.download.DownloadDialog; import org.openstreetmap.josm.tools.bugreport.BugReport; /** * This class represents a state of the {@link MapView}. * @author Michael Zangl * @since 10343 */ public final class MapViewState { private final Projecting projecting; private final int viewWidth; private final int viewHeight; private final double scale; /** * Top left {@link EastNorth} coordinate of the view. */ private final EastNorth topLeft; private final Point topLeftOnScreen; private final Point topLeftInWindow; /** * Create a new {@link MapViewState} * @param projection The projection to use. * @param viewWidth The view width * @param viewHeight The view height * @param scale The scale to use * @param topLeft The top left corner in east/north space. */ private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) { this.projecting = projection; this.scale = scale; this.topLeft = topLeft; this.viewWidth = viewWidth; this.viewHeight = viewHeight; topLeftInWindow = new Point(0, 0); topLeftOnScreen = new Point(0, 0); } private MapViewState(EastNorth topLeft, MapViewState mapViewState) { this.projecting = mapViewState.projecting; this.scale = mapViewState.scale; this.topLeft = topLeft; viewWidth = mapViewState.viewWidth; viewHeight = mapViewState.viewHeight; topLeftInWindow = mapViewState.topLeftInWindow; topLeftOnScreen = mapViewState.topLeftOnScreen; } private MapViewState(double scale, MapViewState mapViewState) { this.projecting = mapViewState.projecting; this.scale = scale; this.topLeft = mapViewState.topLeft; viewWidth = mapViewState.viewWidth; viewHeight = mapViewState.viewHeight; topLeftInWindow = mapViewState.topLeftInWindow; topLeftOnScreen = mapViewState.topLeftOnScreen; } private MapViewState(JComponent position, MapViewState mapViewState) { this.projecting = mapViewState.projecting; this.scale = mapViewState.scale; this.topLeft = mapViewState.topLeft; viewWidth = position.getWidth(); viewHeight = position.getHeight(); topLeftInWindow = new Point(); // better than using swing utils, since this allows us to use the mehtod if no screen is present. Container component = position; while (component != null) { topLeftInWindow.x += component.getX(); topLeftInWindow.y += component.getY(); component = component.getParent(); } try { topLeftOnScreen = position.getLocationOnScreen(); } catch (RuntimeException e) { throw BugReport.intercept(e).put("position", position).put("parent", position::getParent); } } private MapViewState(Projecting projecting, MapViewState mapViewState) { this.projecting = projecting; this.scale = mapViewState.scale; this.topLeft = mapViewState.topLeft; viewWidth = mapViewState.viewWidth; viewHeight = mapViewState.viewHeight; topLeftInWindow = mapViewState.topLeftInWindow; topLeftOnScreen = mapViewState.topLeftOnScreen; } /** * The scale in east/north units per pixel. * @return The scale. */ public double getScale() { return scale; } /** * Gets the MapViewPoint representation for a position in view coordinates. * @param x The x coordinate inside the view. * @param y The y coordinate inside the view. * @return The MapViewPoint. */ public MapViewPoint getForView(double x, double y) { return new MapViewViewPoint(x, y); } /** * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate. * @param eastNorth the position. * @return The point for that position. */ public MapViewPoint getPointFor(EastNorth eastNorth) { return new MapViewEastNorthPoint(eastNorth); } /** * Gets the {@link MapViewPoint} for the given {@link LatLon} coordinate. * @param latlon the position * @return The point for that position. * @since 10651 */ public MapViewPoint getPointFor(LatLon latlon) { return getPointFor(getProjection().latlon2eastNorth(latlon)); } /** * Gets a rectangle representing the whole view area. * @return The rectangle. */ public MapViewRectangle getViewArea() { return getForView(0, 0).rectTo(getForView(viewWidth, viewHeight)); } /** * Gets a rectangle of the view as map view area. * @param rectangle The rectangle to get. * @return The view area. * @since 10458 */ public MapViewRectangle getViewArea(Rectangle rectangle) { return getForView(rectangle.getMinX(), rectangle.getMinY()).rectTo(getForView(rectangle.getMaxX(), rectangle.getMaxY())); } /** * Gets the center of the view. * @return The center position. */ public MapViewPoint getCenter() { return getForView(viewWidth / 2.0, viewHeight / 2.0); } /** * Gets the width of the view on the Screen; * @return The width of the view component in screen pixel. */ public double getViewWidth() { return viewWidth; } /** * Gets the height of the view on the Screen; * @return The height of the view component in screen pixel. */ public double getViewHeight() { return viewHeight; } /** * Gets the current projection used for the MapView. * @return The projection. */ public Projection getProjection() { return projecting.getBaseProjection(); } /** * Creates an affine transform that is used to convert the east/north coordinates to view coordinates. * @return The affine transform. It should not be changed. * @since 10375 */ public AffineTransform getAffineTransform() { return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -topLeft.east() / scale, topLeft.north() / scale); } /** * Creates a new state that is the same as the current state except for that it is using a new center. * @param newCenter The new center coordinate. * @return The new state. * @since 10375 */ public MapViewState usingCenter(EastNorth newCenter) { return movedTo(getCenter(), newCenter); } /** * @param mapViewPoint The reference point. * @param newEastNorthThere The east/north coordinate that should be there. * @return The new state. * @since 10375 */ public MapViewState movedTo(MapViewPoint mapViewPoint, EastNorth newEastNorthThere) { EastNorth delta = newEastNorthThere.subtract(mapViewPoint.getEastNorth()); if (delta.distanceSq(0, 0) < .1e-20) { return this; } else { return new MapViewState(topLeft.add(delta), this); } } /** * Creates a new state that is the same as the current state except for that it is using a new scale. * @param newScale The new scale to use. * @return The new state. * @since 10375 */ public MapViewState usingScale(double newScale) { return new MapViewState(newScale, this); } /** * Creates a new state that is the same as the current state except for that it is using the location of the given component. *

* The view is moved so that the center is the same as the old center. * @param positon The new location to use. * @return The new state. * @since 10375 */ public MapViewState usingLocation(JComponent positon) { EastNorth center = this.getCenter().getEastNorth(); return new MapViewState(positon, this).usingCenter(center); } /** * Creates a state that uses the projection. * @param projection The projection to use. * @return The new state. * @since 10486 */ public MapViewState usingProjection(Projection projection) { if (projection.equals(this.projecting)) { return this; } else { return new MapViewState(projection, this); } } /** * 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 * before the view was added to the hirarchy. * @param width The view width * @param height The view height * @return The state * @since 10375 */ public static MapViewState createDefaultState(int width, int height) { Projection projection = Main.getProjection(); double scale = projection.getDefaultZoomInPPD(); MapViewState state = new MapViewState(projection, width, height, scale, new EastNorth(0, 0)); EastNorth center = calculateDefaultCenter(); return state.movedTo(state.getCenter(), center); } private static EastNorth calculateDefaultCenter() { Bounds b = DownloadDialog.getSavedDownloadBounds(); if (b == null) { b = Main.getProjection().getWorldBoundsLatLon(); } return Main.getProjection().latlon2eastNorth(b.getCenter()); } /** * A class representing a point in the map view. It allows to convert between the different coordinate systems. * @author Michael Zangl */ public abstract class MapViewPoint { /** * Get this point in view coordinates. * @return The point in view coordinates. */ public Point2D getInView() { return new Point2D.Double(getInViewX(), getInViewY()); } protected abstract double getInViewX(); protected abstract double getInViewY(); /** * Convert this point to window coordinates. * @return The point in window coordinates. */ public Point2D getInWindow() { return getUsingCorner(topLeftInWindow); } /** * Convert this point to screen coordinates. * @return The point in screen coordinates. */ public Point2D getOnScreen() { return getUsingCorner(topLeftOnScreen); } private Double getUsingCorner(Point corner) { return new Point2D.Double(corner.getX() + getInViewX(), corner.getY() + getInViewY()); } /** * Gets the {@link EastNorth} coordinate of this point. * @return The east/north coordinate. */ public EastNorth getEastNorth() { return new EastNorth(topLeft.east() + getInViewX() * scale, topLeft.north() - getInViewY() * scale); } /** * Create a rectangle from this to the other point. * @param other The other point. Needs to be of the same {@link MapViewState} * @return A rectangle. */ public MapViewRectangle rectTo(MapViewPoint other) { return new MapViewRectangle(this, other); } /** * Gets the current position in LatLon coordinates according to the current projection. * @return The positon as LatLon. * @see #getLatLonClamped() */ public LatLon getLatLon() { return projecting.getBaseProjection().eastNorth2latlon(getEastNorth()); } /** * Gets the latlon coordinate clamped to the current world area. * @return The lat/lon coordinate * @since 10805 */ public LatLon getLatLonClamped() { return projecting.eastNorth2latlonClamped(getEastNorth()); } /** * Add the given offset to this point * @param en The offset in east/north space. * @return The new point * @since 10651 */ public MapViewPoint add(EastNorth en) { return new MapViewEastNorthPoint(getEastNorth().add(en)); } } private class MapViewViewPoint extends MapViewPoint { private final double x; private final double y; MapViewViewPoint(double x, double y) { this.x = x; this.y = y; } @Override protected double getInViewX() { return x; } @Override protected double getInViewY() { return y; } @Override public String toString() { return "MapViewViewPoint [x=" + x + ", y=" + y + ']'; } } private class MapViewEastNorthPoint extends MapViewPoint { private final EastNorth eastNorth; MapViewEastNorthPoint(EastNorth eastNorth) { this.eastNorth = eastNorth; } @Override protected double getInViewX() { return (eastNorth.east() - topLeft.east()) / scale; } @Override protected double getInViewY() { return (topLeft.north() - eastNorth.north()) / scale; } @Override public EastNorth getEastNorth() { return eastNorth; } @Override public String toString() { return "MapViewEastNorthPoint [eastNorth=" + eastNorth + ']'; } } /** * A rectangle on the MapView. It is rectangular in screen / EastNorth space. * @author Michael Zangl */ public class MapViewRectangle { private final MapViewPoint p1; private final MapViewPoint p2; /** * Create a new MapViewRectangle * @param p1 The first point to use * @param p2 The second point to use. */ MapViewRectangle(MapViewPoint p1, MapViewPoint p2) { this.p1 = p1; this.p2 = p2; } /** * Gets the projection bounds for this rectangle. * @return The projection bounds. */ public ProjectionBounds getProjectionBounds() { ProjectionBounds b = new ProjectionBounds(p1.getEastNorth()); b.extend(p2.getEastNorth()); return b; } /** * Gets a rough estimate of the bounds by assuming lat/lon are parallel to x/y. * @return The bounds computed by converting the corners of this rectangle. * @see #getLatLonBoundsBox() */ public Bounds getCornerBounds() { Bounds b = new Bounds(p1.getLatLon()); b.extend(p2.getLatLon()); return b; } /** * Gets the real bounds that enclose this rectangle. * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates. * @return The bounds. * @since 10458 */ public Bounds getLatLonBoundsBox() { // TODO @michael2402: Use hillclimb. return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds()); } /** * Gets this rectangle on the screen. * @return The rectangle. * @since 10651 */ public Rectangle2D getInView() { double x1 = p1.getInViewX(); double y1 = p1.getInViewY(); double x2 = p2.getInViewX(); double y2 = p2.getInViewY(); return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2)); } } }