planetiler/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileExtents.java

115 wiersze
3.6 KiB
Java

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) {
this.zoomExtents = zoomExtents;
}
private static int quantizeDown(double value, int levels) {
return Math.clamp((int) Math.floor(value * levels), 0, levels);
}
private static int quantizeUp(double value, int levels) {
return Math.clamp((int) Math.ceil(value * levels), 0, levels);
}
/** 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;
var forZoom = new ForZoom(
zoom,
quantizeDown(worldBounds.getMinX(), max),
quantizeDown(worldBounds.getMinY(), max),
quantizeUp(worldBounds.getMaxX(), max),
quantizeUp(worldBounds.getMaxY(), max),
null
);
if (mercator != null) {
Geometry scaled = AffineTransformation.scaleInstance(1 << zoom, 1 << zoom).transform(mercator);
TiledGeometry.CoveredTiles covered;
try {
covered = TiledGeometry.getCoveredTiles(scaled, zoom, forZoom);
} catch (GeometryException e) {
throw new IllegalArgumentException("Invalid geometry: " + scaled);
}
forZoom = forZoom.withShape(covered);
LOGGER.info("prepareShapeForZoom z{} {}", zoom, covered);
}
zoomExtents[zoom] = forZoom;
}
return new TileExtents(zoomExtents);
}
public ForZoom getForZoom(int zoom) {
return zoomExtents[zoom];
}
public boolean test(int x, int y, int z) {
return getForZoom(z).test(x, y);
}
@Override
public boolean test(TileCoord tileCoord) {
return test(tileCoord.x(), tileCoord.y(), tileCoord.z());
}
/**
* X/Y extents within a given zoom level. {@code minX} and {@code minY} are inclusive and {@code maxX} and {@code
* maxY} are exclusive. shape is an optional polygon defining a more refine shape
*/
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) && 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) {
return x >= minX && x < maxX;
}
public boolean testY(int y) {
return y >= minY && y < maxY;
}
}
}