2021-05-18 10:53:12 +00:00
|
|
|
/*
|
2021-08-02 00:26:22 +00:00
|
|
|
ISC License
|
|
|
|
|
|
|
|
Copyright (c) 2015, Mapbox
|
|
|
|
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
|
|
with or without fee is hereby granted, provided that the above copyright notice
|
|
|
|
and this permission notice appear in all copies.
|
|
|
|
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
|
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
|
|
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
|
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
|
|
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
|
|
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
|
|
THIS SOFTWARE.
|
2021-05-18 10:53:12 +00:00
|
|
|
*/
|
2021-12-23 10:42:24 +00:00
|
|
|
package com.onthegomap.planetiler.render;
|
2021-05-18 10:53:12 +00:00
|
|
|
|
|
|
|
import com.carrotsearch.hppc.IntObjectMap;
|
|
|
|
import com.carrotsearch.hppc.cursors.IntCursor;
|
|
|
|
import com.carrotsearch.hppc.cursors.IntObjectCursor;
|
2022-03-01 13:43:19 +00:00
|
|
|
import com.onthegomap.planetiler.collection.Hppc;
|
2021-12-23 10:42:24 +00:00
|
|
|
import com.onthegomap.planetiler.collection.IntRangeSet;
|
|
|
|
import com.onthegomap.planetiler.geo.GeoUtils;
|
|
|
|
import com.onthegomap.planetiler.geo.MutableCoordinateSequence;
|
|
|
|
import com.onthegomap.planetiler.geo.TileCoord;
|
|
|
|
import com.onthegomap.planetiler.geo.TileExtents;
|
2022-07-22 10:48:04 +00:00
|
|
|
import com.onthegomap.planetiler.geo.TilePredicate;
|
|
|
|
import com.onthegomap.planetiler.util.Format;
|
2021-05-18 10:53:12 +00:00
|
|
|
import java.util.ArrayList;
|
2021-05-22 11:07:34 +00:00
|
|
|
import java.util.Collections;
|
2021-05-18 10:53:12 +00:00
|
|
|
import java.util.EnumSet;
|
|
|
|
import java.util.HashMap;
|
2022-07-22 10:48:04 +00:00
|
|
|
import java.util.Iterator;
|
2021-05-18 10:53:12 +00:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.Map;
|
|
|
|
import java.util.TreeSet;
|
2022-07-22 10:48:04 +00:00
|
|
|
import java.util.stream.Stream;
|
2021-09-10 00:46:20 +00:00
|
|
|
import javax.annotation.concurrent.NotThreadSafe;
|
2021-05-22 11:07:34 +00:00
|
|
|
import org.locationtech.jts.geom.Coordinate;
|
2021-05-18 10:53:12 +00:00
|
|
|
import org.locationtech.jts.geom.CoordinateSequence;
|
2021-09-10 00:46:20 +00:00
|
|
|
import org.locationtech.jts.geom.Geometry;
|
2022-07-22 10:48:04 +00:00
|
|
|
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;
|
2021-05-18 10:53:12 +00:00
|
|
|
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
2022-07-22 10:48:04 +00:00
|
|
|
import org.roaringbitmap.RoaringBitmap;
|
2021-05-18 10:53:12 +00:00
|
|
|
|
|
|
|
/**
|
2021-09-10 00:46:20 +00:00
|
|
|
* Splits geometries represented by lists of {@link CoordinateSequence CoordinateSequences} into the geometries that
|
|
|
|
* appear on individual tiles that the geometry touches.
|
|
|
|
* <p>
|
|
|
|
* {@link GeometryCoordinateSequences} converts between JTS {@link Geometry} instances and {@link CoordinateSequence}
|
|
|
|
* lists for this utility.
|
|
|
|
* <p>
|
2022-07-22 10:48:04 +00:00
|
|
|
* 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.
|
2021-05-18 10:53:12 +00:00
|
|
|
*/
|
2021-09-10 00:46:20 +00:00
|
|
|
@NotThreadSafe
|
2022-07-22 10:48:04 +00:00
|
|
|
public class TiledGeometry {
|
2021-05-18 10:53:12 +00:00
|
|
|
|
2022-07-22 10:48:04 +00:00
|
|
|
private static final Format FORMAT = Format.defaultInstance();
|
2021-05-22 11:07:34 +00:00
|
|
|
private static final double NEIGHBOR_BUFFER_EPS = 0.1d / 4096;
|
2021-05-18 10:53:12 +00:00
|
|
|
|
|
|
|
private final Map<TileCoord, List<List<CoordinateSequence>>> tileContents = new HashMap<>();
|
|
|
|
private final TileExtents.ForZoom extents;
|
|
|
|
private final double buffer;
|
2021-05-22 11:07:34 +00:00
|
|
|
private final double neighborBuffer;
|
2021-05-18 10:53:12 +00:00
|
|
|
private final int z;
|
|
|
|
private final boolean area;
|
2021-09-10 00:46:20 +00:00
|
|
|
private final int maxTilesAtThisZoom;
|
2022-07-22 10:48:04 +00:00
|
|
|
/** Map from X coordinate to range of Y coordinates that contain filled tiles inside this geometry */
|
|
|
|
private Map<Integer, IntRangeSet> filledRanges = null;
|
2021-05-18 10:53:12 +00:00
|
|
|
|
2022-07-22 10:48:04 +00:00
|
|
|
private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area) {
|
2021-05-18 10:53:12 +00:00
|
|
|
this.extents = extents;
|
|
|
|
this.buffer = buffer;
|
2021-05-22 11:07:34 +00:00
|
|
|
// make sure we inspect neighboring tiles when a line runs along an edge
|
|
|
|
this.neighborBuffer = buffer + NEIGHBOR_BUFFER_EPS;
|
2021-05-18 10:53:12 +00:00
|
|
|
this.z = z;
|
|
|
|
this.area = area;
|
2021-09-10 00:46:20 +00:00
|
|
|
this.maxTilesAtThisZoom = 1 << z;
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* Returns all the points that appear on tiles representing points at {@code coords}.
|
|
|
|
*
|
|
|
|
* @param extents range of tile coordinates within the bounds of the map to generate
|
|
|
|
* @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 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
|
|
|
|
* @return each tile this feature touches, and the points that appear on each
|
|
|
|
*/
|
2022-07-22 10:48:04 +00:00
|
|
|
static TiledGeometry slicePointsIntoTiles(TileExtents.ForZoom extents, double buffer, int z,
|
|
|
|
Coordinate[] coords) {
|
|
|
|
TiledGeometry result = new TiledGeometry(extents, buffer, z, false);
|
2021-05-22 11:07:34 +00:00
|
|
|
for (Coordinate coord : coords) {
|
|
|
|
result.slicePoint(coord);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int wrapInt(int value, int max) {
|
|
|
|
value %= max;
|
|
|
|
if (value < 0) {
|
|
|
|
value += max;
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2022-07-22 10:48:04 +00:00
|
|
|
/**
|
|
|
|
* 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());
|
2021-05-22 11:07:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-22 10:48:04 +00:00
|
|
|
/**
|
|
|
|
* 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());
|
|
|
|
}
|
2021-05-20 09:59:18 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
2022-07-22 10:48:04 +00:00
|
|
|
* Returns the tiles that this geometry touches, and the contents of those tiles for this geometry.
|
2021-09-10 00:46:20 +00:00
|
|
|
*
|
|
|
|
* @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
|
|
|
|
* the planet
|
|
|
|
* @param buffer how far detail should be included beyond the edge of each tile (0=none, 1=a full tile width)
|
|
|
|
* @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
|
|
|
|
* @return each tile this feature touches, and the points that appear on each
|
|
|
|
*/
|
2022-07-22 10:48:04 +00:00
|
|
|
static TiledGeometry sliceIntoTiles(List<List<CoordinateSequence>> groups, double buffer, boolean area, int z,
|
|
|
|
TileExtents.ForZoom extents) {
|
|
|
|
TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
|
2021-05-18 10:53:12 +00:00
|
|
|
EnumSet<Direction> wrapResult = result.sliceWorldCopy(groups, 0);
|
|
|
|
if (wrapResult.contains(Direction.RIGHT)) {
|
2021-09-10 00:46:20 +00:00
|
|
|
result.sliceWorldCopy(groups, -result.maxTilesAtThisZoom);
|
2021-05-19 10:44:28 +00:00
|
|
|
}
|
|
|
|
if (wrapResult.contains(Direction.LEFT)) {
|
2021-09-10 00:46:20 +00:00
|
|
|
result.sliceWorldCopy(groups, result.maxTilesAtThisZoom);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int wrapX(int x, int max) {
|
|
|
|
x %= max;
|
|
|
|
if (x < 0) {
|
|
|
|
x += max;
|
|
|
|
}
|
|
|
|
return x;
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Adds a new point to {@code out} where the line segment from (ax,ay) to (bx,by) crosses a vertical line at x=x. */
|
2021-05-18 10:53:12 +00:00
|
|
|
private static void intersectX(MutableCoordinateSequence out, double ax, double ay, double bx, double by, double x) {
|
|
|
|
double t = (x - ax) / (bx - ax);
|
|
|
|
out.addPoint(x, ay + (by - ay) * t);
|
|
|
|
}
|
|
|
|
|
2022-03-09 02:08:03 +00:00
|
|
|
/**
|
|
|
|
* Adds a new point to {@code out} where the line segment from (ax,ay) to (bx,by) crosses a horizontal line at y=y.
|
|
|
|
*/
|
2021-05-18 10:53:12 +00:00
|
|
|
private static void intersectY(MutableCoordinateSequence out, double ax, double ay, double bx, double by, double y) {
|
|
|
|
double t = (y - ay) / (by - ay);
|
|
|
|
out.addPoint(ax + (bx - ax) * t, y);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static CoordinateSequence fill(double buffer) {
|
|
|
|
double min = -256d * buffer;
|
|
|
|
double max = 256d - min;
|
|
|
|
return new PackedCoordinateSequence.Double(new double[]{
|
|
|
|
min, min,
|
|
|
|
max, min,
|
|
|
|
max, max,
|
|
|
|
min, max,
|
|
|
|
min, min
|
|
|
|
}, 2, 0);
|
|
|
|
}
|
|
|
|
|
2022-07-22 10:48:04 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* Slices a geometry into tiles and stores in member fields for a single "copy" of the world.
|
|
|
|
* <p>
|
2022-03-09 02:08:03 +00:00
|
|
|
* Instead of handling content outside -180 to 180 degrees longitude, return {@link Direction#LEFT} or
|
|
|
|
* {@link Direction#RIGHT} to indicate whether this method should be called again with a different {@code xOffset} to
|
|
|
|
* process wrapped content.
|
2021-09-10 00:46:20 +00:00
|
|
|
*
|
|
|
|
* @param groups the geometry
|
|
|
|
* @param xOffset offset to apply to each X coordinate (-2^z handles content that wraps too far east and 2^z handles
|
|
|
|
* content that wraps too far west)
|
|
|
|
* @return {@link Direction#LEFT} if there is more content to the west and {@link Direction#RIGHT} if there is more
|
2022-03-09 02:08:03 +00:00
|
|
|
* content to the east.
|
2021-09-10 00:46:20 +00:00
|
|
|
*/
|
2021-05-18 10:53:12 +00:00
|
|
|
private EnumSet<Direction> sliceWorldCopy(List<List<CoordinateSequence>> groups, int xOffset) {
|
|
|
|
EnumSet<Direction> overflow = EnumSet.noneOf(Direction.class);
|
|
|
|
for (List<CoordinateSequence> group : groups) {
|
|
|
|
Map<TileCoord, List<CoordinateSequence>> inProgressShapes = new HashMap<>();
|
|
|
|
for (int i = 0; i < group.size(); i++) {
|
|
|
|
CoordinateSequence segment = group.get(i);
|
2021-09-10 00:46:20 +00:00
|
|
|
boolean isOuterRing = i == 0;
|
|
|
|
/*
|
|
|
|
* Step 1 in the striped clipping algorithm: slice the geometry into vertical slices representing each "x" tile
|
|
|
|
* coordinate:
|
|
|
|
* x=0 1 2 3 4 ...
|
|
|
|
* | | | | | |
|
|
|
|
* |-|-| | | |
|
|
|
|
* | | |\| | |
|
|
|
|
* | | | |-|-|
|
|
|
|
* | | | | | |
|
|
|
|
*/
|
2021-05-19 10:44:28 +00:00
|
|
|
IntObjectMap<List<MutableCoordinateSequence>> xSlices = sliceX(segment);
|
2021-05-18 10:53:12 +00:00
|
|
|
for (IntObjectCursor<List<MutableCoordinateSequence>> xCursor : xSlices) {
|
|
|
|
int x = xCursor.key + xOffset;
|
2021-09-10 00:46:20 +00:00
|
|
|
// skip processing content past the edge of the world, but return that we saw it
|
|
|
|
if (x >= maxTilesAtThisZoom) {
|
2021-05-18 10:53:12 +00:00
|
|
|
overflow.add(Direction.RIGHT);
|
|
|
|
} else if (x < 0) {
|
|
|
|
overflow.add(Direction.LEFT);
|
|
|
|
} else {
|
2021-09-10 00:46:20 +00:00
|
|
|
/*
|
|
|
|
* Step 2 in the striped clipping algorithm: split each vertical column x slice into horizontal slices
|
|
|
|
* representing the row for each Y coordinate.
|
|
|
|
*/
|
2021-05-18 10:53:12 +00:00
|
|
|
for (CoordinateSequence stripeSegment : xCursor.value) {
|
2021-09-10 00:46:20 +00:00
|
|
|
// sliceY only stores content for rings of a polygon, need to store the
|
|
|
|
// filled tiles that it spanned separately
|
|
|
|
IntRangeSet filledYRange = sliceY(stripeSegment, x, isOuterRing, inProgressShapes);
|
2021-05-20 09:59:18 +00:00
|
|
|
if (area && filledYRange != null) {
|
2021-09-10 00:46:20 +00:00
|
|
|
if (isOuterRing) {
|
2021-05-20 09:59:18 +00:00
|
|
|
addFilledRange(x, filledYRange);
|
2021-05-18 10:53:12 +00:00
|
|
|
} else {
|
2021-05-20 09:59:18 +00:00
|
|
|
removeFilledRange(x, filledYRange);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
addShapeToResults(inProgressShapes);
|
|
|
|
}
|
|
|
|
|
|
|
|
return overflow;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void addShapeToResults(Map<TileCoord, List<CoordinateSequence>> inProgressShapes) {
|
|
|
|
for (var entry : inProgressShapes.entrySet()) {
|
|
|
|
TileCoord tileID = entry.getKey();
|
|
|
|
List<CoordinateSequence> inSeqs = entry.getValue();
|
|
|
|
if (area && inSeqs.get(0).size() < 4) {
|
|
|
|
// not enough points in outer polygon, ignore
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
int minPoints = area ? 4 : 2;
|
|
|
|
List<CoordinateSequence> outSeqs = inSeqs.stream()
|
|
|
|
.filter(seq -> seq.size() >= minPoints)
|
|
|
|
.toList();
|
2022-07-22 10:48:04 +00:00
|
|
|
if (!outSeqs.isEmpty() && extents.test(tileID.x(), tileID.y())) {
|
2021-05-18 10:53:12 +00:00
|
|
|
tileContents.computeIfAbsent(tileID, tile -> new ArrayList<>()).add(outSeqs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* Returns a map from X coordinate to segments of this geometry that cross the vertical column formed by all tiles
|
|
|
|
* where {@code x=x}.
|
|
|
|
*/
|
2021-05-19 10:44:28 +00:00
|
|
|
private IntObjectMap<List<MutableCoordinateSequence>> sliceX(CoordinateSequence segment) {
|
2021-09-10 00:46:20 +00:00
|
|
|
double leftLimit = -buffer;
|
|
|
|
double rightLimit = 1 + buffer;
|
2022-03-01 13:43:19 +00:00
|
|
|
IntObjectMap<List<MutableCoordinateSequence>> newGeoms = Hppc.newIntObjectHashMap();
|
|
|
|
IntObjectMap<MutableCoordinateSequence> xSlices = Hppc.newIntObjectHashMap();
|
2021-05-18 10:53:12 +00:00
|
|
|
int end = segment.size() - 1;
|
|
|
|
for (int i = 0; i < end; i++) {
|
2021-05-23 19:06:26 +00:00
|
|
|
double ax = segment.getX(i);
|
2021-05-18 10:53:12 +00:00
|
|
|
double ay = segment.getY(i);
|
2021-05-23 19:06:26 +00:00
|
|
|
double bx = segment.getX(i + 1);
|
2021-05-18 10:53:12 +00:00
|
|
|
double by = segment.getY(i + 1);
|
|
|
|
|
2021-05-23 19:06:26 +00:00
|
|
|
double minX = Math.min(ax, bx);
|
|
|
|
double maxX = Math.max(ax, bx);
|
2021-05-18 10:53:12 +00:00
|
|
|
|
2021-05-22 11:07:34 +00:00
|
|
|
int startX = (int) Math.floor(minX - neighborBuffer);
|
|
|
|
int endX = (int) Math.floor(maxX + neighborBuffer);
|
2021-05-18 10:53:12 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
// for each column this segment crosses
|
2021-05-18 10:53:12 +00:00
|
|
|
for (int x = startX; x <= endX; x++) {
|
2021-05-23 19:06:26 +00:00
|
|
|
double axTile = ax - x;
|
|
|
|
double bxTile = bx - x;
|
2021-05-18 10:53:12 +00:00
|
|
|
MutableCoordinateSequence slice = xSlices.get(x);
|
|
|
|
if (slice == null) {
|
2021-05-19 10:44:28 +00:00
|
|
|
xSlices.put(x, slice = new MutableCoordinateSequence());
|
2021-05-18 10:53:12 +00:00
|
|
|
List<MutableCoordinateSequence> newGeom = newGeoms.get(x);
|
|
|
|
if (newGeom == null) {
|
|
|
|
newGeoms.put(x, newGeom = new ArrayList<>());
|
|
|
|
}
|
|
|
|
newGeom.add(slice);
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean exited = false;
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
if (axTile < leftLimit) {
|
2021-05-18 10:53:12 +00:00
|
|
|
// ---|--> | (line enters the clip region from the left)
|
2021-09-10 00:46:20 +00:00
|
|
|
if (bxTile > leftLimit) {
|
|
|
|
intersectX(slice, axTile, ay, bxTile, by, leftLimit);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
} else if (axTile > rightLimit) {
|
2021-05-18 10:53:12 +00:00
|
|
|
// | <--|--- (line enters the clip region from the right)
|
2021-09-10 00:46:20 +00:00
|
|
|
if (bxTile < rightLimit) {
|
|
|
|
intersectX(slice, axTile, ay, bxTile, by, rightLimit);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-09-10 00:46:20 +00:00
|
|
|
// | --> | (line starts inside)
|
2021-05-23 19:06:26 +00:00
|
|
|
slice.addPoint(axTile, ay);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
if (bxTile < leftLimit && axTile >= leftLimit) {
|
2021-05-18 10:53:12 +00:00
|
|
|
// <--|--- | or <--|-----|--- (line exits the clip region on the left)
|
2021-09-10 00:46:20 +00:00
|
|
|
intersectX(slice, axTile, ay, bxTile, by, leftLimit);
|
2021-05-18 10:53:12 +00:00
|
|
|
exited = true;
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
if (bxTile > rightLimit && axTile <= rightLimit) {
|
2021-05-18 10:53:12 +00:00
|
|
|
// | ---|--> or ---|-----|--> (line exits the clip region on the right)
|
2021-09-10 00:46:20 +00:00
|
|
|
intersectX(slice, axTile, ay, bxTile, by, rightLimit);
|
2021-05-18 10:53:12 +00:00
|
|
|
exited = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!area && exited) {
|
|
|
|
xSlices.remove(x);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// add the last point
|
2021-05-23 19:06:26 +00:00
|
|
|
double ax = segment.getX(segment.size() - 1);
|
2021-05-18 10:53:12 +00:00
|
|
|
double ay = segment.getY(segment.size() - 1);
|
2021-05-23 19:06:26 +00:00
|
|
|
int startX = (int) Math.floor(ax - neighborBuffer);
|
|
|
|
int endX = (int) Math.floor(ax + neighborBuffer);
|
2021-05-18 10:53:12 +00:00
|
|
|
|
|
|
|
for (int x = startX - 1; x <= endX + 1; x++) {
|
2021-05-23 19:06:26 +00:00
|
|
|
double axTile = ax - x;
|
2021-05-18 10:53:12 +00:00
|
|
|
MutableCoordinateSequence slice = xSlices.get(x);
|
2021-09-10 00:46:20 +00:00
|
|
|
if (slice != null && axTile >= leftLimit && axTile <= rightLimit) {
|
2021-05-23 19:06:26 +00:00
|
|
|
slice.addPoint(axTile, ay);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// close the polygons if endpoints are not the same after clipping
|
|
|
|
if (area) {
|
|
|
|
for (IntObjectCursor<MutableCoordinateSequence> cursor : xSlices) {
|
|
|
|
cursor.value.closeRing();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newGeoms.removeAll((x, value) -> {
|
2021-09-10 00:46:20 +00:00
|
|
|
int wrapped = wrapX(x, maxTilesAtThisZoom);
|
2021-05-18 10:53:12 +00:00
|
|
|
return !extents.testX(wrapped);
|
|
|
|
});
|
|
|
|
return newGeoms;
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* Splits an entire vertical X column of edge segments into Y rows that form (X, Y) tile coordinates at this zoom
|
|
|
|
* level, stores the result in {@link #tileContents} and returns the Y ranges of filled tile coordinates if this is a
|
|
|
|
* polygon.
|
|
|
|
*/
|
|
|
|
private IntRangeSet sliceY(CoordinateSequence stripeSegment, int x, boolean outer,
|
2021-05-18 10:53:12 +00:00
|
|
|
Map<TileCoord, List<CoordinateSequence>> inProgressShapes) {
|
2021-05-23 11:34:47 +00:00
|
|
|
if (stripeSegment.size() == 0) {
|
|
|
|
return null;
|
|
|
|
}
|
2021-05-18 10:53:12 +00:00
|
|
|
double leftEdge = -buffer;
|
|
|
|
double rightEdge = 1 + buffer;
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
TreeSet<Integer> tileYsWithDetail = null;
|
|
|
|
IntRangeSet rightFilled = null;
|
|
|
|
IntRangeSet leftFilled = null;
|
2021-05-18 10:53:12 +00:00
|
|
|
|
2022-03-01 13:43:19 +00:00
|
|
|
IntObjectMap<MutableCoordinateSequence> ySlices = Hppc.newIntObjectHashMap();
|
2021-09-10 00:46:20 +00:00
|
|
|
if (x < 0 || x >= maxTilesAtThisZoom) {
|
2021-05-18 10:53:12 +00:00
|
|
|
return null;
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
|
|
|
|
// keep a record of filled tiles that we skipped because an edge of the polygon that gets processed
|
|
|
|
// later may intersect the edge of a filled tile, and we'll need to replay all the edges we skipped
|
2021-05-23 20:10:44 +00:00
|
|
|
record SkippedSegment(Direction side, int lo, int hi) {}
|
2021-05-18 10:53:12 +00:00
|
|
|
List<SkippedSegment> skipped = null;
|
2021-09-10 00:46:20 +00:00
|
|
|
|
2021-05-18 10:53:12 +00:00
|
|
|
for (int i = 0; i < stripeSegment.size() - 1; i++) {
|
|
|
|
double ax = stripeSegment.getX(i);
|
|
|
|
double ay = stripeSegment.getY(i);
|
|
|
|
double bx = stripeSegment.getX(i + 1);
|
|
|
|
double by = stripeSegment.getY(i + 1);
|
|
|
|
|
|
|
|
double minY = Math.min(ay, by);
|
|
|
|
double maxY = Math.max(ay, by);
|
|
|
|
|
|
|
|
int extentMinY = extents.minY();
|
|
|
|
int extentMaxY = extents.maxY();
|
2021-05-22 11:07:34 +00:00
|
|
|
int startY = Math.max(extentMinY, (int) Math.floor(minY - neighborBuffer));
|
|
|
|
int endStartY = Math.max(extentMinY, (int) Math.floor(minY + neighborBuffer));
|
|
|
|
int startEndY = Math.min(extentMaxY - 1, (int) Math.floor(maxY - neighborBuffer));
|
|
|
|
int endY = Math.min(extentMaxY - 1, (int) Math.floor(maxY + neighborBuffer));
|
2021-05-18 10:53:12 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
// inside a fill if one edge of the polygon runs straight down the right side or up the left side of the column
|
2021-05-18 10:53:12 +00:00
|
|
|
boolean onRightEdge = area && ax == bx && ax == rightEdge && by > ay;
|
|
|
|
boolean onLeftEdge = area && ax == bx && ax == leftEdge && by < ay;
|
|
|
|
|
|
|
|
for (int y = startY; y <= endY; y++) {
|
2021-09-10 00:46:20 +00:00
|
|
|
// skip over filled tiles until we get to the next tile that already has detail on it
|
2021-05-18 10:53:12 +00:00
|
|
|
if (area && y > endStartY && y < startEndY) {
|
|
|
|
if (onRightEdge || onLeftEdge) {
|
2021-09-10 00:46:20 +00:00
|
|
|
if (tileYsWithDetail == null) {
|
|
|
|
tileYsWithDetail = new TreeSet<>();
|
2021-05-18 10:53:12 +00:00
|
|
|
for (IntCursor cursor : ySlices.keys()) {
|
2021-09-10 00:46:20 +00:00
|
|
|
tileYsWithDetail.add(cursor.value);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
Integer next = tileYsWithDetail.ceiling(y);
|
2021-05-18 10:53:12 +00:00
|
|
|
int nextNonEdgeTile = next == null ? startEndY : Math.min(next, startEndY);
|
|
|
|
int endSkip = nextNonEdgeTile - 1;
|
|
|
|
if (skipped == null) {
|
|
|
|
skipped = new ArrayList<>();
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
// save the Y range that we skipped in case a later edge intersects a filled tile
|
2021-05-18 10:53:12 +00:00
|
|
|
skipped.add(new SkippedSegment(
|
|
|
|
onLeftEdge ? Direction.LEFT : Direction.RIGHT,
|
|
|
|
y,
|
|
|
|
endSkip
|
|
|
|
));
|
|
|
|
|
|
|
|
if (rightFilled == null) {
|
2021-09-10 00:46:20 +00:00
|
|
|
rightFilled = new IntRangeSet();
|
|
|
|
leftFilled = new IntRangeSet();
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
(onRightEdge ? rightFilled : leftFilled).add(y, endSkip);
|
|
|
|
|
|
|
|
y = nextNonEdgeTile;
|
|
|
|
}
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
|
|
|
|
// emit linestring/polygon ring detail
|
|
|
|
double topLimit = y - buffer;
|
|
|
|
double bottomLimit = y + 1 + buffer;
|
2021-05-18 10:53:12 +00:00
|
|
|
MutableCoordinateSequence slice = ySlices.get(y);
|
|
|
|
if (slice == null) {
|
2021-09-10 00:46:20 +00:00
|
|
|
if (tileYsWithDetail != null) {
|
|
|
|
tileYsWithDetail.add(y);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
// X is already relative to tile, but we need to adjust Y
|
2021-05-19 10:44:28 +00:00
|
|
|
ySlices.put(y, slice = MutableCoordinateSequence.newScalingSequence(0, y, 256));
|
2021-05-18 10:53:12 +00:00
|
|
|
TileCoord tileID = TileCoord.ofXYZ(x, y, z);
|
|
|
|
List<CoordinateSequence> toAddTo = inProgressShapes.computeIfAbsent(tileID, tile -> new ArrayList<>());
|
|
|
|
|
|
|
|
// if this is tile is inside a fill from an outer tile, infer that fill here
|
|
|
|
if (area && !outer && toAddTo.isEmpty()) {
|
|
|
|
toAddTo.add(fill(buffer));
|
|
|
|
}
|
|
|
|
toAddTo.add(slice);
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
// if this tile was skipped because we skipped an edge, and now it needs more points,
|
|
|
|
// backfill all the edges that we skipped for it
|
2021-05-18 10:53:12 +00:00
|
|
|
if (area && leftFilled != null && skipped != null && (leftFilled.contains(y) || rightFilled.contains(y))) {
|
|
|
|
for (SkippedSegment skippedSegment : skipped) {
|
|
|
|
if (skippedSegment.lo <= y && skippedSegment.hi >= y) {
|
|
|
|
double top = y - buffer;
|
|
|
|
double bottom = y + 1 + buffer;
|
|
|
|
if (skippedSegment.side == Direction.LEFT) {
|
|
|
|
slice.addPoint(-buffer, bottom);
|
|
|
|
slice.addPoint(-buffer, top);
|
|
|
|
} else { // side == RIGHT
|
|
|
|
slice.addPoint(1 + buffer, top);
|
|
|
|
slice.addPoint(1 + buffer, bottom);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean exited = false;
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
if (ay < topLimit) {
|
2021-05-18 10:53:12 +00:00
|
|
|
// ---|--> | (line enters the clip region from the top)
|
2021-09-10 00:46:20 +00:00
|
|
|
if (by > topLimit) {
|
|
|
|
intersectY(slice, ax, ay, bx, by, topLimit);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
} else if (ay > bottomLimit) {
|
2021-05-18 10:53:12 +00:00
|
|
|
// | <--|--- (line enters the clip region from the bottom)
|
2021-09-10 00:46:20 +00:00
|
|
|
if (by < bottomLimit) {
|
|
|
|
intersectY(slice, ax, ay, bx, by, bottomLimit);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-09-10 00:46:20 +00:00
|
|
|
// | --> | (line starts inside the clip region)
|
2021-05-18 10:53:12 +00:00
|
|
|
slice.addPoint(ax, ay);
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
if (by < topLimit && ay >= topLimit) {
|
2021-05-18 10:53:12 +00:00
|
|
|
// <--|--- | or <--|-----|--- (line exits the clip region on the top)
|
2021-09-10 00:46:20 +00:00
|
|
|
intersectY(slice, ax, ay, bx, by, topLimit);
|
2021-05-18 10:53:12 +00:00
|
|
|
exited = true;
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
if (by > bottomLimit && ay <= bottomLimit) {
|
2021-05-18 10:53:12 +00:00
|
|
|
// | ---|--> or ---|-----|--> (line exits the clip region on the bottom)
|
2021-09-10 00:46:20 +00:00
|
|
|
intersectY(slice, ax, ay, bx, by, bottomLimit);
|
2021-05-18 10:53:12 +00:00
|
|
|
exited = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!area && exited) {
|
|
|
|
ySlices.remove(y);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add the last point
|
|
|
|
int last = stripeSegment.size() - 1;
|
|
|
|
double ax = stripeSegment.getX(last);
|
|
|
|
double ay = stripeSegment.getY(last);
|
2021-05-22 11:07:34 +00:00
|
|
|
int startY = (int) Math.floor(ay - neighborBuffer);
|
|
|
|
int endY = (int) Math.floor(ay + neighborBuffer);
|
2021-05-18 10:53:12 +00:00
|
|
|
|
|
|
|
for (int y = startY - 1; y <= endY + 1; y++) {
|
|
|
|
MutableCoordinateSequence slice = ySlices.get(y);
|
|
|
|
double k1 = y - buffer;
|
|
|
|
double k2 = y + 1 + buffer;
|
|
|
|
if (ay >= k1 && ay <= k2 && slice != null) {
|
|
|
|
slice.addPoint(ax, ay);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// close the polygons if endpoints are not the same after clipping
|
|
|
|
if (area) {
|
|
|
|
for (IntObjectCursor<MutableCoordinateSequence> cursor : ySlices) {
|
|
|
|
cursor.value.closeRing();
|
|
|
|
}
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
// a tile is filled if we skipped over the entire left and right side of it while processing a polygon
|
2021-05-18 10:53:12 +00:00
|
|
|
return rightFilled != null ? rightFilled.intersect(leftFilled) : null;
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
private void addFilledRange(int x, IntRangeSet yRange) {
|
2021-05-20 09:59:18 +00:00
|
|
|
if (yRange == null) {
|
2021-05-18 10:53:12 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-05-22 11:07:34 +00:00
|
|
|
if (filledRanges == null) {
|
|
|
|
filledRanges = new HashMap<>();
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
IntRangeSet existing = filledRanges.get(x);
|
2021-05-18 10:53:12 +00:00
|
|
|
if (existing == null) {
|
2021-05-20 09:59:18 +00:00
|
|
|
filledRanges.put(x, yRange);
|
2021-05-18 10:53:12 +00:00
|
|
|
} else {
|
2021-05-20 09:59:18 +00:00
|
|
|
existing.addAll(yRange);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
private void removeFilledRange(int x, IntRangeSet yRange) {
|
2021-05-20 09:59:18 +00:00
|
|
|
if (yRange == null) {
|
2021-05-18 10:53:12 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-05-22 11:07:34 +00:00
|
|
|
if (filledRanges == null) {
|
|
|
|
filledRanges = new HashMap<>();
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
IntRangeSet existing = filledRanges.get(x);
|
2021-05-18 10:53:12 +00:00
|
|
|
if (existing != null) {
|
2021-05-20 09:59:18 +00:00
|
|
|
existing.removeAll(yRange);
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-09 02:08:03 +00:00
|
|
|
private enum Direction {
|
|
|
|
RIGHT,
|
|
|
|
LEFT
|
|
|
|
}
|
2022-07-22 10:48:04 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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();
|
|
|
|
}
|
|
|
|
}
|
2021-05-18 10:53:12 +00:00
|
|
|
}
|