org.openstreetmap.osmosis
osmosis-osm-binary
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/collection/IntRangeSet.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/collection/IntRangeSet.java
index 83d2df32..f918f5d1 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/collection/IntRangeSet.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/collection/IntRangeSet.java
@@ -1,99 +1,84 @@
package com.onthegomap.planetiler.collection;
-import com.google.common.collect.Range;
-import com.google.common.collect.TreeRangeSet;
-import java.util.Iterator;
import java.util.PrimitiveIterator;
import javax.annotation.concurrent.NotThreadSafe;
+import org.roaringbitmap.PeekableIntIterator;
+import org.roaringbitmap.RoaringBitmap;
/**
- * A set of ints backed by a {@link TreeRangeSet} to efficiently represent large continuous ranges.
+ * A set of ints backed by a {@link RoaringBitmap} to efficiently represent large continuous ranges.
*
* This makes iterating through tile coordinates inside ocean polygons significantly faster.
*/
-@SuppressWarnings("UnstableApiUsage")
@NotThreadSafe
public class IntRangeSet implements Iterable {
- private final TreeRangeSet rangeSet = TreeRangeSet.create();
+ private final RoaringBitmap bitmap = new RoaringBitmap();
/** Mutates and returns this range set, adding all elements in {@code other} to it. */
public IntRangeSet addAll(IntRangeSet other) {
- rangeSet.addAll(other.rangeSet);
+ bitmap.or(other.bitmap);
return this;
}
/** Mutates and returns this range set, removing all elements in {@code other} from it. */
public IntRangeSet removeAll(IntRangeSet other) {
- rangeSet.removeAll(other.rangeSet);
+ bitmap.andNot(other.bitmap);
return this;
}
+ public static void main(String[] args) {
+ var set = new IntRangeSet();
+ set.add(0, 100000);
+ set.remove(10000);
+ System.err.println(set.bitmap.getSizeInBytes());
+ set.bitmap.runOptimize();
+ System.err.println(set.bitmap.getSizeInBytes());
+ }
+
@Override
public PrimitiveIterator.OfInt iterator() {
- return new Iter(rangeSet.asRanges().iterator());
+ return new Iter(bitmap.getIntIterator());
}
/** Mutates and returns this range set, with range {@code a} to {@code b} (inclusive) added. */
public IntRangeSet add(int a, int b) {
- rangeSet.add(Range.closedOpen(a, b + 1));
+ bitmap.add(a, (long) b + 1);
return this;
}
/** Mutates and returns this range set, with {@code a} removed. */
public IntRangeSet remove(int a) {
- rangeSet.remove(Range.closedOpen(a, a + 1));
+ bitmap.remove(a);
return this;
}
public boolean contains(int y) {
- return rangeSet.contains(y);
+ return bitmap.contains(y);
+ }
+
+ /** Returns the underlying {@link RoaringBitmap} for this int range. */
+ public RoaringBitmap bitmap() {
+ return bitmap;
}
/** Mutates and returns this range set to remove all elements not in {@code other} */
public IntRangeSet intersect(IntRangeSet other) {
- rangeSet.removeAll(other.rangeSet.complement());
+ bitmap.and(other.bitmap);
return this;
}
/** Iterate through all ints in this range */
- private static class Iter implements PrimitiveIterator.OfInt {
+ private record Iter(PeekableIntIterator iter) implements PrimitiveIterator.OfInt {
- private final Iterator> rangeIter;
- Range range;
- int cur;
- boolean hasNext = true;
-
- private Iter(Iterator> rangeIter) {
- this.rangeIter = rangeIter;
- advance();
- }
-
- private void advance() {
- while (true) {
- if (range != null && cur < range.upperEndpoint() - 1) {
- cur++;
- return;
- } else if (rangeIter.hasNext()) {
- range = rangeIter.next();
- cur = range.lowerEndpoint() - 1;
- } else {
- hasNext = false;
- return;
- }
- }
+ @Override
+ public boolean hasNext() {
+ return iter.hasNext();
}
@Override
public int nextInt() {
- int result = cur;
- advance();
- return result;
- }
-
- @Override
- public boolean hasNext() {
- return hasNext;
+ return iter.next();
}
}
}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Bounds.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Bounds.java
index ef489b3e..f77560bf 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Bounds.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Bounds.java
@@ -4,6 +4,7 @@ import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.TileExtents;
import com.onthegomap.planetiler.reader.osm.OsmInputFile;
import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,6 +22,8 @@ public class Bounds {
private Envelope world;
private TileExtents tileExtents;
+ private Geometry shape;
+
Bounds(Envelope latLon) {
set(latLon);
}
@@ -35,7 +38,7 @@ public class Bounds {
public TileExtents tileExtents() {
if (tileExtents == null) {
- tileExtents = TileExtents.computeFromWorldBounds(PlanetilerConfig.MAX_MAXZOOM, world());
+ tileExtents = TileExtents.computeFromWorldBounds(PlanetilerConfig.MAX_MAXZOOM, world(), shape);
}
return tileExtents;
}
@@ -52,11 +55,20 @@ public class Bounds {
return this;
}
+ /** Planetiler will emit any tile that intersects {@code shape}. */
+ public Bounds setShape(Geometry shape) {
+ this.shape = shape;
+ if (latLon == null) {
+ set(shape.getEnvelopeInternal());
+ }
+ return this;
+ }
+
private void set(Envelope latLon) {
if (latLon != null) {
this.latLon = latLon;
this.world = GeoUtils.toWorldBounds(latLon);
- this.tileExtents = TileExtents.computeFromWorldBounds(PlanetilerConfig.MAX_MAXZOOM, world);
+ this.tileExtents = TileExtents.computeFromWorldBounds(PlanetilerConfig.MAX_MAXZOOM, world, shape);
}
}
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 cb17075f..4d429725 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
@@ -2,6 +2,10 @@ package com.onthegomap.planetiler.config;
import com.onthegomap.planetiler.collection.LongLongMap;
import com.onthegomap.planetiler.collection.Storage;
+import com.onthegomap.planetiler.reader.osm.PolyFileReader;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
import java.time.Duration;
import java.util.stream.Stream;
@@ -90,9 +94,19 @@ public record PlanetilerConfig(
int featureProcessThreads =
arguments.getInteger("process_threads", "number of threads to use when processing input features",
Math.max(threads < 4 ? threads : (threads - featureWriteThreads), 1));
+ Bounds bounds = new Bounds(arguments.bounds("bounds", "bounds"));
+ Path polygonFile =
+ arguments.file("polygon", "a .poly file that limits output to tiles intersecting the shape", null);
+ if (polygonFile != null) {
+ try {
+ bounds.setShape(PolyFileReader.parsePolyFile(polygonFile));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
return new PlanetilerConfig(
arguments,
- new Bounds(arguments.bounds("bounds", "bounds")),
+ bounds,
threads,
featureWriteThreads,
featureProcessThreads,
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 accfeed1..e77fa2a8 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
@@ -329,13 +329,13 @@ public class GeoUtils {
*
* @param tilesAtZoom the tile width of the world at this zoom level
* @param labelGridTileSize the tile width of each grid square
- * @param coord the coordinate
+ * @param coord the coordinate, scaled to this zoom level
* @return an ID representing the grid square that {@code coord} falls into.
*/
public static long labelGridId(int tilesAtZoom, double labelGridTileSize, Coordinate coord) {
return GeoUtils.longPair(
- (int) Math.floor(wrapDouble(coord.getX() * tilesAtZoom, tilesAtZoom) / labelGridTileSize),
- (int) Math.floor((coord.getY() * tilesAtZoom) / labelGridTileSize)
+ (int) Math.floor(wrapDouble(coord.getX(), tilesAtZoom) / labelGridTileSize),
+ (int) Math.floor((coord.getY()) / labelGridTileSize)
);
}
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 fea2dd8c..be209361 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
@@ -118,6 +118,7 @@ public record TileCoord(int encoded, int x, int y, int z) implements Comparable<
);
}
+
/** Returns a URL that displays the openstreetmap data for this tile. */
public String getDebugUrl() {
Coordinate coord = getLatLon();
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileExtents.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileExtents.java
index 0c4bf89d..2d464541 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileExtents.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileExtents.java
@@ -1,13 +1,19 @@
package com.onthegomap.planetiler.geo;
+import com.onthegomap.planetiler.render.TiledGeometry;
import java.util.function.Predicate;
import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.util.AffineTransformation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* A function that filters to only tile coordinates that overlap a given {@link Envelope}.
*/
public class TileExtents implements Predicate {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TileExtents.class);
private final ForZoom[] zoomExtents;
private TileExtents(ForZoom[] zoomExtents) {
@@ -24,15 +30,34 @@ public class TileExtents implements Predicate {
/** Returns a filter to tiles that intersect {@code worldBounds} (specified in world web mercator coordinates). */
public static TileExtents computeFromWorldBounds(int maxzoom, Envelope worldBounds) {
+ return computeFromWorldBounds(maxzoom, worldBounds, null);
+ }
+
+
+ /** Returns a filter to tiles that intersect {@code worldBounds} (specified in world web mercator coordinates). */
+ public static TileExtents computeFromWorldBounds(int maxzoom, Envelope worldBounds, Geometry shape) {
ForZoom[] zoomExtents = new ForZoom[maxzoom + 1];
+ var mercator = shape == null ? null : GeoUtils.latLonToWorldCoords(shape);
for (int zoom = 0; zoom <= maxzoom; zoom++) {
int max = 1 << zoom;
- zoomExtents[zoom] = new ForZoom(
+
+ var forZoom = new ForZoom(
+ zoom,
quantizeDown(worldBounds.getMinX(), max),
quantizeDown(worldBounds.getMinY(), max),
quantizeUp(worldBounds.getMaxX(), max),
- quantizeUp(worldBounds.getMaxY(), max)
+ quantizeUp(worldBounds.getMaxY(), max),
+ null
);
+
+ if (mercator != null) {
+ Geometry scaled = AffineTransformation.scaleInstance(1 << zoom, 1 << zoom).transform(mercator);
+ TiledGeometry.CoveredTiles covered = TiledGeometry.getCoveredTiles(scaled, zoom, forZoom);
+ forZoom = forZoom.withShape(covered);
+ LOGGER.info("prepareShapeForZoom z{} {}", zoom, covered);
+ }
+
+ zoomExtents[zoom] = forZoom;
}
return new TileExtents(zoomExtents);
}
@@ -52,12 +77,25 @@ public class TileExtents implements Predicate {
/**
* X/Y extents within a given zoom level. {@code minX} and {@code minY} are inclusive and {@code maxX} and {@code
- * maxY} are exclusive.
+ * maxY} are exclusive. shape is an optional polygon defining a more refine shape
*/
- public record ForZoom(int minX, int minY, int maxX, int maxY) {
+ public record ForZoom(int z, int minX, int minY, int maxX, int maxY, TilePredicate shapeFilter)
+ implements TilePredicate {
+ public ForZoom withShape(TilePredicate shape) {
+ return new ForZoom(z, minX, minY, maxX, maxY, shape);
+ }
+
+ @Override
public boolean test(int x, int y) {
- return testX(x) && testY(y);
+ return testX(x) && testY(y) && testOverShape(x, y);
+ }
+
+ private boolean testOverShape(int x, int y) {
+ if (shapeFilter != null) {
+ return shapeFilter.test(x, y);
+ }
+ return true;
}
public boolean testX(int x) {
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TilePredicate.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TilePredicate.java
new file mode 100644
index 00000000..5e6831da
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TilePredicate.java
@@ -0,0 +1,5 @@
+package com.onthegomap.planetiler.geo;
+
+public interface TilePredicate {
+ boolean test(int x, int y);
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/PolyFileReader.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/PolyFileReader.java
new file mode 100644
index 00000000..4577b804
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/osm/PolyFileReader.java
@@ -0,0 +1,103 @@
+package com.onthegomap.planetiler.reader.osm;
+
+import static com.onthegomap.planetiler.geo.GeoUtils.JTS_FACTORY;
+
+import com.onthegomap.planetiler.geo.GeoUtils;
+import com.onthegomap.planetiler.geo.MutableCoordinateSequence;
+import com.onthegomap.planetiler.reader.FileFormatException;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.locationtech.jts.geom.Geometry;
+
+/**
+ * Parse a polygon file used for filtering planetiler output to a specific shape.
+ *
+ * @see Osmosis/Polygon Filter File
+ * Format
+ */
+public class PolyFileReader {
+
+ private PolyFileReader() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ /**
+ * Reads a polygon from a file.
+ */
+ public static Geometry parsePolyFile(Path polyFile) throws IOException {
+ try (BufferedReader reader = Files.newBufferedReader(polyFile)) {
+ return parsePolyFile(reader);
+ }
+ }
+
+ /**
+ * Reads a polygon from a string.
+ */
+ public static Geometry parsePolyFile(String polyFile) throws IOException {
+ try (Reader reader = new StringReader(polyFile)) {
+ return parsePolyFile(reader);
+ }
+ }
+
+ /**
+ * Reads a polygon from a {@link Reader} {@code input}.
+ */
+ public static Geometry parsePolyFile(Reader input) throws IOException {
+ Geometry result = GeoUtils.EMPTY_POLYGON;
+ try (BufferedReader reader = input instanceof BufferedReader br ? br : new BufferedReader(input)) {
+ String line;
+ MutableCoordinateSequence currentRing = null;
+ boolean firstLine = true, inRing = false, inPolygon = true, hole = false;
+ while ((line = reader.readLine()) != null) {
+ if (line.isBlank()) {
+ // ingore line
+ } else if (!inPolygon) {
+ throw new FileFormatException("File continues after end of polygon");
+ } else if (firstLine) {
+ firstLine = false;
+ // first line is junk.
+ } else if (inRing) {
+ if (line.strip().equals("END")) {
+ // we are at the end of a ring, perhaps with more to come.
+ currentRing.closeRing();
+ var polygon = JTS_FACTORY.createPolygon(JTS_FACTORY.createLinearRing(currentRing), null);
+ if (hole) {
+ result = result.difference(polygon);
+ } else {
+ result = result.union(polygon);
+ }
+ currentRing = null;
+ inRing = false;
+ } else {
+ // we are in a ring and picking up new coordinates.
+ String[] splitted = line.trim().split("\s+");
+ currentRing.addPoint(Double.parseDouble(splitted[0]), Double.parseDouble(splitted[1]));
+ }
+ } else {
+ if (line.strip().equals("END")) {
+ // we are at the end of the whole polygon.
+ inPolygon = false;
+ } else {
+ // we are at the start of a polygon part.
+ currentRing = new MutableCoordinateSequence();
+ hole = line.strip().charAt(0) == '!';
+ inRing = true;
+ }
+ }
+ }
+
+ if (inRing) {
+ throw new FileFormatException("Unclosed ring");
+ }
+ if (inPolygon) {
+ throw new FileFormatException("File ends before end of polygon");
+ }
+
+ return result;
+ }
+ }
+}
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 7632d716..d0a07685 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
@@ -94,13 +94,24 @@ public class FeatureRenderer implements Consumer, Clos
}
}
- private void renderPoint(FeatureCollector.Feature feature, Coordinate... coords) {
+ private void renderPoint(FeatureCollector.Feature feature, Coordinate... origCoords) {
long id = idGenerator.incrementAndGet();
boolean hasLabelGrid = feature.hasLabelGrid();
+ Coordinate[] coords = new Coordinate[origCoords.length];
+ for (int i = 0; i < origCoords.length; i++) {
+ coords[i] = origCoords[i].copy();
+ }
for (int zoom = feature.getMaxZoom(); zoom >= feature.getMinZoom(); zoom--) {
Map attrs = feature.getAttrsAtZoom(zoom);
double buffer = feature.getBufferPixelsAtZoom(zoom) / 256;
int tilesAtZoom = 1 << zoom;
+ // scale coordinates for this zoom
+ for (int i = 0; i < coords.length; i++) {
+ var orig = origCoords[i];
+ coords[i].setX(orig.x * tilesAtZoom);
+ coords[i].setY(orig.y * tilesAtZoom);
+ }
+
// for "label grid" point density limiting, compute the grid square that this point sits in
// only valid if not a multipoint
@@ -115,9 +126,9 @@ public class FeatureRenderer implements Consumer, Clos
// compute the tile coordinate of every tile these points should show up in at the given buffer size
TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(zoom);
- TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords, feature.getSourceId());
+ TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords);
int emitted = 0;
- for (var entry : tiled.getTileData()) {
+ for (var entry : tiled.getTileData().entrySet()) {
TileCoord tile = entry.getKey();
List> result = entry.getValue();
Geometry geom = GeometryCoordinateSequences.reassemblePoints(result);
@@ -186,7 +197,7 @@ public class FeatureRenderer implements Consumer, Clos
List> groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
double buffer = feature.getBufferPixelsAtZoom(z) / 256;
TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(z);
- TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents, feature.getSourceId());
+ TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
Map attrs = feature.getAttrsAtZoom(sliced.zoomLevel());
if (numPointsAttr != null) {
// if profile wants the original number of points that the simplified but untiled geometry started with
@@ -202,7 +213,7 @@ public class FeatureRenderer implements Consumer, Clos
private void writeTileFeatures(int zoom, long id, FeatureCollector.Feature feature, TiledGeometry sliced,
Map attrs) {
int emitted = 0;
- for (var entry : sliced.getTileData()) {
+ for (var entry : sliced.getTileData().entrySet()) {
TileCoord tile = entry.getKey();
try {
List> geoms = entry.getValue();
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 22675822..c9958685 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
@@ -26,20 +26,33 @@ import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.MutableCoordinateSequence;
import com.onthegomap.planetiler.geo.TileCoord;
import com.onthegomap.planetiler.geo.TileExtents;
+import com.onthegomap.planetiler.geo.TilePredicate;
+import com.onthegomap.planetiler.util.Format;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
+import java.util.stream.Stream;
import javax.annotation.concurrent.NotThreadSafe;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryCollection;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.Lineal;
+import org.locationtech.jts.geom.MultiLineString;
+import org.locationtech.jts.geom.MultiPoint;
+import org.locationtech.jts.geom.MultiPolygon;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.geom.Polygonal;
+import org.locationtech.jts.geom.Puntal;
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.roaringbitmap.RoaringBitmap;
/**
* Splits geometries represented by lists of {@link CoordinateSequence CoordinateSequences} into the geometries that
@@ -48,28 +61,27 @@ import org.slf4j.LoggerFactory;
* {@link GeometryCoordinateSequences} converts between JTS {@link Geometry} instances and {@link CoordinateSequence}
* lists for this utility.
*
- * This class is adapted from the stripe clipping algorithm in https://github.com/mapbox/geojson-vt/ and modified so
- * that it eagerly produces all sliced tiles at a zoom level for each input geometry.
+ * This class is adapted from the stripe clipping algorithm in
+ * geojson-vt and modified so that it eagerly produces all sliced
+ * tiles at a zoom level for each input geometry.
*/
@NotThreadSafe
-class TiledGeometry {
+public class TiledGeometry {
- private static final Logger LOGGER = LoggerFactory.getLogger(TiledGeometry.class);
+ private static final Format FORMAT = Format.defaultInstance();
private static final double NEIGHBOR_BUFFER_EPS = 0.1d / 4096;
- private final long featureId;
private final Map>> tileContents = new HashMap<>();
- /** Map from X coordinate to range of Y coordinates that contain filled tiles inside this geometry */
- private Map filledRanges = null;
private final TileExtents.ForZoom extents;
private final double buffer;
private final double neighborBuffer;
private final int z;
private final boolean area;
private final int maxTilesAtThisZoom;
+ /** Map from X coordinate to range of Y coordinates that contain filled tiles inside this geometry */
+ private Map filledRanges = null;
- private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area, long featureId) {
- this.featureId = featureId;
+ private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area) {
this.extents = extents;
this.buffer = buffer;
// make sure we inspect neighboring tiles when a line runs along an edge
@@ -87,12 +99,11 @@ class TiledGeometry {
* @param z zoom level
* @param coords the world web mercator coordinates of each point to emit at this zoom level where (0,0) is the
* northwest and (2^z,2^z) is the southeast corner of the planet
- * @param id feature ID
* @return each tile this feature touches, and the points that appear on each
*/
- public static TiledGeometry slicePointsIntoTiles(TileExtents.ForZoom extents, double buffer, int z,
- Coordinate[] coords, long id) {
- TiledGeometry result = new TiledGeometry(extents, buffer, z, false, id);
+ static TiledGeometry slicePointsIntoTiles(TileExtents.ForZoom extents, double buffer, int z,
+ Coordinate[] coords) {
+ TiledGeometry result = new TiledGeometry(extents, buffer, z, false);
for (Coordinate coord : coords) {
result.slicePoint(coord);
}
@@ -107,35 +118,69 @@ class TiledGeometry {
return value;
}
- private void slicePoint(Coordinate coord) {
- double worldX = coord.getX() * maxTilesAtThisZoom;
- double worldY = coord.getY() * maxTilesAtThisZoom;
- int minX = (int) Math.floor(worldX - neighborBuffer);
- int maxX = (int) Math.floor(worldX + neighborBuffer);
- int minY = Math.max(extents.minY(), (int) Math.floor(worldY - neighborBuffer));
- int maxY = Math.min(extents.maxY() - 1, (int) Math.floor(worldY + neighborBuffer));
- for (int x = minX; x <= maxX; x++) {
- double tileX = worldX - x;
- int wrappedX = wrapInt(x, maxTilesAtThisZoom);
- // point may end up inside bounds after wrapping
- if (extents.testX(wrappedX)) {
- for (int y = minY; y <= maxY; y++) {
- TileCoord tile = TileCoord.ofXYZ(wrappedX, y, z);
- double tileY = worldY - y;
- tileContents.computeIfAbsent(tile, t -> List.of(new ArrayList<>()))
- .get(0)
- .add(GeoUtils.coordinateSequence(tileX * 256, tileY * 256));
- }
- }
+ /**
+ * Returns all the points that appear on tiles representing points at {@code coords}.
+ *
+ * @param scaledGeom the scaled geometry to slice into tiles, in world web mercator coordinates where (0,0) is the
+ * northwest and (1,1) is the southeast corner of the planet
+ * @param minSize the minimum length of a line or area of a polygon to emit
+ * @param buffer how far detail should be included beyond the edge of each tile (0=none, 1=a full tile width)
+ * @param z zoom level
+ * @param extents range of tile coordinates within the bounds of the map to generate
+ * @return each tile this feature touches, and the points that appear on each
+ */
+ public static TiledGeometry sliceIntoTiles(Geometry scaledGeom, double minSize, double buffer, int z,
+ TileExtents.ForZoom extents) {
+
+ if (scaledGeom.isEmpty()) {
+ // ignore
+ return new TiledGeometry(extents, buffer, z, false);
+ } else if (scaledGeom instanceof Point point) {
+ return slicePointsIntoTiles(extents, buffer, z, point.getCoordinates());
+ } else if (scaledGeom instanceof MultiPoint points) {
+ return slicePointsIntoTiles(extents, buffer, z, points.getCoordinates());
+ } else if (scaledGeom instanceof Polygon || scaledGeom instanceof MultiPolygon ||
+ scaledGeom instanceof LineString ||
+ scaledGeom instanceof MultiLineString) {
+ var coordinateSequences = GeometryCoordinateSequences.extractGroups(scaledGeom, minSize);
+ boolean area = scaledGeom instanceof Polygonal;
+ return sliceIntoTiles(coordinateSequences, buffer, area, z, extents);
+ } else {
+ throw new UnsupportedOperationException(
+ "Unsupported JTS geometry type " + scaledGeom.getClass().getSimpleName() + " " +
+ scaledGeom.getGeometryType());
}
}
- public int zoomLevel() {
- return z;
+ /**
+ * Returns the set of tiles that {@code scaledGeom} touches at a zoom level.
+ *
+ * @param scaledGeom The geometry in scaled web mercator coordinates where northwest is (0,0) and southeast is
+ * (2^z,2^z)
+ * @param zoom The zoom level
+ * @param extents The tile extents for this zoom level.
+ * @return A {@link CoveredTiles} instance for the tiles that are covered by this geometry.
+ */
+ public static CoveredTiles getCoveredTiles(Geometry scaledGeom, int zoom, TileExtents.ForZoom extents) {
+ if (scaledGeom.isEmpty()) {
+ return new CoveredTiles(new RoaringBitmap(), zoom);
+ } else if (scaledGeom instanceof Puntal || scaledGeom instanceof Polygonal || scaledGeom instanceof Lineal) {
+ return sliceIntoTiles(scaledGeom, 0, 0, zoom, extents).getCoveredTiles();
+ } else if (scaledGeom instanceof GeometryCollection gc) {
+ CoveredTiles result = new CoveredTiles(new RoaringBitmap(), zoom);
+ for (int i = 0; i < gc.getNumGeometries(); i++) {
+ result = CoveredTiles.merge(getCoveredTiles(gc.getGeometryN(i), zoom, extents), result);
+ }
+ return result;
+ } else {
+ throw new UnsupportedOperationException(
+ "Unsupported JTS geometry type " + scaledGeom.getClass().getSimpleName() + " " +
+ scaledGeom.getGeometryType());
+ }
}
/**
- * Returns all the points that appear on tiles representing points at {@code coords}.
+ * Returns the tiles that this geometry touches, and the contents of those tiles for this geometry.
*
* @param groups the list of linestrings or polygon rings extracted using {@link GeometryCoordinateSequences} in
* world web mercator coordinates where (0,0) is the northwest and (2^z,2^z) is the southeast corner of
@@ -144,12 +189,11 @@ class TiledGeometry {
* @param area {@code true} if this is a polygon {@code false} if this is a linestring
* @param z zoom level
* @param extents range of tile coordinates within the bounds of the map to generate
- * @param id feature ID
* @return each tile this feature touches, and the points that appear on each
*/
- public static TiledGeometry sliceIntoTiles(List> groups, double buffer, boolean area, int z,
- TileExtents.ForZoom extents, long id) {
- TiledGeometry result = new TiledGeometry(extents, buffer, z, area, id);
+ static TiledGeometry sliceIntoTiles(List> groups, double buffer, boolean area, int z,
+ TileExtents.ForZoom extents) {
+ TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
EnumSet wrapResult = result.sliceWorldCopy(groups, 0);
if (wrapResult.contains(Direction.RIGHT)) {
result.sliceWorldCopy(groups, -result.maxTilesAtThisZoom);
@@ -160,32 +204,6 @@ class TiledGeometry {
return result;
}
- /**
- * Returns an iterator over the coordinates of every tile that is completely filled within this polygon at this zoom
- * level, ordered by x ascending, y ascending.
- */
- public Iterable getFilledTiles() {
- return filledRanges == null ? Collections.emptyList() :
- () -> filledRanges.entrySet().stream()
- .mapMulti((entry, next) -> {
- int x = entry.getKey();
- for (int y : entry.getValue()) {
- TileCoord coord = TileCoord.ofXYZ(x, y, z);
- if (!tileContents.containsKey(coord)) {
- next.accept(coord);
- }
- }
- }).iterator();
- }
-
- /**
- * Returns every tile that this geometry touches, and the partial geometry contained on that tile that can be
- * reassembled using {@link GeometryCoordinateSequences}.
- */
- public Iterable>>> getTileData() {
- return tileContents.entrySet();
- }
-
private static int wrapX(int x, int max) {
x %= max;
if (x < 0) {
@@ -220,6 +238,79 @@ class TiledGeometry {
}, 2, 0);
}
+ private void slicePoint(Coordinate coord) {
+ double worldX = coord.getX();
+ double worldY = coord.getY();
+ int minX = (int) Math.floor(worldX - neighborBuffer);
+ int maxX = (int) Math.floor(worldX + neighborBuffer);
+ int minY = Math.max(extents.minY(), (int) Math.floor(worldY - neighborBuffer));
+ int maxY = Math.min(extents.maxY() - 1, (int) Math.floor(worldY + neighborBuffer));
+ for (int x = minX; x <= maxX; x++) {
+ double tileX = worldX - x;
+ int wrappedX = wrapInt(x, maxTilesAtThisZoom);
+ // point may end up inside bounds after wrapping
+ if (extents.testX(wrappedX)) {
+ for (int y = minY; y <= maxY; y++) {
+ if (extents.test(wrappedX, y)) {
+ TileCoord tile = TileCoord.ofXYZ(wrappedX, y, z);
+ double tileY = worldY - y;
+ tileContents.computeIfAbsent(tile, t -> List.of(new ArrayList<>()))
+ .get(0)
+ .add(GeoUtils.coordinateSequence(tileX * 256, tileY * 256));
+ }
+ }
+ }
+ }
+ }
+
+ public int zoomLevel() {
+ return z;
+ }
+
+ /**
+ * Returns an iterator over the coordinates of every tile that is completely filled within this polygon at this zoom
+ * level, ordered by x ascending, y ascending.
+ */
+ public Iterable getFilledTiles() {
+ return filledRanges == null ? Collections.emptyList() :
+ () -> filledRanges.entrySet().stream()
+ .mapMulti((entry, next) -> {
+ int x = entry.getKey();
+ for (int y : entry.getValue()) {
+ if (extents.test(x, y)) {
+ TileCoord coord = TileCoord.ofXYZ(x, y, z);
+ if (!tileContents.containsKey(coord)) {
+ next.accept(coord);
+ }
+ }
+ }
+ }).iterator();
+ }
+
+ /** Returns the tiles touched by this geometry. */
+ public CoveredTiles getCoveredTiles() {
+ RoaringBitmap bitmap = new RoaringBitmap();
+ for (TileCoord coord : tileContents.keySet()) {
+ bitmap.add(maxTilesAtThisZoom * coord.x() + coord.y());
+ }
+ if (filledRanges != null) {
+ for (var entry : filledRanges.entrySet()) {
+ long colStart = (long) entry.getKey() * maxTilesAtThisZoom;
+ var yRanges = entry.getValue();
+ bitmap.or(RoaringBitmap.addOffset(yRanges.bitmap(), colStart));
+ }
+ }
+ return new CoveredTiles(bitmap, z);
+ }
+
+ /**
+ * Returns every tile that this geometry touches, and the partial geometry contained on that tile that can be
+ * reassembled using {@link GeometryCoordinateSequences}.
+ */
+ public Map>> getTileData() {
+ return tileContents;
+ }
+
/**
* Slices a geometry into tiles and stores in member fields for a single "copy" of the world.
*
@@ -251,9 +342,6 @@ class TiledGeometry {
* | | | | | |
*/
IntObjectMap> xSlices = sliceX(segment);
- if (z >= 6 && xSlices.size() >= Math.pow(2, z) - 1) {
- LOGGER.warn("Feature " + featureId + " crosses world at z" + z + ": " + xSlices.size());
- }
for (IntObjectCursor> xCursor : xSlices) {
int x = xCursor.key + xOffset;
// skip processing content past the edge of the world, but return that we saw it
@@ -299,7 +387,7 @@ class TiledGeometry {
List outSeqs = inSeqs.stream()
.filter(seq -> seq.size() >= minPoints)
.toList();
- if (!outSeqs.isEmpty()) {
+ if (!outSeqs.isEmpty() && extents.test(tileID.x(), tileID.y())) {
tileContents.computeIfAbsent(tileID, tile -> new ArrayList<>()).add(outSeqs);
}
}
@@ -448,7 +536,6 @@ class TiledGeometry {
boolean onLeftEdge = area && ax == bx && ax == leftEdge && by < ay;
for (int y = startY; y <= endY; y++) {
-
// skip over filled tiles until we get to the next tile that already has detail on it
if (area && y > endStartY && y < startEndY) {
if (onRightEdge || onLeftEdge) {
@@ -609,4 +696,51 @@ class TiledGeometry {
RIGHT,
LEFT
}
+
+ /**
+ * A set of tiles touched by a geometry.
+ */
+ public static class CoveredTiles implements TilePredicate, Iterable {
+ private final RoaringBitmap bitmap;
+ private final int maxTilesAtZoom;
+ private final int z;
+
+ private CoveredTiles(RoaringBitmap bitmap, int z) {
+ this.bitmap = bitmap;
+ this.maxTilesAtZoom = 1 << z;
+ this.z = z;
+ }
+
+ /**
+ * Returns the union of tiles covered by {@code a} and {@code b}.
+ *
+ * @throws IllegalArgumentException if {@code a} and {@code b} have different zoom levels.
+ */
+ public static CoveredTiles merge(CoveredTiles a, CoveredTiles b) {
+ if (a.z != b.z) {
+ throw new IllegalArgumentException("Cannot combine CoveredTiles with different zoom levels ");
+ }
+ return new CoveredTiles(RoaringBitmap.or(a.bitmap, b.bitmap), a.z);
+ }
+
+ @Override
+ public boolean test(int x, int y) {
+ return bitmap.contains(x * maxTilesAtZoom + y);
+ }
+
+ @Override
+ public String toString() {
+ return "CoveredTiles{z=" + z + ", tiles=" + FORMAT.integer(bitmap.getCardinality()) + ", storage=" +
+ FORMAT.storage(bitmap.getSizeInBytes()) + "B}";
+ }
+
+ public Stream stream() {
+ return bitmap.stream().mapToObj(i -> TileCoord.ofXYZ(i / maxTilesAtZoom, i % maxTilesAtZoom, z));
+ }
+
+ @Override
+ public Iterator iterator() {
+ return stream().iterator();
+ }
+ }
}
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 43a4b40e..f781ba84 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java
@@ -1791,4 +1791,54 @@ class PlanetilerTests {
assertTrue(renderMaxzoomResult.tiles.containsKey(z8Tile));
assertFalse(maxzoomResult.tiles.containsKey(z8Tile));
}
+
+ private PlanetilerResults runForBoundsTest(int minzoom, int maxzoom, String key, String value) throws Exception {
+ return runWithReaderFeatures(
+ Map.of("threads", "1", key, value),
+ List.of(
+ newReaderFeature(WORLD_POLYGON, Map.of())
+ ),
+ (in, features) -> features.polygon("layer")
+ .setZoomRange(minzoom, maxzoom)
+ .setBufferPixels(0)
+ );
+ }
+
+ @Test
+ void testBoundFilters() throws Exception {
+ var origResult = runForBoundsTest(0, 2, "", "");
+ var bboxResult = runForBoundsTest(0, 2, "bounds", "1,-85.05113,180,-1");
+ var polyResult = runForBoundsTest(0, 2, "polygon", TestUtils.pathToResource("bottomrightearth.poly").toString());
+
+ assertEquals(1 + 4 + 16, origResult.tiles.size());
+ assertEquals(Set.of(
+ TileCoord.ofXYZ(0, 0, 0),
+ TileCoord.ofXYZ(1, 1, 1),
+ TileCoord.ofXYZ(2, 2, 2),
+ TileCoord.ofXYZ(3, 2, 2),
+ TileCoord.ofXYZ(2, 3, 2),
+ TileCoord.ofXYZ(3, 3, 2)
+ ), bboxResult.tiles.keySet());
+ assertEquals(Set.of(
+ TileCoord.ofXYZ(0, 0, 0),
+ TileCoord.ofXYZ(1, 1, 1),
+ // TileCoord.ofXYZ(2, 2, 2), - omit since this one is outside of triangle
+ TileCoord.ofXYZ(3, 2, 2),
+ TileCoord.ofXYZ(2, 3, 2),
+ TileCoord.ofXYZ(3, 3, 2)
+ ), polyResult.tiles.keySet());
+
+ // but besides the omitted tile, the rest should be the same
+ bboxResult.tiles.remove(TileCoord.ofXYZ(2, 2, 2));
+ assertEquals(bboxResult.tiles, polyResult.tiles);
+ }
+
+ @Test
+ void testBoundFiltersFill() throws Exception {
+ var polyResultz8 = runForBoundsTest(8, 8, "polygon", TestUtils.pathToResource("bottomrightearth.poly").toString());
+
+ int z8tiles = 1 << 8;
+ assertFalse(polyResultz8.tiles.containsKey(TileCoord.ofXYZ(z8tiles * 3 / 4, z8tiles * 5 / 8, 8)));
+ assertTrue(polyResultz8.tiles.containsKey(TileCoord.ofXYZ(z8tiles * 3 / 4, z8tiles * 7 / 8, 8)));
+ }
}
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/TileExtentsTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/TileExtentsTest.java
index 389a9bf7..96117f06 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/TileExtentsTest.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/TileExtentsTest.java
@@ -1,7 +1,10 @@
package com.onthegomap.planetiler.geo;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.onthegomap.planetiler.TestUtils;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Envelope;
@@ -56,4 +59,29 @@ class TileExtentsTest {
assertEquals(1 << z, extents.getForZoom(z).maxY(), "z" + z + " maxY");
}
}
+
+ @Test
+ void testShape() {
+ var size = Math.pow(2, -14);
+ var shape = GeoUtils.worldToLatLonCoords(TestUtils.newPolygon(
+ 0.5, 0.5 - size * 5,
+ 0.5 + size * 5, 0.5,
+ 0.5, 0.5 + size * 5,
+ 0.5 - size * 5, 0.5,
+ 0.5, 0.5 - size * 5
+ ));
+ TileExtents extents = TileExtents
+ .computeFromWorldBounds(14, new Envelope(0.5 - size * 4, 0.5 + size * 4, 0.5 - size * 4, 0.5 + size * 4),
+ shape);
+ for (int z = 0; z <= 14; z++) {
+ int middle = (1 << z) / 2;
+ assertTrue(extents.test(middle, middle, z), "z" + z);
+ }
+ // inside shape and bounds
+ assertTrue(extents.test((1 << 13) + 3, (1 << 13), 14));
+ // inside shape but outside bounds
+ assertFalse(extents.test((1 << 13) + 4, (1 << 13), 14));
+ // inside bounds, outside shape
+ assertFalse(extents.test((1 << 13) + 3, (1 << 13) + 3, 14));
+ }
}
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/osm/PolyFileReaderTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/osm/PolyFileReaderTest.java
new file mode 100644
index 00000000..454832b0
--- /dev/null
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/osm/PolyFileReaderTest.java
@@ -0,0 +1,169 @@
+package com.onthegomap.planetiler.reader.osm;
+
+import static com.onthegomap.planetiler.reader.osm.PolyFileReader.parsePolyFile;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.onthegomap.planetiler.reader.FileFormatException;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+
+class PolyFileReaderTest {
+
+ @Test
+ void testParseAustralia() throws Exception {
+ var poly = parsePolyFile("""
+ australia_v
+ first_area
+ 0.1446693E+03 -0.3826255E+02
+ 0.1446627E+03 -0.3825661E+02
+ 0.1446763E+03 -0.3824465E+02
+ 0.1446813E+03 -0.3824343E+02
+ 0.1446824E+03 -0.3824484E+02
+ 0.1446826E+03 -0.3825356E+02
+ 0.1446876E+03 -0.3825210E+02
+ 0.1446919E+03 -0.3824719E+02
+ 0.1447006E+03 -0.3824723E+02
+ 0.1447042E+03 -0.3825078E+02
+ 0.1446758E+03 -0.3826229E+02
+ 0.1446693E+03 -0.3826255E+02
+ END
+ second_area
+ 0.1422436E+03 -0.3839315E+02
+ 0.1422496E+03 -0.3839070E+02
+ 0.1422543E+03 -0.3839025E+02
+ 0.1422574E+03 -0.3839155E+02
+ 0.1422467E+03 -0.3840065E+02
+ 0.1422433E+03 -0.3840048E+02
+ 0.1422420E+03 -0.3839857E+02
+ 0.1422436E+03 -0.3839315E+02
+ END
+ END
+ """);
+ assertEquals(2, poly.getNumGeometries());
+ assertEquals(4.60252e-4, poly.getArea(), 1e-10);
+ }
+
+ @Test
+ void testParseAustraliaOceana() throws IOException {
+ var poly = parsePolyFile("""
+ australia-oceania
+ 1
+ -107.863281 11.780702
+ -104.171875 -28.082042
+ -179.999999 -45.652740
+ -179.999999 4.082818
+ -107.863281 11.780702
+ END
+ 0
+ 89.512500 -11.143360
+ 61.663780 -9.177713
+ 44.655470 -57.087780
+ 180.000000 -57.164820
+ 180.000000 26.277810
+ 141.547997 22.628320
+ 130.145100 3.640314
+ 129.953200 -0.535293
+ 131.061600 -3.784815
+ 130.266900 -10.043780
+ 118.255700 -13.011650
+ 102.800900 -8.390453
+ 89.512500 -11.143360
+ END
+ END
+ """);
+ assertEquals(2, poly.getNumGeometries());
+ assertEquals(10876.51613, poly.getArea(), 1e-4);
+ }
+
+ @Test
+ void testParseInvalid() throws IOException {
+ assertThrows(FileFormatException.class, () -> parsePolyFile("""
+ name
+ section
+ 1 2
+ 3 4
+ 5 6
+ 7 8
+ """));
+ assertThrows(FileFormatException.class, () -> parsePolyFile("""
+ name
+ section
+ 1 2
+ 3 4
+ 5 6
+ 7 8
+ END
+ """));
+ parsePolyFile("""
+ name
+ section
+ 1 2
+ 3 4
+ 5 6
+ 7 8
+ END
+ END
+ """);
+ parsePolyFile("""
+
+
+ name
+
+ section
+
+ 1 2
+ 3 4
+ 5 6
+ 7 8
+
+ END
+
+ END
+
+
+ """);
+ assertThrows(FileFormatException.class, () -> parsePolyFile("""
+ name
+ section
+ 1 2
+ 3 4
+ 5 6
+ 7 8
+ END
+ END
+ name
+ section
+ 1 2
+ 3 4
+ 5 6
+ 7 8
+ END
+ END
+ """));
+ }
+
+ @Test
+ void testParseHole() throws IOException {
+ var poly = parsePolyFile("""
+ poly
+ outer
+ 0 0
+ 0 10
+ 10 10
+ 10 0
+ 0 0
+ END
+ !inner
+ 1 1
+ 1 9
+ 9 9
+ 9 1
+ 1 1
+ END
+ END
+ """);
+ assertEquals(1, poly.getNumGeometries());
+ assertEquals(10 * 10 - 8 * 8, poly.getArea(), 1e-4);
+ }
+}
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java
index d40639fe..7833404d 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/render/FeatureRendererTest.java
@@ -137,6 +137,20 @@ class FeatureRendererTest {
), renderGeometry(feature));
}
+ @Test
+ void testEmitPointsRespectShape() {
+ config = PlanetilerConfig.from(Arguments.of(
+ "polygon", TestUtils.pathToResource("bottomrightearth.poly")
+ ));
+ var feature = pointFeature(newPoint(0.5 + 1d / 512, 0.5 + 1d / 512))
+ .setZoomRange(0, 2)
+ .setBufferPixels(2);
+ assertSameNormalizedFeatures(Map.of(
+ TileCoord.ofXYZ(0, 0, 0), List.of(newPoint(128.5, 128.5)),
+ TileCoord.ofXYZ(1, 1, 1), List.of(newPoint(1, 1))
+ ), renderGeometry(feature));
+ }
+
@TestFactory
List testProcessPointsNearInternationalDateLineAndPoles() {
double d = 1d / 512;
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/render/TiledGeometryTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/render/TiledGeometryTest.java
new file mode 100644
index 00000000..be026f22
--- /dev/null
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/render/TiledGeometryTest.java
@@ -0,0 +1,149 @@
+package com.onthegomap.planetiler.render;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.onthegomap.planetiler.TestUtils;
+import com.onthegomap.planetiler.geo.TileCoord;
+import com.onthegomap.planetiler.geo.TileExtents;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.Test;
+
+class TiledGeometryTest {
+ private static final int Z14_TILES = 1 << 14;
+
+ @Test
+ void testPoint() {
+ var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newPoint(0.5, 0.5), 14,
+ new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertTrue(tiledGeom.test(0, 0));
+ assertFalse(tiledGeom.test(0, 1));
+ assertFalse(tiledGeom.test(1, 0));
+ assertFalse(tiledGeom.test(1, 1));
+ assertEquals(Set.of(TileCoord.ofXYZ(0, 0, 14)), tiledGeom.stream().collect(Collectors.toSet()));
+
+ tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newPoint(Z14_TILES - 0.5, Z14_TILES - 0.5), 14,
+ new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertTrue(tiledGeom.test(Z14_TILES - 1, Z14_TILES - 1));
+ assertFalse(tiledGeom.test(Z14_TILES - 2, Z14_TILES - 1));
+ assertFalse(tiledGeom.test(Z14_TILES - 1, Z14_TILES - 2));
+ assertFalse(tiledGeom.test(Z14_TILES - 2, Z14_TILES - 2));
+ assertEquals(Set.of(TileCoord.ofXYZ(Z14_TILES - 1, Z14_TILES - 1, 14)),
+ tiledGeom.stream().collect(Collectors.toSet()));
+ }
+
+ @Test
+ void testMultiPoint() {
+ var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newMultiPoint(
+ TestUtils.newPoint(0.5, 0.5),
+ TestUtils.newPoint(2.5, 1.5)
+ ), 14,
+ new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertEquals(Set.of(
+ TileCoord.ofXYZ(0, 0, 14),
+ TileCoord.ofXYZ(2, 1, 14)
+ ), tiledGeom.stream().collect(Collectors.toSet()));
+ }
+
+ @Test
+ void testLine() {
+ var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newLineString(
+ 0.5, 0.5,
+ 1.5, 0.5
+ ), 14, new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertEquals(Set.of(
+ TileCoord.ofXYZ(0, 0, 14),
+ TileCoord.ofXYZ(1, 0, 14)
+ ), tiledGeom.stream().collect(Collectors.toSet()));
+ }
+
+ @Test
+ void testMultiLine() {
+ var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newMultiLineString(
+ TestUtils.newLineString(
+ 0.5, 0.5,
+ 1.5, 0.5
+ ),
+ TestUtils.newLineString(
+ 3.5, 1.5,
+ 4.5, 1.5
+ )
+ ), 14, new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertEquals(Set.of(
+ TileCoord.ofXYZ(0, 0, 14),
+ TileCoord.ofXYZ(1, 0, 14),
+ TileCoord.ofXYZ(3, 1, 14),
+ TileCoord.ofXYZ(4, 1, 14)
+ ), tiledGeom.stream().collect(Collectors.toSet()));
+ }
+
+ @Test
+ void testPolygon() {
+ var tiledGeom =
+ TiledGeometry.getCoveredTiles(TestUtils.newPolygon(
+ TestUtils.rectangleCoordList(25.5, 27.5),
+ List.of(TestUtils.rectangleCoordList(25.9, 27.1))
+ ), 14, new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertEquals(Set.of(
+ TileCoord.ofXYZ(25, 25, 14),
+ TileCoord.ofXYZ(26, 25, 14),
+ TileCoord.ofXYZ(27, 25, 14),
+
+ TileCoord.ofXYZ(25, 26, 14),
+ // TileCoord.ofXYZ(26, 26, 14), skipped because of hole!
+ TileCoord.ofXYZ(27, 26, 14),
+
+ TileCoord.ofXYZ(25, 27, 14),
+ TileCoord.ofXYZ(26, 27, 14),
+ TileCoord.ofXYZ(27, 27, 14)
+ ), tiledGeom.stream().collect(Collectors.toSet()));
+ }
+
+ @Test
+ void testMultiPolygon() {
+ var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newMultiPolygon(
+ TestUtils.rectangle(25.5, 26.5),
+ TestUtils.rectangle(30.1, 30.9)
+ ), 14,
+ new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertEquals(Set.of(
+ TileCoord.ofXYZ(25, 25, 14),
+ TileCoord.ofXYZ(25, 26, 14),
+ TileCoord.ofXYZ(26, 25, 14),
+ TileCoord.ofXYZ(26, 26, 14),
+
+ TileCoord.ofXYZ(30, 30, 14)
+ ), tiledGeom.stream().collect(Collectors.toSet()));
+ }
+
+ @Test
+ void testEmpty() {
+ var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newGeometryCollection(), 14,
+ new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertEquals(Set.of(), tiledGeom.stream().collect(Collectors.toSet()));
+ }
+
+ @Test
+ void testGeometryCollection() {
+ var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newGeometryCollection(
+ TestUtils.rectangle(0.1, 0.9),
+ TestUtils.newPoint(1.5, 1.5),
+ TestUtils.newGeometryCollection(TestUtils.newLineString(3.5, 10.5, 4.5, 10.5))
+ ), 14,
+ new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
+ assertEquals(Set.of(
+ // rectangle
+ TileCoord.ofXYZ(0, 0, 14),
+
+ // point
+ TileCoord.ofXYZ(1, 1, 14),
+
+ // linestring
+ TileCoord.ofXYZ(3, 10, 14),
+ TileCoord.ofXYZ(4, 10, 14)
+ ), tiledGeom.stream().collect(Collectors.toSet()));
+ }
+}
diff --git a/planetiler-core/src/test/resources/bottomrightearth.poly b/planetiler-core/src/test/resources/bottomrightearth.poly
new file mode 100644
index 00000000..d35b3381
--- /dev/null
+++ b/planetiler-core/src/test/resources/bottomrightearth.poly
@@ -0,0 +1,8 @@
+bottom-right
+1
+ 180 -1
+ 180 -85
+ 1 -85
+ 180 -1
+END
+END