1
votes

I am trying to generate map tiles based on data we have stored in a DB and serve them up to Cesium using an UrlTemplateImageryProvider. For each map tile, I need to know the lat/long bounds in order to find matching data. I'm using the same tile scheme as Google Maps with a request like http://host/tiles/zoom/x/y. Initially I've been using the equations to convert these map tile coordinates into lat/long bounds found here: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_bounding_box but now I'm wondering if this is not the correct projection to be using.

For starters, all the Web Mercator documentation I see says that Tile 0,0 @ zoom lvl 0 should cover the entire world. That's not what I see with Cesium (there are 2 tiles at zoom level 0). That said, my calculations seem to work for zoom levels 0, 1, 2, and 3, but as I go to zoom level 4, the Latitude calculation starts to move south. If I plot my generated lat, long points on a 2D Google Map, they look correct, but I can clearly see that the tiles requested by cesium have different bounds. So I am now assuming I'm just using the wrong projection to calculate the tile bounds.

I've also tried the calculations here: http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/ converting from pixels, to meters, to lat/long but they seem to be the same results as the web mercator calculations.

So can anyone help me figure out how to calculate a Lat/Long bounding box for the tiles requested by Cesium?

Here are the methods I have based on the Slippy Map Tiles:

public static Box getLatLonBoundsForTile(int zoom, int x, int y) {
    double longitudeMin = tile2lon(x, zoom);
    double longitudeMax = tile2lon(x + 1, zoom);
    double latitudeMin = tile2lat(y + 1, zoom);
    double latitudeMax = tile2lat(y, zoom);

    Point swPoint = new Point(longitudeMin, latitudeMin);
    Point nePoint = new Point(longitudeMax, latitudeMax);

    return new Box(swPoint, nePoint);
}

public static double getColsForZoomLevel(int zoom) {
    if (zoom == 0) return 2;
    int noTiles = 8 * (int) (Math.pow(4, zoom - 1));
    return noTiles / getRowsForZoomLevel(zoom);
}
public static double getRowsForZoomLevel(int zoom) {
    if (zoom == 0) return 1;
    return Math.pow(2, zoom);
}
public static double tile2lon(int x, int zoom) {
    return x / getColsForZoomLevel(zoom) * 360.0 - 180;
}

public static double tile2lat(int y, int zoom) {
    double n = Math.PI - (2.0 * Math.PI * y) / getRowsForZoomLevel(zoom);
    return Math.toDegrees(Math.atan(Math.sinh(n)));
}
2

2 Answers

1
votes

Cesium includes a class WebMercatorTilingScheme (source) that calculates lat/lon values (in Radians) for tiles, with a configurable number of tiles at the root level.

For 3D rendering, typically the tiles are square, with a power-of-2 edge length, for example 256 x 256 pixel images, at all levels. The globe itself is usually wrapped with a rectangular texture (360 degrees wide, by 180 degrees tall). To cover this, Level 0 of the tiling scheme is usually two tiles wide but only one tile tall (2 x 1). The next level is 4 x 2 tiles, then 8 x 4, 16 x 8, etc.

To see this in action in realtime, load the Cesium Inspector, look for some options on the right side of the page, click the little + next to Terrain, and then put a checkmark on Show tile coordinates. Coordinates are drawn with L for level, and X and Y for position in the tiling scheme.

0
votes

I just discovered that I was causing the problem in the first place. I was forcing Cesium to use a GeographicTilingScheme in the UrlTemplateImageryProvider. My previous config was like this:

var layer = new Cesium.UrlTemplateImageryProvider({
    url : '/map/tile/{z}/{x}/{y}?debug=true',
    credit : '© Me',
    tilingScheme : new Cesium.GeographicTilingScheme(),
    maximumLevel : 11
});

Which I copied from the example in the docs. So, now that I removed the tilingScheme from the config, it defaults to a WebMercatorTilingScheme. Here's the new config:

var layer = new Cesium.UrlTemplateImageryProvider({
    url : '/map/tile/{z}/{x}/{y}?debug=true',
    credit : '© Me',
    maximumLevel : 11
});

Once I did this, I had to remove my assumption of 2 tiles @ zoom level 0 and change it back to 1 and now everything works fine with the methods above! I just changed this method like so:

public static double getColsForZoomLevel(int zoom) {
    return getRowsForZoomLevel(zoom);
}

So it all came down to the age old lesson of check the simple things first.