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