/***************************************************
*
* cismet GmbH, Saarbruecken, Germany
*
* ... and it just works.
*
****************************************************/
/*
* Originally written by Andrew Rowbottom.
* Released freely into the public domain, use it how you want, don't blame me.
* No warranty for this code is taken in any way.
*/
package de.cismet.cismap.commons.raster.wms.googlemaps;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
/**
* A utility class to assist in encoding and decoding google tile references.
*
* <p>For reasons of my own longitude is treated as being between -180 and +180 and internally latitude is treated as
* being from -1 to +1 and then converted to a mercator projection before return.</p>
*
* <p>All rectangles are sorted so the width and height are +ve</p>
*
* @author thorsten.hell@cismet.de
* @version $Revision$, $Date$
*/
public class GoogleTileUtils {
//~ Constructors -----------------------------------------------------------
/**
* hidden constructor, this is a Utils class.
*/
private GoogleTileUtils() {
super();
}
//~ Methods ----------------------------------------------------------------
/**
* Returns a buffered image with the corner lat/lon,keyhole id and zoom level written on it.
*
* @param keyholeString the keyhole string to return the image for.
*
* @return DOCUMENT ME!
*/
public static BufferedImage getDebugTileFor(final String keyholeString) {
final BufferedImage img = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
final Graphics g = img.getGraphics();
g.setColor(Color.gray);
g.fillRect(0, 0, 256, 256);
g.setColor(Color.black);
final int scale = 400 / keyholeString.length();
g.setFont(new Font("Serif", Font.BOLD, scale)); // NOI18N
g.drawString(keyholeString + " (z=" + getTileZoom(keyholeString) + ")", 10, 200); // NOI18N
final Rectangle2D r = getLatLong(keyholeString);
final DecimalFormat df = new DecimalFormat("#.####"); // NOI18N
g.setFont(new Font("SanSerif", 0, 15)); // NOI18N
g.drawString(df.format(r.getMinY()) + "," + df.format(r.getMinX()) + " (w:" + df.format(r.getWidth()) + " h:"
+ df.format(r.getHeight()) + ")",
10,
250); // NOI18N
g.drawString(df.format(r.getMaxY()) + "," + df.format(r.getMaxX()), 150, 20); // NOI18N
g.drawRect(1, 1, 255, 255);
g.dispose();
return img;
}
/**
* Returns a buffered image with the corner lat/lon,x,y and zoom level written on it.
*
* @param x DOCUMENT ME!
* @param y DOCUMENT ME!
* @param zoom DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public static BufferedImage getDebugTileFor(final int x, final int y, final int zoom) {
final BufferedImage img = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
final Graphics g = img.getGraphics();
g.setColor(Color.gray);
g.fillRect(0, 0, 256, 256);
g.setColor(Color.black);
final int scale = 20;
g.setFont(new Font("Serif", Font.BOLD, scale)); // NOI18N
g.drawString("x:" + x + " y:" + y + " z:" + zoom, 10, 200); // NOI18N
final Rectangle2D r = getLatLong(x, y, zoom);
final DecimalFormat df = new DecimalFormat("#.####"); // NOI18N
g.setFont(new Font("SanSerif", 0, 15)); // NOI18N
g.drawString(df.format(r.getMinY()) + "," + df.format(r.getMinX()) + " (w:" + df.format(r.getWidth()) + " h:"
+ df.format(r.getHeight()) + ")",
10,
250); // NOI18N
g.drawString(df.format(r.getMaxY()) + "," + df.format(r.getMaxX()), 150, 20); // NOI18N
g.drawRect(1, 1, 255, 255);
g.dispose();
return img;
}
/**
* returns a Rectangle2D with x = lon, y = lat, width=lonSpan, height=latSpan for a keyhole string.
*
* @param keyholeStr DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @throws RuntimeException DOCUMENT ME!
*/
public static Rectangle2D.Double getLatLong(final String keyholeStr) {
// must start with "t"
if ((keyholeStr == null) || (keyholeStr.length() == 0) || (keyholeStr.charAt(0) != 't')) {
throw new RuntimeException("Keyhole string must start with 't'"); // NOI18N
}
double lon = -180; // x
double lonWidth = 360; // width 360
// double lat = -90; // y
// double latHeight = 180; // height 180
double lat = -1;
double latHeight = 2;
for (int i = 1; i < keyholeStr.length(); i++) {
lonWidth /= 2;
latHeight /= 2;
final char c = keyholeStr.charAt(i);
switch (c) {
case 's': {
// lat += latHeight;
lon += lonWidth;
break;
}
case 'r': {
lat += latHeight;
lon += lonWidth;
break;
}
case 'q': {
lat += latHeight;
// lon += lonWidth;
break;
}
case 't': {
// lat += latHeight;
// lon += lonWidth;
break;
}
default: {
throw new RuntimeException("unknown char '" + c + "' when decoding keyhole string."); // NOI18N
}
}
}
// convert lat and latHeight to degrees in a transverse mercator projection
// note that in fact the coordinates go from about -85 to +85 not -90 to 90!
latHeight += lat;
latHeight = (2 * Math.atan(Math.exp(Math.PI * latHeight))) - (Math.PI / 2);
latHeight *= (180 / Math.PI);
lat = (2 * Math.atan(Math.exp(Math.PI * lat))) - (Math.PI / 2);
lat *= (180 / Math.PI);
latHeight -= lat;
if (lonWidth < 0) {
lon = lon + lonWidth;
lonWidth = -lonWidth;
}
if (latHeight < 0) {
lat = lat + latHeight;
latHeight = -latHeight;
}
// lat = Math.asin(lat) * 180 / Math.PI;
return new Rectangle2D.Double(lon, lat, lonWidth, latHeight);
}
/**
* returns a Rectangle2D with x = lon, y = lat, width=lonSpan, height=latSpan for an x,y,zoom as used by google.
*
* @param x DOCUMENT ME!
* @param y DOCUMENT ME!
* @param zoom DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public static Rectangle2D.Double getLatLong(final int x, final int y, final int zoom) {
double lon = -180; // x
double lonWidth = 360; // width 360
// double lat = -90; // y
// double latHeight = 180; // height 180
double lat = -1;
double latHeight = 2;
final int tilesAtThisZoom = 1 << (17 - zoom);
lonWidth = 360.0 / tilesAtThisZoom;
lon = -180 + (x * lonWidth);
latHeight = -2.0 / tilesAtThisZoom;
lat = 1 + (y * latHeight);
// convert lat and latHeight to degrees in a transverse mercator projection
// note that in fact the coordinates go from about -85 to +85 not -90 to 90!
latHeight += lat;
latHeight = (2 * Math.atan(Math.exp(Math.PI * latHeight))) - (Math.PI / 2);
latHeight *= (180 / Math.PI);
lat = (2 * Math.atan(Math.exp(Math.PI * lat))) - (Math.PI / 2);
lat *= (180 / Math.PI);
latHeight -= lat;
if (lonWidth < 0) {
lon = lon + lonWidth;
lonWidth = -lonWidth;
}
if (latHeight < 0) {
lat = lat + latHeight;
latHeight = -latHeight;
}
return new Rectangle2D.Double(lon, lat, lonWidth, latHeight);
}
/**
* returns a keyhole string for a longitude (x), latitude (y), and zoom.
*
* @param lon DOCUMENT ME!
* @param lat DOCUMENT ME!
* @param zoom DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public static String getTileRef(double lon, double lat, int zoom) {
zoom = 18 - zoom;
// first convert the lat lon to transverse mercator coordintes.
if (lon > 180) {
lon -= 360;
}
lon /= 180;
// convert latitude to a range -1..+1
lat = Math.log(Math.tan((Math.PI / 4) + ((0.5 * Math.PI * lat) / 180))) / Math.PI;
double tLat = -1;
double tLon = -1;
double lonWidth = 2;
double latHeight = 2;
final StringBuffer keyholeString = new StringBuffer("t");
for (int i = 0; i < zoom; i++) {
lonWidth /= 2;
latHeight /= 2;
if ((tLat + latHeight) > lat) {
if ((tLon + lonWidth) > lon) {
keyholeString.append('t');
} else {
tLon += lonWidth;
keyholeString.append('s');
}
} else {
tLat += latHeight;
if ((tLon + lonWidth) > lon) {
keyholeString.append('q');
} else {
tLon += lonWidth;
keyholeString.append('r');
}
}
}
return keyholeString.toString();
}
/**
* returns the Google zoom level for the keyhole string.
*
* @param keyHoleString DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public static int getTileZoom(final String keyHoleString) {
return 18 - keyHoleString.length();
}
/**
* Tests.
*
* @param args DOCUMENT ME!
*/
public static void main(final String[] args) {
System.out.println(getLatLong(0, 0, 15));
System.out.println(getLatLong(1, 1, 15));
System.out.println(getLatLong(2, 2, 15));
System.out.println(getLatLong(3, 3, 15));
}
}