diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ddd7ba9f68..c5d2235f30 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -66,7 +66,7 @@ generate vector tile features according to the [profile](#profiles) in a worker - Uses an [IntRangeSet](planetiler-core/src/main/java/com/onthegomap/planetiler/collection/IntRangeSet.java) to optimize processing for large filled areas (like oceans) - If any features wrapped past -180 or 180 degrees longitude, repeat with a 360 or -360 degree offset - - Reassemble each vector tile geometry and round to tile precision (4096x4096) + - Reassemble each vector tile geometry and round to tile precision (default 4096x4096) - For polygons, [GeoUtils#snapAndFixPolygon](planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java) uses [JTS](https://github.com/locationtech/jts) utilities to fix any topology errors (i.e. self-intersections) diff --git a/config-example.properties b/config-example.properties index bf11420af0..f5cde36280 100644 --- a/config-example.properties +++ b/config-example.properties @@ -25,6 +25,7 @@ # minzoom=0 # maxzoom=14 +# tile_extent=4096 # Planetiler uses all available cores by default, but to override use: # threads=4 diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java index e326300ba1..301e9b9f2c 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureMerge.java @@ -59,7 +59,7 @@ public class FeatureMerge { private static final BufferParameters bufferOps = new BufferParameters(); // this is slightly faster than Comparator.comparingInt private static final Comparator> BY_HILBERT_INDEX = - (o1, o2) -> Integer.compare(o1.hilbert, o2.hilbert); + (o1, o2) -> Integer.compareUnsigned(o1.hilbert, o2.hilbert); static { bufferOps.setJoinStyle(BufferParameters.JOIN_MITRE); diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java index 56ee3dcb97..beb37b7074 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java @@ -10,6 +10,7 @@ import com.onthegomap.planetiler.collection.LongLongMultimap; import com.onthegomap.planetiler.config.Arguments; import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.geo.GeoUtils; import com.onthegomap.planetiler.reader.GeoPackageReader; import com.onthegomap.planetiler.reader.NaturalEarthReader; import com.onthegomap.planetiler.reader.ShapefileReader; @@ -132,6 +133,8 @@ private Planetiler(Arguments arguments) { stats = arguments.getStats(); overallTimer = stats.startStageQuietly("overall"); config = PlanetilerConfig.from(arguments); + VectorTile.setExtent(config.tileExtent()); + GeoUtils.setTileExtent(config.tileExtent()); if (config.color() != null) { AnsiColors.setUseColors(config.color()); } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/VectorTile.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/VectorTile.java index 3453b9ae64..0cd9b22e6e 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/VectorTile.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/VectorTile.java @@ -93,8 +93,11 @@ public class VectorTile { private static final Logger LOGGER = LoggerFactory.getLogger(VectorTile.class); - // TODO make these configurable - private static final int EXTENT = 4096; + public static final int DEFAULT_EXTENT = 4096; + // Global vector tile extent configured at runtime through PlanetilerConfig. + private static volatile int tileExtent = DEFAULT_EXTENT; + private static final int HILBERT_LEVEL = 16; + private static final long HILBERT_MAX_COORD = (1L << HILBERT_LEVEL) - 1; private static final double SIZE = 256d; // use a treemap to ensure that layers are encoded in a consistent order private final Map layers = new TreeMap<>(); @@ -106,6 +109,17 @@ private static int[] getCommands(Geometry input, int scale) { return encoder.result.toArray(); } + public static int extent() { + return tileExtent; + } + + public static void setExtent(int extent) { + if (extent <= 0) { + throw new IllegalArgumentException("tile_extent must be > 0, got " + extent); + } + tileExtent = extent; + } + /** * Scales a geometry down by a factor of {@code 2^scale} without materializing an intermediate JTS geometry and * returns the encoded result. @@ -368,7 +382,11 @@ public static List decode(byte[] encoded) { List features = new ArrayList<>(); for (VectorTileProto.Tile.Layer layer : tile.getLayersList()) { String layerName = layer.getName(); - assert layer.getExtent() == 4096; + if (layer.getExtent() != tileExtent) { + throw new IllegalStateException( + "Unsupported vector tile extent: " + layer.getExtent() + " (expected " + tileExtent + ")" + ); + } List keys = layer.getKeysList(); List values = new ArrayList<>(); @@ -447,9 +465,22 @@ public static VectorGeometryMerger newMerger(GeometryType geometryType) { */ public static int hilbertIndex(Geometry geometry) { Coordinate coord = geometry.getCoordinate(); - int x = zigZagEncode((int) Math.round(coord.x * 4096 / 256)); - int y = zigZagEncode((int) Math.round(coord.y * 4096 / 256)); - return (int) Hilbert.hilbertXYToIndex(15, x, y); + int x = zigZagEncode((int) Math.round(coord.x * tileExtent / SIZE)); + int y = zigZagEncode((int) Math.round(coord.y * tileExtent / SIZE)); + return hilbertIndexForEncodedCoords(x, y); + } + + private static int hilbertIndexForEncodedCoords(int x, int y) { + int shift = Math.max(hilbertShiftToFitLevel(x), hilbertShiftToFitLevel(y)); + return (int) Hilbert.hilbertXYToIndex(HILBERT_LEVEL, x >>> shift, y >>> shift); + } + + private static int hilbertShiftToFitLevel(int coord) { + long unsignedCoord = Integer.toUnsignedLong(coord); + if (unsignedCoord <= HILBERT_MAX_COORD) { + return 0; + } + return Long.SIZE - Long.numberOfLeadingZeros(unsignedCoord) - HILBERT_LEVEL; } /** @@ -479,8 +510,8 @@ public static int countGeometries(VectorTileProto.Tile.Feature feature) { * avoid needing to create an extra JTS geometry for encoding. */ public static VectorGeometry encodeFill(double buffer) { - int min = (int) Math.round(EXTENT * buffer / 256d); - int width = EXTENT + min + min; + int min = (int) Math.round(tileExtent * buffer / 256d); + int width = tileExtent + min + min; return new VectorGeometry(new int[]{ CommandEncoder.commandAndLength(Command.MOVE_TO, 1), zigZagEncode(-min), zigZagEncode(-min), @@ -556,7 +587,7 @@ public VectorTileProto.Tile toProto(boolean includeIds) { VectorTileProto.Tile.Layer.Builder tileLayer = VectorTileProto.Tile.Layer.newBuilder() .setVersion(2) .setName(layerName) - .setExtent(EXTENT) + .setExtent(tileExtent) .addAllKeys(layer.keys()); for (Object value : layer.values()) { @@ -682,7 +713,7 @@ public MapboxVectorTile toMltInput(Stats stats) { return null; } }).filter(Objects::nonNull).toList(); - return new org.maplibre.mlt.data.Layer(name, features, EXTENT); + return new org.maplibre.mlt.data.Layer(name, features, tileExtent); }).toList()); } @@ -791,8 +822,9 @@ public VectorGeometry finish() { * specification. *

* To encode extra precision in intermediate feature geometries, the geometry contained in {@code commands} is scaled - * to a tile extent of {@code EXTENT * 2^scale}, so when the {@code scale == 0} the extent is {@link #EXTENT} and when - * {@code scale == 2} the extent is 4x{@link #EXTENT}. Geometries must be scaled back to 0 using {@link #unscale()} + * to a tile extent of {@code tileExtent * 2^scale}, so when the {@code scale == 0} the extent is + * {@link #tileExtent} and when {@code scale == 2} the extent is 4x{@link #tileExtent}. Geometries must be scaled + * back to 0 using {@link #unscale()} * before outputting to the archive. */ public record VectorGeometry(int[] commands, GeometryType geomType, int scale) { @@ -857,7 +889,7 @@ private static boolean visitedEnoughSides(boolean allowEdges, int sides) { /** Converts an encoded geometry back to a JTS geometry. */ public Geometry decode() throws GeometryException { - return decodeCommands(geomType, commands, (EXTENT << scale) / SIZE); + return decodeCommands(geomType, commands, (tileExtent << scale) / SIZE); } /** Converts an encoded geometry back to a JTS geometry. */ @@ -926,7 +958,7 @@ public boolean isFillOrEdge(boolean allowEdges) { boolean isLine = geomType == GeometryType.LINE; - int extent = EXTENT << scale; + int extent = tileExtent << scale; int visited = INSIDE; int firstX = 0; int firstY = 0; @@ -1005,7 +1037,7 @@ public VectorGeometry filterPointsOutsideBuffer(double buffer) { } IntArrayList result = null; - int extent = (EXTENT << scale); + int extent = (tileExtent << scale); int bufferInt = (int) Math.ceil(buffer * extent / 256); int min = -bufferInt; int max = extent + bufferInt; @@ -1070,9 +1102,9 @@ public int hilbertIndex() { if (commands.length < 3) { return 0; } - int x = commands[1]; - int y = commands[2]; - return (int) Hilbert.hilbertXYToIndex(15, x >> scale, y >> scale); + int x = commands[1] >>> scale; + int y = commands[2] >>> scale; + return hilbertIndexForEncodedCoords(x, y); } @@ -1085,8 +1117,8 @@ public CoordinateXY firstCoordinate() { return null; } double factor = 1 << scale; - double x = zigZagDecode(commands[1]) * SIZE / EXTENT / factor; - double y = zigZagDecode(commands[2]) * SIZE / EXTENT / factor; + double x = zigZagDecode(commands[1]) * SIZE / tileExtent / factor; + double y = zigZagDecode(commands[2]) * SIZE / tileExtent / factor; return new CoordinateXY(x, y); } } @@ -1192,7 +1224,7 @@ private static class CommandEncoder { int x = 0, y = 0; CommandEncoder(int scale) { - this.SCALE = (EXTENT << scale) / SIZE; + this.SCALE = (tileExtent << scale) / SIZE; } static boolean shouldClosePath(Geometry geometry) { diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/collection/FeatureGroup.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/collection/FeatureGroup.java index 72c566c783..74dc27f238 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/collection/FeatureGroup.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/collection/FeatureGroup.java @@ -7,6 +7,7 @@ import com.onthegomap.planetiler.Profile; import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.geo.GeoUtils; import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.geo.GeometryType; import com.onthegomap.planetiler.geo.TileCoord; @@ -76,6 +77,8 @@ public final class FeatureGroup implements Iterable, this.profile = profile; this.config = config; this.stats = stats; + VectorTile.setExtent(config.tileExtent()); + GeoUtils.setTileExtent(config.tileExtent()); if (config.logJtsExceptions() && CURRENT_TILE == null) { CURRENT_TILE = ThreadLocal.withInitial(() -> null); } 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 3a826153c9..8c30785196 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 @@ -29,6 +29,7 @@ public record PlanetilerConfig( int minzoom, int maxzoom, int maxzoomForRendering, + int tileExtent, boolean force, boolean append, boolean compressTempStorage, @@ -90,6 +91,9 @@ public record PlanetilerConfig( if (maxzoom > MAX_MAXZOOM) { throw new IllegalArgumentException("Max zoom must be <= " + MAX_MAXZOOM + ", was " + maxzoom); } + if (tileExtent <= 0 || (tileExtent & (tileExtent - 1)) != 0) { + throw new IllegalArgumentException("tile_extent must be a power of 2, was " + tileExtent); + } if (httpRetries < 0) { throw new IllegalArgumentException("HTTP Retries must be >= 0, was " + httpRetries); } @@ -139,6 +143,8 @@ public static PlanetilerConfig from(Arguments arguments) { int renderMaxzoom = arguments.getInteger("render_maxzoom", "maximum rendering zoom level up to " + MAX_MAXZOOM, Math.max(maxzoom, DEFAULT_MAXZOOM)); + int tileExtent = arguments.getInteger("tile_extent", + "vector tile extent (default 4096)", 4096); Path tmpDir = arguments.file("tmpdir|tmp", "temp directory", Path.of("data", "tmp")); List extraNameTags = arguments.getList("extra_name_tags", "Extra name tags to copy from OSM to output", List.of()); @@ -162,6 +168,7 @@ public static PlanetilerConfig from(Arguments arguments) { minzoom, maxzoom, renderMaxzoom, + tileExtent, arguments.getBoolean("force", "overwriting output file and ignore disk/RAM warnings", false), arguments.getBoolean("append", "append to the output file - only supported by " + Stream.of(TileArchiveConfig.Format.values()) @@ -196,13 +203,13 @@ public static PlanetilerConfig from(Arguments arguments) { "Maximum bandwidth to consume when downloading files in units mb/s, mbps, kbps, etc.", "")), arguments.getDouble("min_feature_size_at_max_zoom", "Default value for the minimum size in tile pixels of features to emit at the maximum zoom level to allow for overzooming", - 256d / 4096), + 256d / tileExtent), arguments.getDouble("min_feature_size", "Default value for the minimum size in tile pixels of features to emit below the maximum zoom level", 1), arguments.getDouble("simplify_tolerance_at_max_zoom", "Default value for the tile pixel tolerance to use when simplifying features at the maximum zoom level to allow for overzooming", - 256d / 4096), + 256d / tileExtent), arguments.getDouble("simplify_tolerance", "Default value for the tile pixel tolerance to use when simplifying features below the maximum zoom level", 0.1d), diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java index 4b684ae4b4..9135ad4723 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java @@ -5,6 +5,7 @@ import com.onthegomap.planetiler.stats.Stats; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import java.util.stream.Stream; import org.geotools.api.referencing.FactoryException; @@ -48,10 +49,24 @@ */ public class GeoUtils { - /** Rounding precision for 256x256px tiles encoded using 4096 values. */ - public static final PrecisionModel TILE_PRECISION = new PrecisionModel(4096d / 256d); + /** + * Rounding precision for 256x256px tiles encoded using {@code tile_extent} values (default 4096). + */ + private static final AtomicReference tilePrecision = + new AtomicReference<>(new PrecisionModel(4096d / 256d)); public static final GeometryFactory JTS_FACTORY = new GeometryFactory(PackedCoordinateSequenceFactory.DOUBLE_FACTORY); + public static PrecisionModel tilePrecision() { + return tilePrecision.get(); + } + + public static void setTileExtent(int tileExtent) { + if (tileExtent <= 0) { + throw new IllegalArgumentException("tile_extent must be > 0, got " + tileExtent); + } + tilePrecision.set(new PrecisionModel(tileExtent / 256d)); + } + public static final Geometry EMPTY_GEOMETRY = JTS_FACTORY.createGeometryCollection(); public static final CoordinateSequence EMPTY_COORDINATE_SEQUENCE = new PackedCoordinateSequence.Double(0, 2, 0); public static final Point EMPTY_POINT = JTS_FACTORY.createPoint(); @@ -309,11 +324,11 @@ public static Geometry combinePoints(List points) { } /** - * Returns a copy of {@code geom} with coordinates rounded to {@link #TILE_PRECISION} and fixes any polygon + * Returns a copy of {@code geom} with coordinates rounded to {@link #tilePrecision()} and fixes any polygon * self-intersections or overlaps that may have caused. */ public static Geometry snapAndFixPolygon(Geometry geom, Stats stats, String stage) throws GeometryException { - return snapAndFixPolygon(geom, TILE_PRECISION, stats, stage); + return snapAndFixPolygon(geom, tilePrecision(), stats, stage); } private static class OrientationFixer extends GeometryTransformer { diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/render/FeatureRenderer.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/render/FeatureRenderer.java index a29477ee6e..4c989913f9 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/render/FeatureRenderer.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/render/FeatureRenderer.java @@ -49,6 +49,8 @@ public class FeatureRenderer implements Consumer, Clos /** Constructs a new feature render that will send rendered features to {@code consumer}. */ public FeatureRenderer(PlanetilerConfig config, Consumer consumer, Stats stats, Closeable closeable) { + VectorTile.setExtent(config.tileExtent()); + GeoUtils.setTileExtent(config.tileExtent()); this.config = config; this.consumer = consumer; this.stats = stats; @@ -141,7 +143,7 @@ private void renderPoint(int zoom, Map attrs, FeatureCollector.F RenderedFeature.Group groupInfo = null; if (hasLabelGrid && coords.length == 1) { double labelGridTileSize = feature.getPointLabelGridPixelSizeAtZoom(zoom) / 256d; - groupInfo = labelGridTileSize < 1d / 4096d ? null : new RenderedFeature.Group( + groupInfo = labelGridTileSize < 1d / config.tileExtent() ? null : new RenderedFeature.Group( GeoUtils.labelGridId(tilesAtZoom, labelGridTileSize, coords[0]), feature.getPointLabelGridLimitAtZoom(zoom) ); @@ -264,9 +266,11 @@ private void writeTileFeatures(int zoom, long id, FeatureCollector.Feature featu // post-processing. Features need to be "unscaled" in FeatureGroup after line merging, // and before emitting to the output archive. scale = Math.max(config.maxzoom(), 14) - zoom; - // need 14 bits to represent tile coordinates (4096 * 2 for buffer * 2 for zigzag encoding) + // need enough bits to represent tile coordinates (extent * 2 for buffer * 2 for zigzag encoding) // so cap the scale factor to avoid overflowing 32-bit integer space - scale = Math.min(31 - 14, scale); + long maxCoordinate = config.tileExtent() * 4L; + int bits = 64 - Long.numberOfLeadingZeros(maxCoordinate - 1); + scale = Math.clamp(scale, 0, Math.max(0, 31 - bits)); } if (!geom.isEmpty()) { diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/render/TiledGeometry.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/render/TiledGeometry.java index 07534fc445..ba92923bad 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/render/TiledGeometry.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/render/TiledGeometry.java @@ -20,6 +20,7 @@ import com.carrotsearch.hppc.IntObjectMap; import com.carrotsearch.hppc.cursors.IntCursor; import com.carrotsearch.hppc.cursors.IntObjectCursor; +import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.collection.Hppc; import com.onthegomap.planetiler.collection.IntRangeSet; import com.onthegomap.planetiler.geo.GeoUtils; @@ -71,7 +72,6 @@ public class TiledGeometry { private static final Format FORMAT = Format.defaultInstance(); - private static final double NEIGHBOR_BUFFER_EPS = 0.1d / 4096; private final Map>> tileContents = new HashMap<>(); private final TileExtents.ForZoom extents; @@ -87,7 +87,7 @@ private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean this.extents = extents; this.buffer = buffer; // make sure we inspect neighboring tiles when a line runs along an edge - this.neighborBuffer = buffer + NEIGHBOR_BUFFER_EPS; + this.neighborBuffer = buffer + 0.1d / VectorTile.extent(); this.z = z; this.area = area; this.maxTilesAtThisZoom = 1 << z; diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LoopLineMerger.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LoopLineMerger.java index a80cf1a5a5..6d75215f69 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LoopLineMerger.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LoopLineMerger.java @@ -42,7 +42,7 @@ public class LoopLineMerger { private final List output = new ArrayList<>(); private int numNodes = 0; private int numEdges = 0; - private PrecisionModel precisionModel = new PrecisionModel(GeoUtils.TILE_PRECISION); + private PrecisionModel precisionModel = new PrecisionModel(GeoUtils.tilePrecision()); private GeometryFactory factory = new GeometryFactory(precisionModel); private double minLength = 0.0; private double loopMinLength = 0.0; diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureCollectorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureCollectorTest.java index 8fa6e39449..8485cbd22e 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureCollectorTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/FeatureCollectorTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.onthegomap.planetiler.config.Arguments; import com.onthegomap.planetiler.config.PlanetilerConfig; import com.onthegomap.planetiler.geo.GeoUtils; import com.onthegomap.planetiler.geo.GeometryException; @@ -244,6 +245,16 @@ void testSetTolerance() { assertEquals(256d / 4096, poly.getPixelToleranceAtZoom(14)); } + @Test + void testTileExtentArgAffectsDefaultMaxZoomThresholds() { + var customConfig = PlanetilerConfig.from(Arguments.of(Map.of("tile_extent", "8192"))); + var customFactory = new FeatureCollector.Factory(customConfig, Stats.inMemory()); + var collector = customFactory.get(newReaderFeature(rectangle(10, 20), Map.of())); + var poly = collector.polygon("layername"); + assertEquals(256d / 8192, poly.getMinPixelSizeAtZoom(14)); + assertEquals(256d / 8192, poly.getPixelToleranceAtZoom(14)); + } + @Test void testSetToleranceAtAllZooms() { var collector = factory.get(newReaderFeature(rectangle(10, 20), Map.of())); 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 1f402eb4e3..f7741ffaf9 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java @@ -2088,11 +2088,91 @@ void testMergeLineStringsIgnoresRoundingIntersections() throws Exception { feature(newMultiLineString( newLineString(32, 64.3125, 37, 64.0625, 42, 64.3125), newLineString(32, 64, 37, 64.0625, 42, 64) - ), Map.of()) + ), "layer", Map.of(), 0) ) )), sortListValues(results.tiles)); } + @Test + void testTileExtentWorstCasePointLinePolygon() throws Exception { + var baseline = runTileExtentWorstCasePointLinePolygon(4096); + + // Include both high and low extents to stress clipping/rounding behavior. + for (int tileExtent : List.of(512, 1024, 8192, 16384)) { + var result = runTileExtentWorstCasePointLinePolygon(tileExtent); + assertEquals(sortListValues(baseline.tiles), sortListValues(result.tiles), "tile_extent=" + tileExtent); + } + } + + private PlanetilerResults runTileExtentWorstCasePointLinePolygon(int tileExtent) throws Exception { + var points = newMultiPoint( + z14Point(-255, -255), + z14Point(513, -255), + z14Point(513, 513), + z14Point(-255, 513), + z14Point(0, 0), + z14Point(128, 128), + z14Point(256, 256) + ); + + var lines = newMultiLineString( + newLineString(z14CoordinatePixelList( + -255, -255, + 513, -255, + 513, 513, + -255, 513, + -255, -255 + )), + newLineString(z14CoordinatePixelList( + 0, 0, + 128, 128, + 256, 256 + )) + ); + + var polygon = newPolygon(z14CoordinatePixelList( + -255, -255, + 513, -255, + 513, 513, + -255, 513, + -255, -255 + )); + + return runWithReaderFeatures( + Map.of( + "threads", "1", + "maxzoom", "14", + "tile_extent", Integer.toString(tileExtent) + ), + List.of( + newReaderFeature(points, Map.of()), + newReaderFeature(lines, Map.of()), + newReaderFeature(polygon, Map.of()) + ), + (in, features) -> { + if (in.isPoint()) { + features.point("points") + .setZoomRange(14, 14) + .setBufferPixels(257); + } + if (in.canBeLine()) { + features.line("lines") + .setZoomRange(14, 14) + .setBufferPixels(257) + .setMinPixelSize(0) + .setPixelTolerance(0); + } + if (in.canBePolygon()) { + features.polygon("polygons") + .setZoomRange(14, 14) + .setBufferPixels(257) + .setMinPixelSize(0) + .setPixelTolerance(0); + } + } + ); + } + @ParameterizedTest @ValueSource(booleans = {false, true}) void testMergePolygons(boolean unionOverlapping) throws Exception { diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/VectorTileTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/VectorTileTest.java index d3f9346958..e8e78952bb 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/VectorTileTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/VectorTileTest.java @@ -256,6 +256,24 @@ void testRoundTripMultipoint() { })); } + @Test + void testHilbertIndexLargeTileExtent() { + int originalExtent = VectorTile.extent(); + try { + VectorTile.setExtent(16384); + var point = newPoint(513, 513); + + int fromGeometry = VectorTile.hilbertIndex(point); + int fromEncodedGeometry = VectorTile.encodeGeometry(point).hilbertIndex(); + int fromScaledEncodedGeometry = VectorTile.encodeGeometry(point, 2).hilbertIndex(); + + assertEquals(fromGeometry, fromEncodedGeometry); + assertEquals(fromGeometry, fromScaledEncodedGeometry); + } finally { + VectorTile.setExtent(originalExtent); + } + } + @Test void testRoundTripLineString() { testRoundTripGeometry(JTS_FACTORY.createLineString(new Coordinate[]{ diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/config/PlanetilerConfigTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/config/PlanetilerConfigTest.java new file mode 100644 index 0000000000..c738adcefb --- /dev/null +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/config/PlanetilerConfigTest.java @@ -0,0 +1,22 @@ +package com.onthegomap.planetiler.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class PlanetilerConfigTest { + + @Test + void testTileExtentMustBePowerOf2() { + var exception = assertThrows(IllegalArgumentException.class, + () -> PlanetilerConfig.from(Arguments.of("tile_extent", "5000"))); + assertTrue(exception.getMessage().contains("power of 2")); + } + + @Test + void testTileExtentPowerOf2Allowed() { + assertEquals(8192, PlanetilerConfig.from(Arguments.of("tile_extent", "8192")).tileExtent()); + } +}