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);
}
}