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;
public static final int ZOOMLEVEL_MIN_PERSONALIZED = 12;
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(calcX(origin, clippedZoomlevel(zoomlevel)), calcY(origin, clippedZoomlevel(zoomlevel)), clippedZoomlevel(zoomlevel));
}
private Tile(int tileX, int tileY, int zoomlevel) {
this.zoomlevel = clippedZoomlevel(zoomlevel);
this.tileX = tileX;
this.tileY = tileY;
viewPort = new Viewport(getCoord(new UTFGridPosition(0, 0)), getCoord(new UTFGridPosition(63, 63)));
}
public int getZoomlevel() {
return zoomlevel;
}
private static int clippedZoomlevel(int zoomlevel) {
return Math.max(Math.min(zoomlevel, ZOOMLEVEL_MAX), ZOOMLEVEL_MIN);
}
/**
* Calculate the tile for a Geopoint based on the Spherical Mercator.
*
* @see Cloudmade
*/
private static int calcX(final Geopoint origin, final int zoomlevel) {
// The cut of the fractional part instead of rounding to the nearest integer is intentional and part of the algorithm
return (int) ((origin.getLongitude() + 180.0) / 360.0 * NUMBER_OF_TILES[zoomlevel]);
}
/**
* Calculate the tile for a Geopoint based on the Spherical Mercator.
*
*/
private static int calcY(final Geopoint origin, final int zoomlevel) {
// 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()));
// The cut of the fractional part instead of rounding to the nearest integer is intentional and part of the algorithm
return (int) ((0.5 - Math.log((1 + sinLatRad) / (1 - sinLatRad)) / (4 * Math.PI)) * NUMBER_OF_TILES[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 at least by the supplied number of
* adjacent tiles on the east/west axis.
* This criterion can be exactly met for even numbers of tiles
* while it may result in one more tile as requested for odd numbers
* of tiles.
*
* 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, final int numberOfTiles) {
int zoom = (int) Math.floor(
Math.log(360.0 * numberOfTiles / (2.0 * Math.abs(left.getLongitude() - right.getLongitude())))
/ Math.log(2)
);
Tile tileLeft = new Tile(left, zoom);
Tile tileRight = new Tile(right, zoom);
if (Math.abs(tileLeft.tileX - tileRight.tileX) < (numberOfTiles - 1)) {
zoom += 1;
}
return Math.min(zoom, ZOOMLEVEL_MAX);
}
/**
* Calculates the maximum possible zoom level where the supplied points
* are covered by at least by the supplied number of
* adjacent tiles on the north/south axis.
* This criterion can be exactly met for even numbers of tiles
* while it may result in one more tile as requested for odd numbers
* of tiles.
*
* 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, final int numberOfTiles) {
int zoom = (int) Math.ceil(
Math.log(2.0 * Math.PI * numberOfTiles / (
Math.abs(
asinh(tanGrad(bottom.getLatitude()))
- asinh(tanGrad(top.getLatitude()))
) * 2.0)
) / Math.log(2)
);
Tile tileBottom = new Tile(bottom, zoom);
Tile tileTop = new Tile(top, zoom);
if (Math.abs(tileBottom.tileY - tileTop.tileY) > (numberOfTiles - 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("Tile.requestMapTile() ", e);
}
return null;
}
public boolean containsPoint(final ICoordinates point) {
return viewPort.contains(point);
}
public Viewport getViewport() {
return viewPort;
}
/**
* Calculate needed tiles for the given viewport to cover it with
* max 2x2 tiles
*
* @param viewport
* @return
*/
protected static Set getTilesForViewport(final Viewport viewport) {
return getTilesForViewport(viewport, 2, Tile.ZOOMLEVEL_MIN);
}
/**
* Calculate needed tiles for the given viewport.
* You can define the minimum number of tiles on the longer axis
* and/or the minimum zoom level.
*
* @param viewport
* @param tilesOnAxis
* @param minZoom
* @return
*/
protected static Set getTilesForViewport(final Viewport viewport, final int tilesOnAxis, final int minZoom) {
Set tiles = new HashSet();
int zoom = Math.max(
Math.min(Tile.calcZoomLon(viewport.bottomLeft, viewport.topRight, tilesOnAxis),
Tile.calcZoomLat(viewport.bottomLeft, viewport.topRight, tilesOnAxis)),
minZoom);
Tile tileBottomLeft = new Tile(viewport.bottomLeft, zoom);
Tile tileTopRight = new Tile(viewport.topRight, zoom);
int xLow = Math.min(tileBottomLeft.getX(), tileTopRight.getX());
int xHigh = Math.max(tileBottomLeft.getX(), tileTopRight.getX());
int yLow = Math.min(tileBottomLeft.getY(), tileTopRight.getY());
int yHigh = Math.max(tileBottomLeft.getY(), tileTopRight.getY());
for (int xNum = xLow; xNum <= xHigh; xNum++) {
for (int yNum = yLow; yNum <= yHigh; yNum++) {
tiles.add(new Tile(xNum, yNum, 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);
}
}
}