// License: GPL. For details, see Readme.txt file.
package org.openstreetmap.gui.jmapviewer;
/**
* This class implements the Mercator Projection as it is used by OpenStreetMap
* (and google). It provides methods to translate coordinates from 'map space'
* into latitude and longitude (on the WGS84 ellipsoid) and vice versa. Map
* space is measured in pixels. The origin of the map space is the top left
* corner. The map space origin (0,0) has latitude ~85 and longitude -180.
* @author Jan Peter Stotz
* @author Jason Huntley
*/
public class OsmMercator {
/**
* default tile size
*/
public static final int DEFAUL_TILE_SIZE = 256;
/** maximum latitude (north) for mercator display */
public static final double MAX_LAT = 85.05112877980659;
/** minimum latitude (south) for mercator display */
public static final double MIN_LAT = -85.05112877980659;
/** equatorial earth radius for EPSG:3857 (Mercator) */
public static final double EARTH_RADIUS = 6_378_137;
/**
* instance with tile size of 256 for easy conversions
*/
public static final OsmMercator MERCATOR_256 = new OsmMercator();
/** tile size of the displayed tiles */
private int tileSize = DEFAUL_TILE_SIZE;
/**
* Creates instance with default tile size of 256
*/
public OsmMercator() {
}
/**
* Creates instance with provided tile size.
* @param tileSize tile size in pixels
*/
public OsmMercator(int tileSize) {
this.tileSize = tileSize;
}
public double radius(int aZoomlevel) {
return (tileSize * (1 << aZoomlevel)) / (2.0 * Math.PI);
}
/**
* Returns the absolut number of pixels in y or x, defined as: 2^Zoomlevel *
* tileSize where tileSize is the width of a tile in pixels
*
* @param aZoomlevel zoom level to request pixel data
* @return number of pixels
*/
public int getMaxPixels(int aZoomlevel) {
return tileSize * (1 << aZoomlevel);
}
public int falseEasting(int aZoomlevel) {
return getMaxPixels(aZoomlevel) / 2;
}
public int falseNorthing(int aZoomlevel) {
return -1 * getMaxPixels(aZoomlevel) / 2;
}
/**
* Transform pixelspace to coordinates and get the distance.
*
* @param x1 the first x coordinate
* @param y1 the first y coordinate
* @param x2 the second x coordinate
* @param y2 the second y coordinate
*
* @param zoomLevel the zoom level
* @return the distance
*/
public double getDistance(int x1, int y1, int x2, int y2, int zoomLevel) {
double la1 = yToLat(y1, zoomLevel);
double lo1 = xToLon(x1, zoomLevel);
double la2 = yToLat(y2, zoomLevel);
double lo2 = xToLon(x2, zoomLevel);
return getDistance(la1, lo1, la2, lo2);
}
/**
* Gets the distance using Spherical law of cosines.
*
* @param la1 the Latitude in degrees
* @param lo1 the Longitude in degrees
* @param la2 the Latitude from 2nd coordinate in degrees
* @param lo2 the Longitude from 2nd coordinate in degrees
* @return the distance
*/
public double getDistance(double la1, double lo1, double la2, double lo2) {
double aStartLat = Math.toRadians(la1);
double aStartLong = Math.toRadians(lo1);
double aEndLat = Math.toRadians(la2);
double aEndLong = Math.toRadians(lo2);
double distance = Math.acos(Math.sin(aStartLat) * Math.sin(aEndLat)
+ Math.cos(aStartLat) * Math.cos(aEndLat)
* Math.cos(aEndLong - aStartLong));
return EARTH_RADIUS * distance;
}
/**
* Transform longitude to pixelspace
*
* <p>
* Mathematical optimization<br>
* <code>
* x = radius(aZoomlevel) * toRadians(aLongitude) + falseEasting(aZoomLevel)<br>
* x = getMaxPixels(aZoomlevel) / (2 * PI) * (aLongitude * PI) / 180 + getMaxPixels(aZoomlevel) / 2<br>
* x = getMaxPixels(aZoomlevel) * aLongitude / 360 + 180 * getMaxPixels(aZoomlevel) / 360<br>
* x = getMaxPixels(aZoomlevel) * (aLongitude + 180) / 360<br>
* </code>
* </p>
*
* @param aLongitude
* [-180..180]
* @param aZoomlevel zoom level
* @return [0..2^Zoomlevel*TILE_SIZE[
*/
public double lonToX(double aLongitude, int aZoomlevel) {
int mp = getMaxPixels(aZoomlevel);
double x = (mp * (aLongitude + 180L)) / 360L;
return Math.min(x, mp);
}
/**
* Transforms latitude to pixelspace
* <p>
* Mathematical optimization<br>
* <code>
* log(u) := log((1.0 + sin(toRadians(aLat))) / (1.0 - sin(toRadians(aLat))<br>
*
* y = -1 * (radius(aZoomlevel) / 2 * log(u)))) - falseNorthing(aZoomlevel))<br>
* y = -1 * (getMaxPixel(aZoomlevel) / 2 * PI / 2 * log(u)) - -1 * getMaxPixel(aZoomLevel) / 2<br>
* y = getMaxPixel(aZoomlevel) / (-4 * PI) * log(u)) + getMaxPixel(aZoomLevel) / 2<br>
* y = getMaxPixel(aZoomlevel) * ((log(u) / (-4 * PI)) + 1/2)<br>
* </code>
* </p>
* @param aLat
* [-90...90]
* @param aZoomlevel zoom level
* @return [0..2^Zoomlevel*TILE_SIZE[
*/
public double latToY(double aLat, int aZoomlevel) {
if (aLat < MIN_LAT)
aLat = MIN_LAT;
else if (aLat > MAX_LAT)
aLat = MAX_LAT;
double sinLat = Math.sin(Math.toRadians(aLat));
double log = Math.log((1.0 + sinLat) / (1.0 - sinLat));
int mp = getMaxPixels(aZoomlevel);
double y = mp * (0.5 - (log / (4.0 * Math.PI)));
return Math.min(y, mp - 1);
}
/**
* Transforms pixel coordinate X to longitude
*
* <p>
* Mathematical optimization<br>
* <code>
* lon = toDegree((aX - falseEasting(aZoomlevel)) / radius(aZoomlevel))<br>
* lon = 180 / PI * ((aX - getMaxPixels(aZoomlevel) / 2) / getMaxPixels(aZoomlevel) / (2 * PI)<br>
* lon = 180 * ((aX - getMaxPixels(aZoomlevel) / 2) / getMaxPixels(aZoomlevel))<br>
* lon = 360 / getMaxPixels(aZoomlevel) * (aX - getMaxPixels(aZoomlevel) / 2)<br>
* lon = 360 * aX / getMaxPixels(aZoomlevel) - 180<br>
* </code>
* </p>
* @param aX
* [0..2^Zoomlevel*TILE_WIDTH[
* @param aZoomlevel zoom level
* @return ]-180..180[
*/
public double xToLon(int aX, int aZoomlevel) {
return ((360d * aX) / getMaxPixels(aZoomlevel)) - 180.0;
}
/**
* Transforms pixel coordinate Y to latitude
*
* @param aY
* [0..2^Zoomlevel*TILE_WIDTH[
* @param aZoomlevel zoom level
* @return [MIN_LAT..MAX_LAT] is about [-85..85]
*/
public double yToLat(int aY, int aZoomlevel) {
aY += falseNorthing(aZoomlevel);
double latitude = (Math.PI / 2) - (2 * Math.atan(Math.exp(-1.0 * aY / radius(aZoomlevel))));
return -1 * Math.toDegrees(latitude);
}
}