Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions config-example.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

# minzoom=0
# maxzoom=14
# tile_extent=4096

# Planetiler uses all available cores by default, but to override use:
# threads=4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<WithIndex<?>> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Layer> layers = new TreeMap<>();
Expand All @@ -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.
Expand Down Expand Up @@ -368,7 +382,11 @@ public static List<Feature> decode(byte[] encoded) {
List<Feature> 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<String> keys = layer.getKeysList();
List<Object> values = new ArrayList<>();

Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -791,8 +822,9 @@ public VectorGeometry finish() {
* specification</a>.
* <p>
* 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) {
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}


Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -76,6 +77,8 @@ public final class FeatureGroup implements Iterable<FeatureGroup.TileFeatures>,
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public record PlanetilerConfig(
int minzoom,
int maxzoom,
int maxzoomForRendering,
int tileExtent,
boolean force,
boolean append,
boolean compressTempStorage,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<String> extraNameTags = arguments.getList("extra_name_tags", "Extra name tags to copy from OSM to output",
List.of());
Expand All @@ -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())
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<PrecisionModel> tilePrecision =
new AtomicReference<>(new PrecisionModel(4096d / 256d));
Comment on lines +52 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we move all references to 4096 out of static variables and either pass them as args to functions that need them or extract them from geoutils to a class that you instantiate with a tile extent. That might make this PR very big though, let me know what you think - if it's too much I could do in a followup PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue also asks for the extent to be configurable per-zoom #1286 so we might even want to make it a global setting 🤔

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would you implement per zoom tile extent configuration? per zoom args?
--tileExtentZ12, --tileExtentZ13, etc... ?

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();
Expand Down Expand Up @@ -309,11 +324,11 @@ public static Geometry combinePoints(List<Point> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
/** Constructs a new feature render that will send rendered features to {@code consumer}. */
public FeatureRenderer(PlanetilerConfig config, Consumer<RenderedFeature> consumer, Stats stats,
Closeable closeable) {
VectorTile.setExtent(config.tileExtent());
GeoUtils.setTileExtent(config.tileExtent());
this.config = config;
this.consumer = consumer;
this.stats = stats;
Expand Down Expand Up @@ -141,7 +143,7 @@ private void renderPoint(int zoom, Map<String, Object> 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)
);
Expand Down Expand Up @@ -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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few more subtle places that the 4096 assumption has snuck in that might not explicitly reference 4096 - this is one of them, thanks for fixing! I'm trying to think if there might be any others...

}

if (!geom.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TileCoord, List<List<CoordinateSequence>>> tileContents = new HashMap<>();
private final TileExtents.ForZoom extents;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class LoopLineMerger {
private final List<Node> 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;
Expand Down
Loading
Loading