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