001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer.imagery;
003
004import java.awt.GraphicsConfiguration;
005import java.awt.Polygon;
006import java.awt.Rectangle;
007import java.awt.Shape;
008import java.awt.geom.AffineTransform;
009import java.awt.geom.Point2D;
010import java.awt.geom.Rectangle2D;
011import java.util.Objects;
012import java.util.Optional;
013
014import org.openstreetmap.gui.jmapviewer.Tile;
015import org.openstreetmap.gui.jmapviewer.TileXY;
016import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
017import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
018import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
019import org.openstreetmap.josm.data.coor.EastNorth;
020import org.openstreetmap.josm.data.coor.LatLon;
021import org.openstreetmap.josm.data.imagery.CoordinateConversion;
022import org.openstreetmap.josm.data.projection.Projecting;
023import org.openstreetmap.josm.data.projection.ProjectionRegistry;
024import org.openstreetmap.josm.data.projection.ShiftedProjecting;
025import org.openstreetmap.josm.gui.MapView;
026import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
027import org.openstreetmap.josm.tools.JosmRuntimeException;
028import org.openstreetmap.josm.tools.bugreport.BugReport;
029
030/**
031 * This class handles tile coordinate management and computes their position in the map view.
032 * @author Michael Zangl
033 * @since 10651
034 */
035public class TileCoordinateConverter {
036    private final MapView mapView;
037    private final TileSourceDisplaySettings settings;
038    private final TileSource tileSource;
039    private final AffineTransform transform;
040
041    /**
042     * Create a new coordinate converter for the map view.
043     * @param mapView The map view.
044     * @param tileSource The tile source to use when converting coordinates.
045     * @param settings displacement settings.
046     * @throws NullPointerException if one argument is null
047     */
048    public TileCoordinateConverter(MapView mapView, TileSource tileSource, TileSourceDisplaySettings settings) {
049        this.mapView = Objects.requireNonNull(mapView, "mapView");
050        this.tileSource = Objects.requireNonNull(tileSource, "tileSource");
051        this.settings = Objects.requireNonNull(settings, "settings");
052        this.transform = Optional.ofNullable(mapView.getGraphicsConfiguration())
053                .map(GraphicsConfiguration::getDefaultTransform)
054                .orElseGet(AffineTransform::new);
055    }
056
057    private MapViewPoint pos(ICoordinate ll) {
058        return mapView.getState().getPointFor(CoordinateConversion.coorToLL(ll)).add(settings.getDisplacement());
059    }
060
061    private MapViewPoint pos(IProjected p) {
062        return mapView.getState().getPointFor(CoordinateConversion.projToEn(p)).add(settings.getDisplacement());
063    }
064
065    /**
066     * Apply reverse shift to EastNorth coordinate.
067     *
068     * @param en EastNorth coordinate representing a pixel on screen
069     * @return IProjected coordinate as it would e.g. be sent to a WMS server
070     */
071    public IProjected shiftDisplayToServer(EastNorth en) {
072        return CoordinateConversion.enToProj(en.subtract(settings.getDisplacement()));
073    }
074
075    /**
076     * Gets the projecting instance to use to convert between latlon and eastnorth coordinates.
077     * @return The {@link Projecting} instance.
078     */
079    public Projecting getProjecting() {
080        return new ShiftedProjecting(mapView.getProjection(), settings.getDisplacement());
081    }
082
083    /**
084     * Gets the top left position of the tile inside the map view.
085     * @param x x tile index
086     * @param y y tile index
087     * @param zoom zoom level
088     * @return the position
089     */
090    public Point2D getPixelForTile(int x, int y, int zoom) {
091        try {
092            ICoordinate coord = tileSource.tileXYToLatLon(x, y, zoom);
093            if (Double.isNaN(coord.getLat()) || Double.isNaN(coord.getLon())) {
094                throw new JosmRuntimeException("tileXYToLatLon returned " + coord);
095            }
096            return pos(coord).getInView();
097        } catch (RuntimeException e) {
098            throw BugReport.intercept(e).put("tileSource", tileSource).put("x", x).put("y", y).put("zoom", zoom);
099        }
100    }
101
102    /**
103     * Gets the top left position of the tile inside the map view.
104     * @param tile The tile
105     * @return The position.
106     */
107    public Point2D getPixelForTile(Tile tile) {
108        return getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
109    }
110
111    /**
112     * Convert screen pixel coordinate to tile position at certain zoom level.
113     * @param sx x coordinate (screen pixel)
114     * @param sy y coordinate (screen pixel)
115     * @param zoom zoom level
116     * @return the tile
117     */
118    public TileXY getTileforPixel(int sx, int sy, int zoom) {
119        if (requiresReprojection()) {
120            LatLon ll = getProjecting().eastNorth2latlonClamped(mapView.getEastNorth(sx, sy));
121            return tileSource.latLonToTileXY(CoordinateConversion.llToCoor(ll), zoom);
122        } else {
123            IProjected p = shiftDisplayToServer(mapView.getEastNorth(sx, sy));
124            return tileSource.projectedToTileXY(p, zoom);
125        }
126    }
127
128    /**
129     * Gets the position of the tile inside the map view.
130     * @param tile The tile
131     * @return The position as a rectangle in screen coordinates
132     */
133    public Rectangle2D getRectangleForTile(Tile tile) {
134        ICoordinate c1 = tile.getTileSource().tileXYToLatLon(tile);
135        ICoordinate c2 = tile.getTileSource().tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
136
137        return pos(c1).rectTo(pos(c2)).getInView();
138    }
139
140    /**
141     * Returns a shape that approximates the outline of the tile in screen coordinates.
142     *
143     * If the tile is rectangular, this will be the exact border of the tile.
144     * The tile may be more oddly shaped due to reprojection, then it is an approximation
145     * of the tile outline.
146     * @param tile the tile
147     * @return tile outline in screen coordinates
148     */
149    public Shape getTileShapeScreen(Tile tile) {
150        if (requiresReprojection()) {
151            Point2D p00 = this.getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
152            Point2D p10 = this.getPixelForTile(tile.getXtile() + 1, tile.getYtile(), tile.getZoom());
153            Point2D p11 = this.getPixelForTile(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
154            Point2D p01 = this.getPixelForTile(tile.getXtile(), tile.getYtile() + 1, tile.getZoom());
155            return new Polygon(new int[] {
156                    (int) Math.round(p00.getX()),
157                    (int) Math.round(p01.getX()),
158                    (int) Math.round(p11.getX()),
159                    (int) Math.round(p10.getX())},
160                new int[] {
161                    (int) Math.round(p00.getY()),
162                    (int) Math.round(p01.getY()),
163                    (int) Math.round(p11.getY()),
164                    (int) Math.round(p10.getY())}, 4);
165        } else {
166            Point2D p00 = this.getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
167            Point2D p11 = this.getPixelForTile(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
168            return new Rectangle((int) Math.round(p00.getX()), (int) Math.round(p00.getY()),
169                    (int) Math.round(p11.getX()) - (int) Math.round(p00.getX()),
170                    (int) Math.round(p11.getY()) - (int) Math.round(p00.getY()));
171        }
172    }
173
174    /**
175     * Returns average number of screen pixels per tile pixel for current mapview
176     * @param zoom zoom level
177     * @return average number of screen pixels per tile pixel
178     */
179    public double getScaleFactor(int zoom) {
180        TileXY t1, t2;
181        if (requiresReprojection()) {
182            LatLon topLeft = mapView.getLatLon(0, 0);
183            LatLon botRight = mapView.getLatLon(mapView.getWidth(), mapView.getHeight());
184            t1 = tileSource.latLonToTileXY(CoordinateConversion.llToCoor(topLeft), zoom);
185            t2 = tileSource.latLonToTileXY(CoordinateConversion.llToCoor(botRight), zoom);
186        } else {
187            EastNorth topLeftEN = mapView.getEastNorth(0, 0);
188            EastNorth botRightEN = mapView.getEastNorth(mapView.getWidth(), mapView.getHeight());
189            t1 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(topLeftEN), zoom);
190            t2 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(botRightEN), zoom);
191        }
192        int screenPixels = (int) (mapView.getWidth() * mapView.getHeight() * transform.getScaleX() * transform.getScaleY());
193        double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize());
194        if (screenPixels == 0 || tilePixels == 0) return 1;
195        return screenPixels/tilePixels;
196    }
197
198    /**
199     * Get {@link TileAnchor} for a tile in screen pixel coordinates.
200     * @param tile the tile
201     * @return position of the tile in screen coordinates
202     */
203    public TileAnchor getScreenAnchorForTile(Tile tile) {
204        if (requiresReprojection()) {
205            ICoordinate c1 = tile.getTileSource().tileXYToLatLon(tile);
206            ICoordinate c2 = tile.getTileSource().tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
207            return new TileAnchor(pos(c1).getInView(), pos(c2).getInView());
208        } else {
209            IProjected p1 = tileSource.tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom());
210            IProjected p2 = tileSource.tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
211            return new TileAnchor(pos(p1).getInView(), pos(p2).getInView());
212        }
213    }
214
215    /**
216     * Return true if tiles need to be reprojected from server projection to display projection.
217     * @return true if tiles need to be reprojected from server projection to display projection
218     */
219    public boolean requiresReprojection() {
220        return !Objects.equals(tileSource.getServerCRS(), ProjectionRegistry.getProjection().toCode());
221    }
222}