diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileCoord.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileCoord.java index fea2dd8ca5..503261512e 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileCoord.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileCoord.java @@ -1,6 +1,5 @@ package com.onthegomap.planetiler.geo; -import com.onthegomap.planetiler.mbtiles.Mbtiles; import com.onthegomap.planetiler.util.Format; import javax.annotation.concurrent.Immutable; import org.locationtech.jts.geom.Coordinate; @@ -9,29 +8,24 @@ /** * The coordinate of a slippy map tile. *

- * In order to encode into a 32-bit integer, only zoom levels {@code <= 14} are supported since we need 4 bits for the - * zoom-level, and 14 bits each for the x/y coordinates. + * In order to encode into a 32-bit integer, we define a sequence of Hilbert curves for each zoom level, starting at the + * top-left. *

- * Tiles are ordered by z ascending, x ascending, y descending to match index ordering of {@link Mbtiles} sqlite - * database. * * @param encoded the tile ID encoded as a 32-bit integer * @param x x coordinate of the tile where 0 is the western-most tile just to the east the international date line * and 2^z-1 is the eastern-most tile * @param y y coordinate of the tile where 0 is the northern-most tile and 2^z-1 is the southern-most tile - * @param z zoom level ({@code <= 14}) + * @param z zoom level ({@code <= 15}) */ @Immutable public record TileCoord(int encoded, int x, int y, int z) implements Comparable { - // TODO: support higher than z14 - // z15 could theoretically fit into a 32-bit integer but needs a different packing strategy // z16+ would need more space - // also need to remove hardcoded z14 limits private static final int XY_MASK = (1 << 14) - 1; public TileCoord { - assert z <= 14; + assert z <= 15; } public static TileCoord ofXYZ(int x, int y, int z) { @@ -39,10 +33,44 @@ public static TileCoord ofXYZ(int x, int y, int z) { } public static TileCoord decode(int encoded) { - int z = (encoded >> 28) + 8; - int x = (encoded >> 14) & XY_MASK; - int y = ((1 << z) - 1) - ((encoded) & XY_MASK); - return new TileCoord(encoded, x, y, z); + int acc = 0; + int tmp_z = 0; + while (true) { + int num_tiles = (1 << tmp_z) * (1 << tmp_z); + if (acc + num_tiles > encoded) { + int position = encoded - acc; + return decodeOnLevel(tmp_z, position, encoded); + } + acc += num_tiles; + tmp_z++; + } + } + + private static void rotate(int n, int[] xy, int rx, int ry) { + if (ry == 0) { + if (rx == 1) { + xy[0] = n - 1 - xy[0]; + xy[1] = n - 1 - xy[1]; + } + int t = xy[0]; + xy[0] = xy[1]; + xy[1] = t; + } + } + + private static TileCoord decodeOnLevel(int z, int position, int encoded) { + int n = 1 << z; + int rx, ry, t = position; + int[] xy = {0, 0}; + for (int s = 1; s < n; s *= 2) { + rx = 1 & Integer.divideUnsigned(t, 2); + ry = 1 & (t ^ rx); + rotate(s, xy, rx, ry); + xy[0] += s * rx; + xy[1] += s * ry; + t = Integer.divideUnsigned(t, 4); + } + return new TileCoord(encoded, xy[0], xy[1], z); } /** Returns the tile containing a latitude/longitude coordinate at a given zoom level. */ @@ -54,30 +82,20 @@ public static TileCoord aroundLngLat(double lng, double lat, int zoom) { } private static int encode(int x, int y, int z) { - int max = 1 << z; - if (x >= max) { - x %= max; - } - if (x < 0) { - x += max; - } - if (y < 0) { - y = 0; - } - if (y >= max) { - y = max - 1; + int acc = 0; + for (int tmp_z = 0; tmp_z < z; tmp_z++) { + acc += (1 << tmp_z) * (1 << tmp_z); } - // since most significant bit is treated as the sign bit, make: - // z0-7 get encoded from 8 (0b1000) to 15 (0b1111) - // z8-14 get encoded from 0 (0b0000) to 6 (0b0110) - // so that encoded tile coordinates are ordered by zoom level - if (z < 8) { - z += 8; - } else { - z -= 8; + int n = 1 << z; + int rx, ry, d = 0; + int[] xy = {x, y}; + for (int s = Integer.divideUnsigned(n, 2); s > 0; s = Integer.divideUnsigned(s, 2)) { + rx = (xy[0] & s) > 0 ? 1 : 0; + ry = (xy[1] & s) > 0 ? 1 : 0; + d += s * s * ((3 * rx) ^ ry); + rotate(s, xy, rx, ry); } - y = max - 1 - y; - return (z << 28) | (x << 14) | y; + return acc + d; } @Override diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/TileCoordTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/TileCoordTest.java index ce7e4964f5..7170ddb314 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/TileCoordTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/TileCoordTest.java @@ -1,10 +1,7 @@ package com.onthegomap.planetiler.geo; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -17,6 +14,14 @@ class TileCoordTest { "0,1,1", "1,1,1", "100,100,14", + "0,0,14", + "16383,0,14", + "0,16383,14", + "16363,16363,14", + "0,0,15", + "32767,0,15", + "0,32767,15", + "32767,32767,15" }) void testTileCoord(int x, int y, int z) { TileCoord coord1 = TileCoord.ofXYZ(x, y, z); @@ -27,31 +32,17 @@ void testTileCoord(int x, int y, int z) { assertEquals(coord1, coord2); } - @Test - void testTileSortOrderRespectZ() { - int last = Integer.MIN_VALUE; - for (int z = 0; z <= 14; z++) { - int encoded = TileCoord.ofXYZ(0, 0, z).encoded(); - if (encoded < last) { - fail("encoded value for z" + (z - 1) + " (" + last + ") is not less than z" + z + " (" + encoded + ")"); - } - last = encoded; - } - } - - @Test - void testTileSortOrderFlipY() { - for (int z = 1; z <= 14; z++) { - int encoded1 = TileCoord.ofXYZ(0, 1, z).encoded(); - int encoded2 = TileCoord.ofXYZ(0, 0, z).encoded(); - if (encoded2 < encoded1) { - fail("encoded value for y=1 is not less than y=0 at z=" + z); - } - } - } - - @Test - void testThrowsPastZ14() { - assertThrows(AssertionError.class, () -> TileCoord.ofXYZ(0, 0, 15)); + @ParameterizedTest + @CsvSource({ + "0,0,0,0", + "0,0,1,1", + "0,1,1,2", + "1,1,1,3", + "1,0,1,4", + "0,0,2,5", + }) + void testTileOrderHilbert(int x, int y, int z, int i) { + int encoded = TileCoord.ofXYZ(x, y, z).encoded(); + assertEquals(i, encoded); } }