feat: `--polygon` argument to constrain mbtiles to a poly shape (#280)

pull/304/head
farfromrefuge 2022-07-22 12:48:04 +02:00 zatwierdzone przez GitHub
rodzic d1d68cf753
commit 7818634774
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
18 zmienionych plików z 861 dodań i 134 usunięć

Wyświetl plik

@ -23,6 +23,7 @@ The `planetiler-core` module includes the following software:
- org.openstreetmap.osmosis:osmosis-osm-binary (LGPL 3.0)
- com.carrotsearch:hppc (Apache license)
- com.github.jnr:jnr-ffi (Apache license)
- org.roaringbitmap:RoaringBitmap (Apache license)
- Adapted code:
- `DouglasPeuckerSimplifier` from [JTS](https://github.com/locationtech/jts) (EDL)
- `OsmMultipolygon` from [imposm3](https://github.com/omniscale/imposm3) (Apache license)

Wyświetl plik

@ -27,6 +27,11 @@
<artifactId>hppc</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>0.9.30</version>
</dependency>
<dependency>
<groupId>org.openstreetmap.osmosis</groupId>
<artifactId>osmosis-osm-binary</artifactId>

Wyświetl plik

@ -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.
* <p>
* This makes iterating through tile coordinates inside ocean polygons significantly faster.
*/
@SuppressWarnings("UnstableApiUsage")
@NotThreadSafe
public class IntRangeSet implements Iterable<Integer> {
private final TreeRangeSet<Integer> 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<Range<Integer>> rangeIter;
Range<Integer> range;
int cur;
boolean hasNext = true;
private Iter(Iterator<Range<Integer>> 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();
}
}
}

Wyświetl plik

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

Wyświetl plik

@ -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,

Wyświetl plik

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

Wyświetl plik

@ -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();

Wyświetl plik

@ -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<TileCoord> {
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<TileCoord> {
/** 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<TileCoord> {
/**
* 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) {

Wyświetl plik

@ -0,0 +1,5 @@
package com.onthegomap.planetiler.geo;
public interface TilePredicate {
boolean test(int x, int y);
}

Wyświetl plik

@ -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 <a href="https://wiki.openstreetmap.org/wiki/Osmosis/Polygon_Filter_File_Format">Osmosis/Polygon Filter File
* Format</a>
*/
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;
}
}
}

Wyświetl plik

@ -94,13 +94,24 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, 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<String, Object> 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<FeatureCollector.Feature>, 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<List<CoordinateSequence>> result = entry.getValue();
Geometry geom = GeometryCoordinateSequences.reassemblePoints(result);
@ -186,7 +197,7 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
List<List<CoordinateSequence>> 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<String, Object> 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<FeatureCollector.Feature>, Clos
private void writeTileFeatures(int zoom, long id, FeatureCollector.Feature feature, TiledGeometry sliced,
Map<String, Object> attrs) {
int emitted = 0;
for (var entry : sliced.getTileData()) {
for (var entry : sliced.getTileData().entrySet()) {
TileCoord tile = entry.getKey();
try {
List<List<CoordinateSequence>> geoms = entry.getValue();

Wyświetl plik

@ -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.
* <p>
* 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
* <a href="https://github.com/mapbox/geojson-vt/">geojson-vt</a> 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<TileCoord, List<List<CoordinateSequence>>> tileContents = new HashMap<>();
/** Map from X coordinate to range of Y coordinates that contain filled tiles inside this geometry */
private Map<Integer, IntRangeSet> 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<Integer, IntRangeSet> 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<List<CoordinateSequence>> 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<List<CoordinateSequence>> groups, double buffer, boolean area, int z,
TileExtents.ForZoom extents) {
TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
EnumSet<Direction> 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<TileCoord> getFilledTiles() {
return filledRanges == null ? Collections.emptyList() :
() -> filledRanges.entrySet().stream()
.<TileCoord>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<Map.Entry<TileCoord, List<List<CoordinateSequence>>>> 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<TileCoord> getFilledTiles() {
return filledRanges == null ? Collections.emptyList() :
() -> filledRanges.entrySet().stream()
.<TileCoord>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<TileCoord, List<List<CoordinateSequence>>> getTileData() {
return tileContents;
}
/**
* Slices a geometry into tiles and stores in member fields for a single "copy" of the world.
* <p>
@ -251,9 +342,6 @@ class TiledGeometry {
* | | | | | |
*/
IntObjectMap<List<MutableCoordinateSequence>> 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<List<MutableCoordinateSequence>> 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<CoordinateSequence> 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<TileCoord> {
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<TileCoord> stream() {
return bitmap.stream().mapToObj(i -> TileCoord.ofXYZ(i / maxTilesAtZoom, i % maxTilesAtZoom, z));
}
@Override
public Iterator<TileCoord> iterator() {
return stream().iterator();
}
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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<DynamicTest> testProcessPointsNearInternationalDateLineAndPoles() {
double d = 1d / 512;

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,8 @@
bottom-right
1
180 -1
180 -85
1 -85
180 -1
END
END