package cgeo.geocaching.connector.gc; import cgeo.geocaching.ICoordinates; import cgeo.geocaching.geopoint.Geopoint; import cgeo.geocaching.geopoint.Viewport; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.utils.LeastRecentlyUsedMap; import cgeo.geocaching.utils.Log; import ch.boye.httpclientandroidlib.HttpResponse; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Locale; import java.util.Set; /** * All about tiles. * * @see MSDN * @see OSM */ public class Tile { public static final double LATITUDE_MIN = -85.05112878; public static final double LATITUDE_MAX = 85.05112878; public static final double LONGITUDE_MIN = -180; public static final double LONGITUDE_MAX = 180; public static final int TILE_SIZE = 256; public static final int ZOOMLEVEL_MAX = 18; public static final int ZOOMLEVEL_MIN = 0; static final int[] NUMBER_OF_TILES = new int[ZOOMLEVEL_MAX - ZOOMLEVEL_MIN + 1]; static final int[] NUMBER_OF_PIXELS = new int[ZOOMLEVEL_MAX - ZOOMLEVEL_MIN + 1]; static { for (int z = ZOOMLEVEL_MIN; z <= ZOOMLEVEL_MAX; z++) { NUMBER_OF_TILES[z] = 1 << z; NUMBER_OF_PIXELS[z] = TILE_SIZE * 1 << z; } } private final int tileX; private final int tileY; private final int zoomlevel; private final Viewport viewPort; public Tile(Geopoint origin, int zoomlevel) { this.zoomlevel = Math.max(Math.min(zoomlevel, ZOOMLEVEL_MAX), ZOOMLEVEL_MIN); tileX = calcX(origin); tileY = calcY(origin); viewPort = new Viewport(getCoord(new UTFGridPosition(0, 0)), getCoord(new UTFGridPosition(63, 63))); } public int getZoomlevel() { return zoomlevel; } /** * Calculate the tile for a Geopoint based on the Spherical Mercator. * * @see Cloudmade */ private int calcX(final Geopoint origin) { return (int) ((origin.getLongitude() + 180.0) / 360.0 * NUMBER_OF_TILES[this.zoomlevel]); } /** * Calculate the tile for a Geopoint based on the Spherical Mercator. * */ private int calcY(final Geopoint origin) { // double latRad = Math.toRadians(origin.getLatitude()); // return (int) ((1 - (Math.log(Math.tan(latRad) + (1 / Math.cos(latRad))) / Math.PI)) / 2 * numberOfTiles); // Optimization from Bing double sinLatRad = Math.sin(Math.toRadians(origin.getLatitude())); return (int) ((0.5 - Math.log((1 + sinLatRad) / (1 - sinLatRad)) / (4 * Math.PI)) * NUMBER_OF_TILES[this.zoomlevel]); } public int getX() { return tileX; } public int getY() { return tileY; } /** * Calculate latitude/longitude for a given x/y position in this tile. * * @see Cloudmade */ public Geopoint getCoord(UTFGridPosition pos) { double pixX = tileX * TILE_SIZE + pos.x * 4; double pixY = tileY * TILE_SIZE + pos.y * 4; double lonDeg = ((360.0 * pixX) / NUMBER_OF_PIXELS[this.zoomlevel]) - 180.0; double latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * pixY / NUMBER_OF_PIXELS[this.zoomlevel]))); return new Geopoint(Math.toDegrees(latRad), lonDeg); } @Override public String toString() { return String.format(Locale.US, "(%d/%d), zoom=%d", tileX, tileY, zoomlevel); } /** * Calculates the maximum possible zoom level where the supplied points * are covered by adjacent tiles on the east/west axis. * The order of the points (left/right) is irrelevant. * * @param left * First point * @param right * Second point * @return */ public static int calcZoomLon(final Geopoint left, final Geopoint right) { int zoom = (int) Math.floor( Math.log(360.0 / Math.abs(left.getLongitude() - right.getLongitude())) / Math.log(2) ); Tile tileLeft = new Tile(left, zoom); Tile tileRight = new Tile(right, zoom); if (tileLeft.tileX == tileRight.tileX) { zoom += 1; } return Math.min(zoom, ZOOMLEVEL_MAX); } /** * Calculates the maximum possible zoom level where the supplied points * are covered by adjacent tiles on the north/south axis. * The order of the points (bottom/top) is irrelevant. * * @param bottom * First point * @param top * Second point * @return */ public static int calcZoomLat(final Geopoint bottom, final Geopoint top) { int zoom = (int) Math.ceil( Math.log(2.0 * Math.PI / Math.abs( asinh(tanGrad(bottom.getLatitude())) - asinh(tanGrad(top.getLatitude())) ) ) / Math.log(2) ); Tile tileBottom = new Tile(bottom, zoom); Tile tileTop = new Tile(top, zoom); if (Math.abs(tileBottom.tileY - tileTop.tileY) > 1) { zoom -= 1; } return Math.min(zoom, ZOOMLEVEL_MAX); } private static double tanGrad(double angleGrad) { return Math.tan(angleGrad / 180.0 * Math.PI); } /** * Calculates the inverted hyperbolic sine * (after Bronstein, Semendjajew: Taschenbuch der Mathematik * * @param x * @return */ private static double asinh(double x) { return Math.log(x + Math.sqrt(x * x + 1.0)); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Tile)) { return false; } return (this.tileX == ((Tile) o).tileX) && (this.tileY == ((Tile) o).tileY) && (this.zoomlevel == ((Tile) o).zoomlevel); } @Override public int hashCode() { return toString().hashCode(); } /** Request JSON informations for a tile */ public static String requestMapInfo(final String url, final Parameters params, final String referer) { return Network.getResponseData(Network.getRequest(url, params, new Parameters("Referer", referer))); } /** Request .png image for a tile. */ public static Bitmap requestMapTile(final Parameters params) { final HttpResponse response = Network.getRequest(GCConstants.URL_MAP_TILE, params, new Parameters("Referer", GCConstants.URL_LIVE_MAP)); try { return response != null ? BitmapFactory.decodeStream(response.getEntity().getContent()) : null; } catch (IOException e) { Log.e("cgBase.requestMapTile() " + e.getMessage()); } return null; } public boolean containsPoint(final ICoordinates point) { return viewPort.contains(point); } /** * Calculate needed tiles for the given viewport * * @param viewport * @return */ protected static Set getTilesForViewport(final Viewport viewport) { Set tiles = new HashSet(); int zoom = Math.min(Tile.calcZoomLon(viewport.bottomLeft, viewport.topRight), Tile.calcZoomLat(viewport.bottomLeft, viewport.topRight)); tiles.add(new Tile(viewport.bottomLeft, zoom)); tiles.add(new Tile(new Geopoint(viewport.getLatitudeMin(), viewport.getLongitudeMax()), zoom)); tiles.add(new Tile(new Geopoint(viewport.getLatitudeMax(), viewport.getLongitudeMin()), zoom)); tiles.add(new Tile(viewport.topRight, zoom)); return tiles; } public static class Cache { private final static LeastRecentlyUsedMap tileCache = new LeastRecentlyUsedMap.LruCache(64); public static void removeFromTileCache(final ICoordinates point) { if (point != null) { Collection tiles = new ArrayList(tileCache.values()); for (Tile tile : tiles) { if (tile.containsPoint(point)) { tileCache.remove(tile.hashCode()); } } } } public static boolean contains(final Tile tile) { return tileCache.containsKey(tile.hashCode()); } public static void add(final Tile tile) { tileCache.put(tile.hashCode(), tile); } } }