/* * Copyright (c) 2014 Oculus Info Inc. * http://www.oculusinfo.com/ * * Released under the MIT License. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.oculusinfo.binning.impl; import com.oculusinfo.binning.BinIndex; import com.oculusinfo.binning.TileIndex; import com.oculusinfo.binning.TileIterator; import com.oculusinfo.binning.TilePyramid; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * Mercator tile projections using simple mercator (uncorrected for ellipsoidal * distortion, which really shouldn't matter anyhow, except very near the * equator) * * Sources: * * https://en.wikipedia.org/wiki/Mercator_projection * * http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames * * * Root coordinate system is longitude/latitude, both in degrees. * * @author nkronenfeld */ public class WebMercatorTilePyramid implements TilePyramid, Serializable { // We have no intrinsic data yet, so a default serial version should be fine. private static final long serialVersionUID = 1L; // From http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/ // What is the coordinate extent of Earth in EPSG:900913? // // [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244] // Constant 20037508.342789244 comes from the circumference of the Earth in meters, // which is 40 thousand kilometers, the coordinate origin is in the middle of extent. // In fact you can calculate the constant as: 2 * math.pi * 6378137 / 2.0 // // Polar areas with abs(latitude) bigger then 85.05112878 are clipped off. private static final double EPSG_900913_SCALE_FACTOR = 20037508.342789244; private static final double EPSG_900913_LATITUDE = 85.05112878; private static final double PI = Math.PI; // ellipsoid equatorial getRadius, in meters public static final double WGS84_EQUATORIAL_RADIUS = 6378137.0; // ellipsoid polar getRadius, in meters public static final double WGS84_POLAR_RADIUS = 6356752.3; // eccentricity squared, semi-major axis public static final double WGS84_ES = 0.00669437999013; private static double _minX = 180.0; private static double _maxX = -180.0; private static double _minY = -EPSG_900913_LATITUDE; private static double _maxY = EPSG_900913_LATITUDE; protected double gudermannian(double y) { // converts a y value from -PI(bottom) to PI(top) into the // mercator projection latitude return Math.toDegrees(Math.atan( Math.sinh(y) )); } protected double gudermannianInv (double latitude ) { // converts a latitude value from -EPSG_900913_LATITUDE to EPSG_900913_LATITUDE into // a y value from -PI(bottom) to PI(top) double sign = ( latitude != 0 ) ? latitude / Math.abs(latitude) : 0, sin = Math.sin(Math.toRadians(latitude) * sign); return sign * (Math.log((1.0 + sin) / (1.0 - sin)) / 2.0); } protected double linearToGudermannian (double value) { // convert linear coordinates into their equivalent gudermannian counterparts return gudermannian( (value / EPSG_900913_LATITUDE) * Math.PI ); } protected double gudermannianToLinear (double value) { // convert gudermannian coordinates into their equivalent linear counterparts return (gudermannianInv( value ) / Math.PI) * EPSG_900913_LATITUDE; } private Point2D rootToTileMercator (Point2D point, int level) { return rootToTileMercator(point.getX(), point.getY(), level); } private Point2D rootToTileMercator (double lon, double lat, int level) { double latR = Math.toRadians(lat); int pow2 = 1 << level; double x = (lon+180.0)/360.0 * pow2; double y = (1 - Math.log(Math.tan(latR) + 1 / Math.cos(latR)) / Math.PI) / 2 * pow2; return new Point2D.Double(x, pow2-y); } @Override public Rectangle2D getBounds() { return new Rectangle2D.Double( _minX, _minY, _maxX - _minX, _maxY - _minY ); } @Override public String getProjection () { return "EPSG:900913"; } @Override public String getTileScheme () { return "TMS"; } public Rectangle2D getEPSG_900913Bounds (TileIndex tile, BinIndex bin) { int pow2 = 1 << tile.getLevel(); double tileIncrement = 1.0/pow2; double minX = tile.getX() * tileIncrement - 0.5; double minY = tile.getY() * tileIncrement - 0.5; double maxX, maxY; if (null != bin) { double binXInc = tileIncrement/tile.getXBins(); double binYInc = tileIncrement/tile.getYBins(); minX = minX + bin.getX()*binXInc; minY = minY + (tile.getYBins() - bin.getY()-1)*binYInc; maxX = minX + binXInc; maxY = minY + binYInc; } else { maxX = minX + tileIncrement; maxY = minY + tileIncrement; } // Our range is actually (-0.5, 0.5), not (-1.0, 1.0), so we need 2 x ScaleFactor return new Rectangle2D.Double (minX*2.0*EPSG_900913_SCALE_FACTOR, minY*2.0*EPSG_900913_SCALE_FACTOR, (maxX-minX)*2.0*EPSG_900913_SCALE_FACTOR, (maxY-minY)*2.0*EPSG_900913_SCALE_FACTOR); } @Override public TileIndex rootToTile (Point2D point, int level) { return rootToTile(point, level, 256, 256); } @Override public TileIndex rootToTile (Point2D point, int level, int xBins, int yBins) { Point2D tileMercator = rootToTileMercator(point, level); return new TileIndex(level, (int) Math.floor(tileMercator.getX()), (int) Math.floor(tileMercator.getY()), xBins, yBins); } @Override public TileIndex rootToTile (double x, double y, int level) { return rootToTile(x, y, level, 256, 256); } @Override public TileIndex rootToTile (double x, double y, int level, int xBins, int yBins) { Point2D tileMercator = rootToTileMercator(x, y, level); return new TileIndex(level, (int) Math.floor(tileMercator.getX()), (int) Math.floor(tileMercator.getY()), yBins, xBins); } @Override public BinIndex rootToBin (Point2D point, TileIndex tile) { Point2D tileMercator = rootToTileMercator(point, tile.getLevel()); return new BinIndex((int) Math.floor((tileMercator.getX()-tile.getX())*tile.getXBins()), tile.getYBins()-1-(int) Math.floor((tileMercator.getY()-tile.getY())*tile.getYBins())); } @Override public BinIndex rootToBin (double x, double y, TileIndex tile) { Point2D tileMercator = rootToTileMercator(x, y, tile.getLevel()); return new BinIndex((int) Math.floor((tileMercator.getX()-tile.getX())*tile.getXBins()), tile.getYBins()-1-(int) Math.floor((tileMercator.getY()-tile.getY())*tile.getYBins())); } private double tileToLon (double x, int level) { int pow2 = 1 << level; return x/pow2 * 360.0 - 180.0; } private double tileToLat (double y, int level) { int pow2 = 1 << level; double n = -PI + (2.0*PI*y)/pow2; return Math.toDegrees(Math.atan(Math.sinh(n))); } @Override public Rectangle2D getTileBounds (TileIndex tile) { int level = tile.getLevel(); double north = tileToLat(tile.getY()+1, level); double south = tileToLat(tile.getY(), level); double east = tileToLon(tile.getX()+1, level); double west = tileToLon(tile.getX(), level); return new Rectangle2D.Double(west, south, east-west, north-south); } @Override public Rectangle2D getBinBounds (TileIndex tile, BinIndex bin) { int level = tile.getLevel(); double binXInc = 1.0/tile.getXBins(); double baseX = tile.getX()+bin.getX()*binXInc; double binYInc = 1.0/tile.getYBins(); double baseY = tile.getY()+(tile.getYBins()-1-bin.getY())*binYInc; double north = tileToLat(baseY + binYInc, level); double south = tileToLat(baseY, level); double east = tileToLon(baseX + binXInc, level); double west = tileToLon(baseX, level); return new Rectangle2D.Double(west, south, east-west, north-south); } @Override public double getBinOverlap (TileIndex tile, BinIndex bin, Rectangle2D area) { Point2D lowerLeftRoot = new Point2D.Double(area.getMinX(), area.getMinY()); Point2D lowerLeftMercator = rootToTileMercator(lowerLeftRoot, tile.getLevel()); double left = (lowerLeftMercator.getX()-tile.getX())*tile.getXBins()-bin.getX(); double bottom = (tile.getYBins()-1) - (lowerLeftMercator.getY()-tile.getY())*tile.getYBins() - bin.getY(); Point2D upperRightRoot = new Point2D.Double(area.getMaxX(), area.getMaxY()); Point2D upperRightMercator = rootToTileMercator(upperRightRoot, tile.getLevel()); double right = (upperRightMercator.getX()-tile.getX())*tile.getXBins()-bin.getX(); double top = (tile.getYBins()-1) - (upperRightMercator.getY() - tile.getY())*tile.getYBins() - bin.getY(); // Top and bottom actually reversed, but since we take absolute values, it doesn't really matter. left = Math.min(Math.max(left, 0.0), 1.0); right = Math.min(Math.max(right, 0.0), 1.0); top = Math.min(Math.max(top, -1.0), 0.0); bottom = Math.min(Math.max(bottom, -1.0), 0.0); return Math.abs((right-left) * (top-bottom)); } @Override public Collection<TileIndex> getBestTiles (Rectangle2D bounds) { // TODO Auto-generated method stub return null; } public Collection<TileIndex> getTiles (Rectangle2D bounds, int level){ TileIterator tileIt = new TileIterator(this, level, bounds); List<TileIndex> results = new ArrayList<TileIndex>(); while (tileIt.hasNext()) { results.add(tileIt.next()); } return results; } // All web mercator tile pyramids are equal @Override public int hashCode () { return 1500450271; } @Override public boolean equals (Object that) { if (null == that) return false; return (that instanceof WebMercatorTilePyramid); } }