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}