diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95f661597b..5db1aa7f96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,8 @@ To set up your local development environment: - Install Java 16 or later. You can download Java manually from [Adoptium](https://adoptium.net/installation.html) or use: - [Windows installer](https://adoptium.net/installation.html#windows-msi) - - [macOS installer](https://adoptium.net/installation.html#macos-pkg) (or `brew install --cask temurin`, or `port install openjdk17-temurin`) + - [macOS installer](https://adoptium.net/installation.html#macos-pkg) (or `brew install --cask temurin`, + or `port install openjdk17-temurin`) - [Linux installer](https://github.com/adoptium/website-v2/blob/main/src/asciidoc-pages/installation/linux.adoc) (or `apt-get install openjdk-17-jdk`) - Build and run the tests ([mvnw](https://github.com/takari/maven-wrapper) automatically downloads maven the first time @@ -25,10 +26,10 @@ To set up your local development environment: - to run just one test e.g. `GeoUtilsTest`: `./mvnw -pl planetiler-core -Dtest=GeoUtilsTest test` - to run benchmarks e.g. `BenchmarkTileCoord`: -```sh -./scripts/build.sh -java -cp planetiler-dist/target/planetiler-dist-*-with-deps.jar com.onthegomap.planetiler.benchmarks.BenchmarkTileCoord -``` + ```sh + ./scripts/build.sh + java -cp planetiler-dist/target/planetiler-dist-*-with-deps.jar com.onthegomap.planetiler.benchmarks.BenchmarkTileCoord + ``` GitHub Workflows will run regression tests on any pull request. diff --git a/NOTICE.md b/NOTICE.md index ac4701c66e..f830e805b2 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -35,6 +35,8 @@ The `planetiler-core` module includes the following software: - `PbfFieldDecoder` from [osmosis](https://github.com/openstreetmap/osmosis) (Public Domain) - `Madvise` from [uppend](https://github.com/upserve/uppend/) (MIT License) - `ArrayLongMinHeap` implementations from [graphhopper](https://github.com/graphhopper/graphhopper) (Apache license) + - `Hilbert` implementation + from [github.com/rawrunprotected/hilbert_curves](https://github.com/rawrunprotected/hilbert_curves) (Public Domain) Additionally, the `planetiler-basemap` module is based on [OpenMapTiles](https://github.com/openmaptiles/openmaptiles): diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java index 4d42972547..cb8db4c5f8 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java @@ -52,7 +52,8 @@ public record PlanetilerConfig( ) { public static final int MIN_MINZOOM = 0; - public static final int MAX_MAXZOOM = 14; + public static final int MAX_MAXZOOM = 15; + private static final int DEFAULT_MAXZOOM = 14; public PlanetilerConfig { if (minzoom > maxzoom) { @@ -104,6 +105,12 @@ public static PlanetilerConfig from(Arguments arguments) { throw new UncheckedIOException(e); } } + + int minzoom = arguments.getInteger("minzoom", "minimum zoom level", MIN_MINZOOM); + int maxzoom = arguments.getInteger("maxzoom", "maximum zoom level up to " + MAX_MAXZOOM, DEFAULT_MAXZOOM); + int renderMaxzoom = + arguments.getInteger("render_maxzoom", "maximum rendering zoom level up to " + MAX_MAXZOOM, + Math.max(maxzoom, DEFAULT_MAXZOOM)); return new PlanetilerConfig( arguments, bounds, @@ -113,9 +120,9 @@ public static PlanetilerConfig from(Arguments arguments) { arguments.getInteger("feature_read_threads", "number of threads to use when reading features at tile write time", threads < 32 ? 1 : 2), arguments.getDuration("loginterval", "time between logs", "10s"), - arguments.getInteger("minzoom", "minimum zoom level", MIN_MINZOOM), - arguments.getInteger("maxzoom", "maximum zoom level (limit 14)", MAX_MAXZOOM), - arguments.getInteger("render_maxzoom", "maximum rendering zoom level (limit 14)", MAX_MAXZOOM), + minzoom, + maxzoom, + renderMaxzoom, arguments.getBoolean("skip_mbtiles_index_creation", "skip adding index to mbtiles file", false), arguments.getBoolean("optimize_db", "optimize mbtiles after writing", false), arguments.getBoolean("emit_tiles_in_order", "emit tiles in index order", true), 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 8c42924477..b304cd4d10 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,5 +1,7 @@ package com.onthegomap.planetiler.geo; +import static com.onthegomap.planetiler.config.PlanetilerConfig.MAX_MAXZOOM; + import com.onthegomap.planetiler.util.Format; import javax.annotation.concurrent.Immutable; import org.locationtech.jts.geom.Coordinate; @@ -21,8 +23,36 @@ */ @Immutable public record TileCoord(int encoded, int x, int y, int z) implements Comparable { + + private static final int[] ZOOM_START_INDEX = new int[MAX_MAXZOOM + 1]; + + static { + int idx = 0; + for (int z = 0; z <= MAX_MAXZOOM; z++) { + ZOOM_START_INDEX[z] = idx; + int count = (1 << z) * (1 << z); + if (Integer.MAX_VALUE - idx < count) { + throw new IllegalStateException("Too many zoom levels " + MAX_MAXZOOM); + } + idx += count; + } + } + + private static int startIndexForZoom(int z) { + return ZOOM_START_INDEX[z]; + } + + private static int zoomForIndex(int idx) { + for (int z = MAX_MAXZOOM; z >= 0; z--) { + if (ZOOM_START_INDEX[z] <= idx) { + return z; + } + } + throw new IllegalArgumentException("Bad index: " + idx); + } + public TileCoord { - assert z <= 15; + assert z <= MAX_MAXZOOM; } public static TileCoord ofXYZ(int x, int y, int z) { @@ -30,19 +60,9 @@ public static TileCoord ofXYZ(int x, int y, int z) { } public static TileCoord decode(int encoded) { - int acc = 0; - int tmpZ = 0; - while (true) { - int numTiles = (1 << tmpZ) * (1 << tmpZ); - if (acc + numTiles > encoded) { - int position = encoded - acc; - // long xy = hilbertPositionToXY(tmpZ, position); - long xy = tmsPositionToXY(tmpZ, position); - return new TileCoord(encoded, (int) (xy >>> 32 & 0xFFFFFFFFL), (int) (xy & 0xFFFFFFFFL), tmpZ); - } - acc += numTiles; - tmpZ++; - } + int z = zoomForIndex(encoded); + long xy = tmsPositionToXY(z, encoded - startIndexForZoom(z)); + return new TileCoord(encoded, (int) (xy >>> 32 & 0xFFFFFFFFL), (int) (xy & 0xFFFFFFFFL), z); } /** Returns the tile containing a latitude/longitude coordinate at a given zoom level. */ @@ -54,12 +74,7 @@ public static TileCoord aroundLngLat(double lng, double lat, int zoom) { } public static int encode(int x, int y, int z) { - int acc = 0; - for (int tmpZ = 0; tmpZ < z; tmpZ++) { - acc += (1 << tmpZ) * (1 << tmpZ); - } - // return acc + hilbertXYToPosition(z, x, y); - return acc + tmsXYToPosition(z, x, y); + return startIndexForZoom(z) + tmsXYToPosition(z, x, y); } @Override @@ -86,17 +101,11 @@ public String toString() { return "{x=" + x + " y=" + y + " z=" + z + '}'; } - public double progressOnLevel() { - int acc = 0; - int tmpZ = 0; - while (true) { - int numTiles = (1 << tmpZ) * (1 << tmpZ); - if (acc + numTiles > encoded) { - return (encoded - acc) / (double) numTiles; - } - acc += numTiles; - tmpZ++; - } + public double progressOnLevel(TileExtents extents) { + // approximate percent complete within a bounding box by computing what % of the way through the columns we are + // (for hilbert ordering, we probably won't be able to reflect the bounding box) + var zoomBounds = extents.getForZoom(z); + return 1d * (x - zoomBounds.minX()) / (zoomBounds.maxX() - zoomBounds.minX()); } @Override @@ -141,107 +150,4 @@ public static int tmsXYToPosition(int z, int x, int y) { int dim = 1 << z; return x * dim + (dim - 1 - y); } - - // hilbert implementation (not currently used) - // Fast Hilbert curve algorithm by http://threadlocalmutex.com/ - // Ported from C++ https://github.com/rawrunprotected/hilbert_curves (public domain) - private static int deinterleave(int tx) { - tx = tx & 0x55555555; - tx = (tx | (tx >>> 1)) & 0x33333333; - tx = (tx | (tx >>> 2)) & 0x0F0F0F0F; - tx = (tx | (tx >>> 4)) & 0x00FF00FF; - tx = (tx | (tx >>> 8)) & 0x0000FFFF; - return tx; - } - - private static int interleave(int tx) { - tx = (tx | (tx << 8)) & 0x00FF00FF; - tx = (tx | (tx << 4)) & 0x0F0F0F0F; - tx = (tx | (tx << 2)) & 0x33333333; - tx = (tx | (tx << 1)) & 0x55555555; - return tx; - } - - private static int prefixScan(int tx) { - tx = (tx >>> 8) ^ tx; - tx = (tx >>> 4) ^ tx; - tx = (tx >>> 2) ^ tx; - tx = (tx >>> 1) ^ tx; - return tx; - } - - private static long hilbertPositionToXY(int z, int pos) { - pos = pos << (32 - 2 * z); - - int i0 = deinterleave(pos); - int i1 = deinterleave(pos >>> 1); - - int t0 = (i0 | i1) ^ 0xFFFF; - int t1 = i0 & i1; - - int prefixT0 = prefixScan(t0); - int prefixT1 = prefixScan(t1); - - int a = (((i0 ^ 0xFFFF) & prefixT1) | (i0 & prefixT0)); - - int resultX = (a ^ i1) >>> (16 - z); - int resultY = (a ^ i0 ^ i1) >>> (16 - z); - return ((long) resultX << 32) | resultY; - } - - private static int hilbertXYToIndex(int z, int x, int y) { - x = x << (16 - z); - y = y << (16 - z); - - int hA, hB, hC, hD; - - int a1 = x ^ y; - int b1 = 0xFFFF ^ a1; - int c1 = 0xFFFF ^ (x | y); - int d1 = x & (y ^ 0xFFFF); - - hA = a1 | (b1 >>> 1); - hB = (a1 >>> 1) ^ a1; - - hC = ((c1 >>> 1) ^ (b1 & (d1 >>> 1))) ^ c1; - hD = ((a1 & (c1 >>> 1)) ^ (d1 >>> 1)) ^ d1; - - int a2 = hA; - int b2 = hB; - int c2 = hC; - int d2 = hD; - - hA = ((a2 & (a2 >>> 2)) ^ (b2 & (b2 >>> 2))); - hB = ((a2 & (b2 >>> 2)) ^ (b2 & ((a2 ^ b2) >>> 2))); - - hC ^= ((a2 & (c2 >>> 2)) ^ (b2 & (d2 >>> 2))); - hD ^= ((b2 & (c2 >>> 2)) ^ ((a2 ^ b2) & (d2 >>> 2))); - - int a3 = hA; - int b3 = hB; - int c3 = hC; - int d3 = hD; - - hA = ((a3 & (a3 >>> 4)) ^ (b3 & (b3 >>> 4))); - hB = ((a3 & (b3 >>> 4)) ^ (b3 & ((a3 ^ b3) >>> 4))); - - hC ^= ((a3 & (c3 >>> 4)) ^ (b3 & (d3 >>> 4))); - hD ^= ((b3 & (c3 >>> 4)) ^ ((a3 ^ b3) & (d3 >>> 4))); - - int a4 = hA; - int b4 = hB; - int c4 = hC; - int d4 = hD; - - hC ^= ((a4 & (c4 >>> 8)) ^ (b4 & (d4 >>> 8))); - hD ^= ((b4 & (c4 >>> 8)) ^ ((a4 ^ b4) & (d4 >>> 8))); - - int a = hC ^ (hC >>> 1); - int b = hD ^ (hD >>> 1); - - int i0 = x ^ y; - int i1 = b | (0xFFFF ^ (i0 | a)); - - return ((interleave(i1) << 1) | interleave(i0)) >>> (32 - 2 * z); - } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/MbtilesWriter.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/MbtilesWriter.java index 3b14db18ef..5e3d28e1d8 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/MbtilesWriter.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/mbtiles/MbtilesWriter.java @@ -203,9 +203,9 @@ private String getLastTileLogDetails() { if (lastTile == null) { blurb = "n/a"; } else { - blurb = "%d/%d/%d (z%d %s%%) %s".formatted( + blurb = "%d/%d/%d (z%d %s) %s".formatted( lastTile.z(), lastTile.x(), lastTile.y(), - lastTile.z(), 100 * lastTile.progressOnLevel(), + lastTile.z(), Format.defaultInstance().percent(lastTile.progressOnLevel(config.bounds().tileExtents())), lastTile.getDebugUrl() ); } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Hilbert.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Hilbert.java new file mode 100644 index 0000000000..34f76f95a2 --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Hilbert.java @@ -0,0 +1,128 @@ +package com.onthegomap.planetiler.util; + +/** + * Fast hilbert space-filling curve implementation ported from C++ + * github.com/rawrunprotected/hilbert_curves by + * threadlocalmutex.com (public domain). + */ +public class Hilbert { + private Hilbert() { + throw new IllegalStateException("Utility class"); + } + + private static int deinterleave(int tx) { + tx = tx & 0x55555555; + tx = (tx | (tx >>> 1)) & 0x33333333; + tx = (tx | (tx >>> 2)) & 0x0F0F0F0F; + tx = (tx | (tx >>> 4)) & 0x00FF00FF; + tx = (tx | (tx >>> 8)) & 0x0000FFFF; + return tx; + } + + private static int interleave(int tx) { + tx = (tx | (tx << 8)) & 0x00FF00FF; + tx = (tx | (tx << 4)) & 0x0F0F0F0F; + tx = (tx | (tx << 2)) & 0x33333333; + tx = (tx | (tx << 1)) & 0x55555555; + return tx; + } + + private static int prefixScan(int tx) { + tx = (tx >>> 8) ^ tx; + tx = (tx >>> 4) ^ tx; + tx = (tx >>> 2) ^ tx; + tx = (tx >>> 1) ^ tx; + return tx; + } + + /** Returns the x coordinate extracted from the result of {@link #hilbertPositionToXY(int, int)}. */ + public static int extractX(long xy) { + return (int) (xy >>> 32); + } + + /** Returns the y coordinate extracted from the result of {@link #hilbertPositionToXY(int, int)}. */ + public static int extractY(long xy) { + return (int) xy; + } + + /** + * Returns the x/y coordinates from hilbert index {@code pos} at {@code level} packed into a long. + * + * Use {@link #extractX(long)} and {@link #extractY(long)} to extract x and y from the result. + */ + public static long hilbertPositionToXY(int level, int pos) { + pos = pos << (32 - 2 * level); + + int i0 = deinterleave(pos); + int i1 = deinterleave(pos >>> 1); + + int t0 = (i0 | i1) ^ 0xFFFF; + int t1 = i0 & i1; + + int prefixT0 = prefixScan(t0); + int prefixT1 = prefixScan(t1); + + int a = (((i0 ^ 0xFFFF) & prefixT1) | (i0 & prefixT0)); + + int resultX = (a ^ i1) >>> (16 - level); + int resultY = (a ^ i0 ^ i1) >>> (16 - level); + return ((long) resultX << 32) | resultY; + } + + /** Returns the hilbert index at {@code level} for an x/y coordinate. */ + public static int hilbertXYToIndex(int level, int x, int y) { + x = x << (16 - level); + y = y << (16 - level); + + int hA, hB, hC, hD; + + int a1 = x ^ y; + int b1 = 0xFFFF ^ a1; + int c1 = 0xFFFF ^ (x | y); + int d1 = x & (y ^ 0xFFFF); + + hA = a1 | (b1 >>> 1); + hB = (a1 >>> 1) ^ a1; + + hC = ((c1 >>> 1) ^ (b1 & (d1 >>> 1))) ^ c1; + hD = ((a1 & (c1 >>> 1)) ^ (d1 >>> 1)) ^ d1; + + int a2 = hA; + int b2 = hB; + int c2 = hC; + int d2 = hD; + + hA = ((a2 & (a2 >>> 2)) ^ (b2 & (b2 >>> 2))); + hB = ((a2 & (b2 >>> 2)) ^ (b2 & ((a2 ^ b2) >>> 2))); + + hC ^= ((a2 & (c2 >>> 2)) ^ (b2 & (d2 >>> 2))); + hD ^= ((b2 & (c2 >>> 2)) ^ ((a2 ^ b2) & (d2 >>> 2))); + + int a3 = hA; + int b3 = hB; + int c3 = hC; + int d3 = hD; + + hA = ((a3 & (a3 >>> 4)) ^ (b3 & (b3 >>> 4))); + hB = ((a3 & (b3 >>> 4)) ^ (b3 & ((a3 ^ b3) >>> 4))); + + hC ^= ((a3 & (c3 >>> 4)) ^ (b3 & (d3 >>> 4))); + hD ^= ((b3 & (c3 >>> 4)) ^ ((a3 ^ b3) & (d3 >>> 4))); + + int a4 = hA; + int b4 = hB; + int c4 = hC; + int d4 = hD; + + hC ^= ((a4 & (c4 >>> 8)) ^ (b4 & (d4 >>> 8))); + hD ^= ((b4 & (c4 >>> 8)) ^ ((a4 ^ b4) & (d4 >>> 8))); + + int a = hC ^ (hC >>> 1); + int b = hD ^ (hD >>> 1); + + int i0 = x ^ y; + int i1 = b | (0xFFFF ^ (i0 | a)); + + return ((interleave(i1) << 1) | interleave(i0)) >>> (32 - 2 * level); + } +} diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java index f781ba84ff..3dcf473d1a 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java @@ -64,6 +64,8 @@ class PlanetilerTests { private static final String TEST_PROFILE_DESCRIPTION = "test description"; private static final String TEST_PROFILE_ATTRIBUTION = "test attribution"; private static final String TEST_PROFILE_VERSION = "test version"; + private static final int Z15_TILES = 1 << 15; + private static final double Z15_WIDTH = 1d / Z15_TILES; private static final int Z14_TILES = 1 << 14; private static final double Z14_WIDTH = 1d / Z14_TILES; private static final int Z13_TILES = 1 << 13; @@ -281,43 +283,49 @@ void testOverrideMetadata() throws Exception { @Test void testSinglePoint() throws Exception { - double x = 0.5 + Z14_WIDTH / 2; - double y = 0.5 + Z14_WIDTH / 2; + double x = 0.5 + Z14_WIDTH / 4; + double y = 0.5 + Z14_WIDTH / 4; double lat = GeoUtils.getWorldLat(y); double lng = GeoUtils.getWorldLon(x); var results = runWithReaderFeatures( - Map.of("threads", "1"), + Map.of("threads", "1", "maxzoom", "15"), List.of( newReaderFeature(newPoint(lng, lat), Map.of( "attr", "value" )) ), (in, features) -> features.point("layer") - .setZoomRange(13, 14) + .setZoomRange(13, 15) .setAttr("name", "name value") .inheritAttrFromSource("attr") ); assertSubmap(Map.of( - TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( + TileCoord.ofXYZ(Z15_TILES / 2, Z15_TILES / 2, 15), List.of( feature(newPoint(128, 128), Map.of( "attr", "value", "name", "name value" )) ), - TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of( + TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( feature(newPoint(64, 64), Map.of( "attr", "value", "name", "name value" )) + ), + TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of( + feature(newPoint(32, 32), Map.of( + "attr", "value", + "name", "name value" + )) ) ), results.tiles); assertSameJson( """ { "vector_layers": [ - {"id": "layer", "fields": {"name": "String", "attr": "String"}, "minzoom": 13, "maxzoom": 14} + {"id": "layer", "fields": {"name": "String", "attr": "String"}, "minzoom": 13, "maxzoom": 15} ] } """, @@ -656,6 +664,33 @@ void testPolygonWithHoleSpanningMultipleTiles() throws Exception { ), results.tiles); } + @Test + void testZ15Fill() throws Exception { + List outerPoints = z14CoordinateList( + -2, -2, + 2, -2, + 2, 2, + -2, 2, + -2, -2 + ); + + var results = runWithReaderFeatures( + Map.of("threads", "1", "maxzoom", "15"), + List.of( + newReaderFeature(newPolygon( + outerPoints + ), Map.of()) + ), + (in, features) -> features.polygon("layer") + .setZoomRange(15, 15) + .setBufferPixels(4) + ); + + assertEquals(List.of( + feature(newPolygon(tileFill(5)), Map.of()) + ), results.tiles.get(TileCoord.ofXYZ(Z15_TILES / 2, Z15_TILES / 2, 15))); + } + @Test void testFullWorldPolygon() throws Exception { var results = runWithReaderFeatures( @@ -1158,17 +1193,17 @@ void testPostProcessNodeUseLabelGridRank() throws Exception { @Test void testMergeLineStrings() throws Exception { - double y = 0.5 + Z14_WIDTH / 2; + double y = 0.5 + Z15_WIDTH / 2; double lat = GeoUtils.getWorldLat(y); - double x1 = 0.5 + Z14_WIDTH / 4; + double x1 = 0.5 + Z15_WIDTH / 4; double lng1 = GeoUtils.getWorldLon(x1); - double lng2 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 10d / 256); - double lng3 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 20d / 256); - double lng4 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 30d / 256); + double lng2 = GeoUtils.getWorldLon(x1 + Z15_WIDTH * 10d / 256); + double lng3 = GeoUtils.getWorldLon(x1 + Z15_WIDTH * 20d / 256); + double lng4 = GeoUtils.getWorldLon(x1 + Z15_WIDTH * 30d / 256); var results = runWithReaderFeatures( - Map.of("threads", "1"), + Map.of("threads", "1", "maxzoom", "15"), List.of( // merge at z13 (same "group"): newReaderFeature(newLineString( @@ -1186,22 +1221,27 @@ void testMergeLineStrings() throws Exception { ), Map.of("group", "2", "other", "3")) ), (in, features) -> features.line("layer") - .setZoomRange(13, 14) + .setMinZoom(13) .setAttrWithMinzoom("z14attr", in.getTag("other"), 14) .inheritAttrFromSource("group"), (layer, zoom, items) -> FeatureMerge.mergeLineStrings(items, 0, 0, 0) ); assertSubmap(sortListValues(Map.of( - TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( + TileCoord.ofXYZ(Z15_TILES / 2, Z15_TILES / 2, 15), List.of( feature(newLineString(64, 128, 74, 128), Map.of("group", "1", "z14attr", "1")), feature(newLineString(74, 128, 84, 128), Map.of("group", "1", "z14attr", "2")), feature(newLineString(84, 128, 94, 128), Map.of("group", "2", "z14attr", "3")) ), + TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( + feature(newLineString(32, 64, 37, 64), Map.of("group", "1", "z14attr", "1")), + feature(newLineString(37, 64, 42, 64), Map.of("group", "1", "z14attr", "2")), + feature(newLineString(42, 64, 47, 64), Map.of("group", "2", "z14attr", "3")) + ), TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of( // merge 32->37 and 37->42 since they have same attrs - feature(newLineString(32, 64, 42, 64), Map.of("group", "1")), - feature(newLineString(42, 64, 47, 64), Map.of("group", "2")) + feature(newLineString(16, 32, 21, 32), Map.of("group", "1")), + feature(newLineString(21, 32, 23.5, 32), Map.of("group", "2")) ) )), sortListValues(results.tiles)); } 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 0adce2940a..c5839d056a 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,7 +1,9 @@ package com.onthegomap.planetiler.geo; import static org.junit.jupiter.api.Assertions.assertEquals; +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; @@ -45,17 +47,28 @@ void testTileOrder(int x, int y, int z, int i) { assertEquals(decoded.z(), z, "z"); } + @Test + void testTileSortOrderRespectZ() { + int last = Integer.MIN_VALUE; + for (int z = 0; z <= 15; 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; + } + } + @ParameterizedTest @CsvSource({ "0,0,0,0", "0,1,1,0", - "0,0,1,0.25", "1,1,1,0.5", - "1,0,1,0.75", "0,3,2,0" }) void testTileProgressOnLevel(int x, int y, int z, double p) { - double progress = TileCoord.ofXYZ(x, y, z).progressOnLevel(); + double progress = + TileCoord.ofXYZ(x, y, z).progressOnLevel(TileExtents.computeFromWorldBounds(15, GeoUtils.WORLD_BOUNDS)); assertEquals(p, progress); } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/HilbertTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/HilbertTest.java new file mode 100644 index 0000000000..6cfc2bc24a --- /dev/null +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/HilbertTest.java @@ -0,0 +1,59 @@ +package com.onthegomap.planetiler.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class HilbertTest { + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 15}) + void testRoundTrip(int level) { + int max = (1 << level) * (1 << level); + int step = Math.max(1, max / 100); + for (int i = 0; i < max; i += step) { + long decoded = Hilbert.hilbertPositionToXY(level, i); + int x = Hilbert.extractX(decoded); + int y = Hilbert.extractY(decoded); + int reEncoded = Hilbert.hilbertXYToIndex(level, x, y); + if (reEncoded != i) { + fail("x=" + x + ", y=" + y + " index=" + i + " re-encoded=" + reEncoded); + } + } + } + + @ParameterizedTest + @CsvSource({ + "0,0,0,0", + + "1,0,0,0", + "1,0,1,1", + "1,1,1,2", + "1,1,0,3", + + "2,1,1,2", + + "15,0,0,0", + "15,0,1,1", + "15,1,1,2", + "15,1,0,3", + "15,32767,0,1073741823", + "15,32767,32767,715827882", + + "16,0,0,0", + "16,1,0,1", + "16,1,1,2", + "16,0,1,3", + "16,65535,0,-1", + "16,65535,65535,-1431655766", + }) + void testEncoding(int level, int x, int y, int encoded) { + int actualEncoded = Hilbert.hilbertXYToIndex(level, x, y); + assertEquals(encoded, actualEncoded); + long decoded = Hilbert.hilbertPositionToXY(level, encoded); + assertEquals(x, Hilbert.extractX(decoded)); + assertEquals(y, Hilbert.extractY(decoded)); + } +}