Tile <-> lon/lat conversion problem

I’m writing a program to use OSM for imagery and while verifying my tile to/from lon/lat conversion code it seems like my y-axis values are sometimes off by 1. I’ve pulled the code from multiple languages off the slippy map tilename wiki page and converted them for my application as well as grabbing the python code from the svn repo and still the xyz → lonlat → xyz don’t seem to match.

Here’s the output from checking zoom levels 0-3. The left is the original x,y,z, the center is the converted lon,lat, and the right is the converted x,y,z from the converted lon,lat.


not equal: 0, 1, 2 -> -180.0, 66.51326044311186 -> [0, 0, 2]
not equal: 1, 1, 2 -> -90.0, 66.51326044311186 -> [1, 0, 2]
not equal: 2, 1, 2 -> 0.0, 66.51326044311186 -> [2, 0, 2]
not equal: 3, 1, 2 -> 90.0, 66.51326044311186 -> [3, 0, 2]
not equal: 0, 2, 3 -> -180.0, 66.51326044311186 -> [0, 1, 3]
not equal: 1, 2, 3 -> -135.0, 66.51326044311186 -> [1, 1, 3]
not equal: 2, 2, 3 -> -90.0, 66.51326044311186 -> [2, 1, 3]
not equal: 3, 2, 3 -> -45.0, 66.51326044311186 -> [3, 1, 3]
not equal: 4, 2, 3 -> 0.0, 66.51326044311186 -> [4, 1, 3]
not equal: 5, 2, 3 -> 45.0, 66.51326044311186 -> [5, 1, 3]
not equal: 6, 2, 3 -> 90.0, 66.51326044311186 -> [6, 1, 3]
not equal: 7, 2, 3 -> 135.0, 66.51326044311186 -> [7, 1, 3]

And here’s my conversion code I used to generate the above (my MathUtils.sec(rads) returns 1/cos; the rest are built in functions from Java)


public static void main(final String[] args) {
   for(int z = 3; z < 4; z++) {
         for(int x = 0; x < Math.pow(2, z); x++) {
            for(int y = 0; y < Math.pow(2, z); y++) {
               final double[] lonLat = xy2lonlat(x, y, z);
               final int[] tile = tileXYZ(lonLat[0], lonLat[1], z);
               
               if(!Arrays.equals(tile, new int[] { x, y, z })) {
                  System.err.println("not equal: " + x + ", " + y + ", " + z + " -> " + lonLat[0] + ", " + lonLat[1] + " -> " + Arrays.toString(tile));
               }
            }
         }
      }
}


   
   private static int numTiles(final int zoom) {
      return (int) Math.pow(2, zoom);
   }
   
   private static double[] lonlat2relativeXY(final double lon, final double lat) {
      final double x = (lon + 180) / 360;
      final double y = (1 - Math.log(Math.tan(Math.toRadians(lat)) + MathUtils.sec(Math.toRadians(lat))) / Math.PI) / 2;
      
      return new double[] { x, y };
   }

   private static double[] lonlat2xy(final double lon, final double lat, final int z) {
      final double n = numTiles(z);
      final double[] xy = lonlat2relativeXY(lon, lat);
      xy[0] *= n;
      xy[1] *= n;
      
      return xy;
   }
      
   private static int[] tileXYZ(final double lon, final double lat, final int z) {
      final double[] xy = lonlat2xy(lon,lat,z);
      return new int[] { (int) xy[0] , (int) xy[1], z };
   }

   private static double[] xy2lonlat(final int x, final int y, final int z) {
      final double n = numTiles(z);
      final double relY = y / n;
      final double lat = mercatorToLat(Math.PI * (1 - 2 * relY));
      final double lon = -180.0 + 360.0 * x / n;
      return new double[] { lon, lat };
   }

   private static double mercatorToLat(final double mercatorY) {
      return Math.toDegrees(Math.atan(Math.sinh(mercatorY)));
   }

Any help or insight would be appreciated; thanks in advance!
Stephen

I think you are running into precision issues with floating point numbers. The xy2lonlat function seems to calculate the coordinates of the corner of the tile. A small change caused by rounding or truncation can cause this location to be located outside of the tile.

If the xy2lonlat function would return the lat and lon of the center of the tile, I believe the tileXYZ function will correctly map it to the original x, y, and z. This can be done by adding 0.5 to both x and y.

I figured it was something like that but wasn’t sure where the issue was; I assumed that the code on the wiki and in the osm python repo would be handling all that.

Thanks for the help!