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}