Skip to content

Commit

Permalink
proof-of-concept for TileCoord hilbert curve sequence supporting up t…
Browse files Browse the repository at this point in the history
  • Loading branch information
bdon committed Jul 11, 2022
1 parent 52432b8 commit 6726e6d
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,40 +8,69 @@
/**
* The coordinate of a <a href="https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames">slippy map tile</a>.
* <p>
* 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.
* <p>
* 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<TileCoord> {
// 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) {
return new TileCoord(encode(x, y, z), x, y, 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. */
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
Expand All @@ -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);
}
}

0 comments on commit 6726e6d

Please sign in to comment.