kopia lustrzana https://github.com/onthegomap/planetiler
Cheap isFill check and add `--skip-filled-tiles` option (#234)
rodzic
b0f634bcaf
commit
bed2220e83
|
@ -495,6 +495,34 @@ public class VectorTile {
|
||||||
return tile.build().toByteArray();
|
return tile.build().toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this tile contains only polygon fills.
|
||||||
|
*/
|
||||||
|
public boolean containsOnlyFills() {
|
||||||
|
return containsOnlyFillsOrEdges(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this tile contains only polygon fills or horizontal/vertical edges that are likely to be repeated
|
||||||
|
* across tiles.
|
||||||
|
*/
|
||||||
|
public boolean containsOnlyFillsOrEdges() {
|
||||||
|
return containsOnlyFillsOrEdges(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsOnlyFillsOrEdges(boolean allowEdges) {
|
||||||
|
boolean empty = true;
|
||||||
|
for (var layer : layers.values()) {
|
||||||
|
for (var feature : layer.encodedFeatures) {
|
||||||
|
empty = false;
|
||||||
|
if (!feature.geometry.isFillOrEdge(allowEdges)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !empty;
|
||||||
|
}
|
||||||
|
|
||||||
private enum Command {
|
private enum Command {
|
||||||
MOVE_TO(1),
|
MOVE_TO(1),
|
||||||
LINE_TO(2),
|
LINE_TO(2),
|
||||||
|
@ -519,12 +547,63 @@ public class VectorTile {
|
||||||
*/
|
*/
|
||||||
public record VectorGeometry(int[] commands, GeometryType geomType, int scale) {
|
public record VectorGeometry(int[] commands, GeometryType geomType, int scale) {
|
||||||
|
|
||||||
|
private static final int LEFT = 1;
|
||||||
|
private static final int RIGHT = 1 << 1;
|
||||||
|
private static final int TOP = 1 << 2;
|
||||||
|
private static final int BOTTOM = 1 << 3;
|
||||||
|
private static final int INSIDE = 0;
|
||||||
|
private static final int ALL = TOP | LEFT | RIGHT | BOTTOM;
|
||||||
|
|
||||||
public VectorGeometry {
|
public VectorGeometry {
|
||||||
if (scale < 0) {
|
if (scale < 0) {
|
||||||
throw new IllegalArgumentException("scale can not be less than 0, got: " + scale);
|
throw new IllegalArgumentException("scale can not be less than 0, got: " + scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int getSide(int x, int y, int extent) {
|
||||||
|
int result = INSIDE;
|
||||||
|
if (x < 0) {
|
||||||
|
result |= LEFT;
|
||||||
|
} else if (x > extent) {
|
||||||
|
result |= RIGHT;
|
||||||
|
}
|
||||||
|
if (y < 0) {
|
||||||
|
result |= TOP;
|
||||||
|
} else if (y > extent) {
|
||||||
|
result |= BOTTOM;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean slanted(int x1, int y1, int x2, int y2) {
|
||||||
|
return x1 != x2 && y1 != y2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean segmentCrossesTile(int x1, int y1, int x2, int y2, int extent) {
|
||||||
|
return (y1 >= 0 || y2 >= 0) &&
|
||||||
|
(y1 <= extent || y2 <= extent) &&
|
||||||
|
(x1 >= 0 || x2 >= 0) &&
|
||||||
|
(x1 <= extent || x2 <= extent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSegmentInvalid(boolean allowEdges, int x1, int y1, int x2, int y2, int extent) {
|
||||||
|
boolean crossesTile = segmentCrossesTile(x1, y1, x2, y2, extent);
|
||||||
|
if (allowEdges) {
|
||||||
|
return crossesTile && slanted(x1, y1, x2, y2);
|
||||||
|
} else {
|
||||||
|
return crossesTile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean visitedEnoughSides(boolean allowEdges, int sides) {
|
||||||
|
if (allowEdges) {
|
||||||
|
return ((sides & LEFT) > 0 && (sides & RIGHT) > 0) || ((sides & TOP) > 0 && (sides & BOTTOM) > 0);
|
||||||
|
} else {
|
||||||
|
return sides == ALL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Converts an encoded geometry back to a JTS geometry. */
|
/** Converts an encoded geometry back to a JTS geometry. */
|
||||||
public Geometry decode() throws GeometryException {
|
public Geometry decode() throws GeometryException {
|
||||||
return decodeCommands(geomType, commands, scale);
|
return decodeCommands(geomType, commands, scale);
|
||||||
|
@ -566,6 +645,95 @@ public class VectorTile {
|
||||||
"], geomType=" + geomType +
|
"], geomType=" + geomType +
|
||||||
" (" + geomType.asByte() + ")]";
|
" (" + geomType.asByte() + ")]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns true if the encoded geometry is a polygon fill. */
|
||||||
|
public boolean isFill() {
|
||||||
|
return isFillOrEdge(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the encoded geometry is a polygon fill, rectangle edge, or part of a horizontal/vertical line
|
||||||
|
* that is likely to be repeated across tiles.
|
||||||
|
*/
|
||||||
|
public boolean isFillOrEdge() {
|
||||||
|
return isFillOrEdge(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the encoded geometry is a polygon fill, or if {@code allowEdges == true} then also a rectangle
|
||||||
|
* edge, or part of a horizontal/vertical line that is likely to be repeated across tiles.
|
||||||
|
*/
|
||||||
|
public boolean isFillOrEdge(boolean allowEdges) {
|
||||||
|
if (geomType != GeometryType.POLYGON && (!allowEdges || geomType != GeometryType.LINE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isLine = geomType == GeometryType.LINE;
|
||||||
|
|
||||||
|
int extent = EXTENT << scale;
|
||||||
|
int visited = INSIDE;
|
||||||
|
int firstX = 0;
|
||||||
|
int firstY = 0;
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
|
||||||
|
int geometryCount = commands.length;
|
||||||
|
int length = 0;
|
||||||
|
int command = 0;
|
||||||
|
int i = 0;
|
||||||
|
while (i < geometryCount) {
|
||||||
|
|
||||||
|
if (length <= 0) {
|
||||||
|
length = commands[i++];
|
||||||
|
command = length & ((1 << 3) - 1);
|
||||||
|
length = length >> 3;
|
||||||
|
if (isLine && length > 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length > 0) {
|
||||||
|
if (command == Command.CLOSE_PATH.value) {
|
||||||
|
if (isSegmentInvalid(allowEdges, x, y, firstX, firstY, extent) ||
|
||||||
|
!visitedEnoughSides(allowEdges, visited)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
length--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dx = commands[i++];
|
||||||
|
int dy = commands[i++];
|
||||||
|
|
||||||
|
length--;
|
||||||
|
|
||||||
|
dx = zigZagDecode(dx);
|
||||||
|
dy = zigZagDecode(dy);
|
||||||
|
|
||||||
|
int nextX = x + dx;
|
||||||
|
int nextY = y + dy;
|
||||||
|
|
||||||
|
if (command == Command.MOVE_TO.value) {
|
||||||
|
firstX = nextX;
|
||||||
|
firstY = nextY;
|
||||||
|
if ((visited = getSide(firstX, firstY, extent)) == INSIDE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isSegmentInvalid(allowEdges, x, y, nextX, nextY, extent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
visited |= getSide(nextX, nextY, extent);
|
||||||
|
}
|
||||||
|
y = nextY;
|
||||||
|
x = nextX;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return visitedEnoughSides(allowEdges, visited);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -41,7 +41,8 @@ public record PlanetilerConfig(
|
||||||
double simplifyToleranceAtMaxZoom,
|
double simplifyToleranceAtMaxZoom,
|
||||||
double simplifyToleranceBelowMaxZoom,
|
double simplifyToleranceBelowMaxZoom,
|
||||||
boolean osmLazyReads,
|
boolean osmLazyReads,
|
||||||
boolean compactDb
|
boolean compactDb,
|
||||||
|
boolean skipFilledTiles
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static final int MIN_MINZOOM = 0;
|
public static final int MIN_MINZOOM = 0;
|
||||||
|
@ -142,7 +143,10 @@ public record PlanetilerConfig(
|
||||||
false),
|
false),
|
||||||
arguments.getBoolean("compact_db",
|
arguments.getBoolean("compact_db",
|
||||||
"Reduce the DB size by separating and deduping the tile data",
|
"Reduce the DB size by separating and deduping the tile data",
|
||||||
true)
|
true),
|
||||||
|
arguments.getBoolean("skip_filled_tiles",
|
||||||
|
"Skip writing tiles containing only polygon fills to the output",
|
||||||
|
false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,6 @@ public class MbtilesWriter {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(MbtilesWriter.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(MbtilesWriter.class);
|
||||||
private static final long MAX_FEATURES_PER_BATCH = 10_000;
|
private static final long MAX_FEATURES_PER_BATCH = 10_000;
|
||||||
private static final long MAX_TILES_PER_BATCH = 1_000;
|
private static final long MAX_TILES_PER_BATCH = 1_000;
|
||||||
private static final int MAX_FEATURES_HASHING_THRESHOLD = 5;
|
|
||||||
private final Counter.Readable featuresProcessed;
|
private final Counter.Readable featuresProcessed;
|
||||||
private final Counter memoizedTiles;
|
private final Counter memoizedTiles;
|
||||||
private final Mbtiles db;
|
private final Mbtiles db;
|
||||||
|
@ -258,7 +257,9 @@ public class MbtilesWriter {
|
||||||
*/
|
*/
|
||||||
byte[] lastBytes = null, lastEncoded = null;
|
byte[] lastBytes = null, lastEncoded = null;
|
||||||
Long lastTileDataHash = null;
|
Long lastTileDataHash = null;
|
||||||
|
boolean lastIsFill = false;
|
||||||
boolean compactDb = config.compactDb();
|
boolean compactDb = config.compactDb();
|
||||||
|
boolean skipFilled = config.skipFilledTiles();
|
||||||
|
|
||||||
for (TileBatch batch : prev) {
|
for (TileBatch batch : prev) {
|
||||||
Queue<TileEncodingResult> result = new ArrayDeque<>(batch.size());
|
Queue<TileEncodingResult> result = new ArrayDeque<>(batch.size());
|
||||||
|
@ -270,23 +271,30 @@ public class MbtilesWriter {
|
||||||
byte[] bytes, encoded;
|
byte[] bytes, encoded;
|
||||||
Long tileDataHash;
|
Long tileDataHash;
|
||||||
if (tileFeatures.hasSameContents(last)) {
|
if (tileFeatures.hasSameContents(last)) {
|
||||||
|
if (skipFilled && lastIsFill) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
bytes = lastBytes;
|
bytes = lastBytes;
|
||||||
encoded = lastEncoded;
|
encoded = lastEncoded;
|
||||||
tileDataHash = lastTileDataHash;
|
tileDataHash = lastTileDataHash;
|
||||||
memoizedTiles.inc();
|
memoizedTiles.inc();
|
||||||
} else {
|
} else {
|
||||||
VectorTile en = tileFeatures.getVectorTileEncoder();
|
VectorTile en = tileFeatures.getVectorTileEncoder();
|
||||||
encoded = en.encode();
|
if (skipFilled) {
|
||||||
bytes = gzip(encoded);
|
lastIsFill = en.containsOnlyFills();
|
||||||
|
if (lastIsFill) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastEncoded = encoded = en.encode();
|
||||||
|
lastBytes = bytes = gzip(encoded);
|
||||||
last = tileFeatures;
|
last = tileFeatures;
|
||||||
lastEncoded = encoded;
|
|
||||||
lastBytes = bytes;
|
|
||||||
if (encoded.length > 1_000_000) {
|
if (encoded.length > 1_000_000) {
|
||||||
LOGGER.warn("{} {}kb uncompressed",
|
LOGGER.warn("{} {}kb uncompressed",
|
||||||
tileFeatures.tileCoord(),
|
tileFeatures.tileCoord(),
|
||||||
encoded.length / 1024);
|
encoded.length / 1024);
|
||||||
}
|
}
|
||||||
if (compactDb && tileFeatures.getNumFeaturesToEmit() < MAX_FEATURES_HASHING_THRESHOLD) {
|
if (compactDb && en.containsOnlyFillsOrEdges()) {
|
||||||
tileDataHash = tileFeatures.generateContentHash();
|
tileDataHash = tileFeatures.generateContentHash();
|
||||||
} else {
|
} else {
|
||||||
tileDataHash = null;
|
tileDataHash = null;
|
||||||
|
|
|
@ -675,6 +675,25 @@ class PlanetilerTests {
|
||||||
)).stream().map(d -> d.geometry().geom().norm()).toList());
|
)).stream().map(d -> d.geometry().geom().norm()).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSkipFill() throws Exception {
|
||||||
|
var results = runWithReaderFeatures(
|
||||||
|
Map.of("threads", "1", "skip-filled-tiles", "true"),
|
||||||
|
List.of(
|
||||||
|
newReaderFeature(WORLD_POLYGON, Map.of())
|
||||||
|
),
|
||||||
|
(in, features) -> features.polygon("layer")
|
||||||
|
.setZoomRange(0, 6)
|
||||||
|
.setBufferPixels(4)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(481, results.tiles.size());
|
||||||
|
// spot-check one filled tile does not exist
|
||||||
|
assertNull(results.tiles.get(TileCoord.ofXYZ(
|
||||||
|
Z4_TILES / 2, Z4_TILES / 2, 4
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@CsvSource({
|
||||||
"chesapeake.wkb, 4076",
|
"chesapeake.wkb, 4076",
|
||||||
|
|
|
@ -33,6 +33,8 @@ import java.util.stream.Stream;
|
||||||
import org.junit.jupiter.api.DynamicTest;
|
import org.junit.jupiter.api.DynamicTest;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.TestFactory;
|
import org.junit.jupiter.api.TestFactory;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
import org.locationtech.jts.geom.Coordinate;
|
import org.locationtech.jts.geom.Coordinate;
|
||||||
import org.locationtech.jts.geom.CoordinateXY;
|
import org.locationtech.jts.geom.CoordinateXY;
|
||||||
import org.locationtech.jts.geom.Geometry;
|
import org.locationtech.jts.geom.Geometry;
|
||||||
|
@ -333,6 +335,99 @@ class VectorTileTest {
|
||||||
assertEquals("layer2", decoded.get(2).layer());
|
assertEquals("layer2", decoded.get(2).layer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({
|
||||||
|
"true,true,-1,-1,257,257",
|
||||||
|
"true,true,-5,-5,260,260",
|
||||||
|
"false,false,-1,-1,254,254",
|
||||||
|
"true,false,0,-1,257,257",
|
||||||
|
"true,false,-1,0,257,257",
|
||||||
|
"true,false,-1,-1,256,257",
|
||||||
|
"true,false,-1,-1,257,256",
|
||||||
|
|
||||||
|
"false,false,0,0,1,1",
|
||||||
|
"false,false,1,1,2,2",
|
||||||
|
"false,false,1,1,2,2",
|
||||||
|
|
||||||
|
"false,false,-10,-10,-5,-5",
|
||||||
|
"false,false,260,-10,270,5",
|
||||||
|
"false,false,-10,260,-5,270",
|
||||||
|
"false,false,260,260,270,270",
|
||||||
|
|
||||||
|
"true,false,1,-1,257,257",
|
||||||
|
"true,false,1,-1,255,257",
|
||||||
|
"false,false,1,-1,255,255",
|
||||||
|
})
|
||||||
|
void testRectangleIsFillOrEdge(boolean isFillOrEdge, boolean isFill, double x1, double y1, double x2, double y2) {
|
||||||
|
assertIsFillOrEdge(isFillOrEdge, isFill, rectangle(x1, y1, x2, y2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRectangleWithSlantedEdgeIsNotFill() {
|
||||||
|
assertIsFillOrEdge(false, false, newPolygon(
|
||||||
|
1, -1,
|
||||||
|
257, -1,
|
||||||
|
257, 257,
|
||||||
|
2, 257,
|
||||||
|
1, -1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({
|
||||||
|
"true,1,-1,1,257",
|
||||||
|
"false,1,1,1,257",
|
||||||
|
"false,1,-1,1,255",
|
||||||
|
"false,1,-1,2,257"
|
||||||
|
})
|
||||||
|
void testLineIsEdge(boolean isFillOrEdge, double x1, double y1, double x2, double y2) {
|
||||||
|
assertIsFillOrEdge(isFillOrEdge, false, newLineString(
|
||||||
|
x1, y1,
|
||||||
|
x2, y2
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCrossBoundaryNotFillOrEdge() {
|
||||||
|
assertIsFillOrEdge(false, false, newPolygon(
|
||||||
|
-1, -1,
|
||||||
|
257, -1,
|
||||||
|
-1, 257,
|
||||||
|
-1, -1
|
||||||
|
));
|
||||||
|
assertIsFillOrEdge(false, false, newPolygon(
|
||||||
|
257, -1,
|
||||||
|
-1, 257,
|
||||||
|
-1, -1,
|
||||||
|
257, -1
|
||||||
|
));
|
||||||
|
assertIsFillOrEdge(false, false, newPolygon(
|
||||||
|
-1, 257,
|
||||||
|
-1, -1,
|
||||||
|
257, -1,
|
||||||
|
-1, 257
|
||||||
|
));
|
||||||
|
assertIsFillOrEdge(false, false, newPolygon(
|
||||||
|
-1, -1,
|
||||||
|
513, -1,
|
||||||
|
-1, 513,
|
||||||
|
-1, -1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertIsFillOrEdge(boolean isFillOrEdge, boolean isFill, Geometry geom) {
|
||||||
|
for (int rotation : List.of(0, 90, 180, 270)) {
|
||||||
|
var rectangle =
|
||||||
|
AffineTransformation.rotationInstance(Math.PI * rotation / 180, 128, 128).transform(geom);
|
||||||
|
for (int scale = 0; scale < 4; scale++) {
|
||||||
|
assertEquals(isFillOrEdge, VectorTile.encodeGeometry(rectangle, scale).isFillOrEdge(),
|
||||||
|
"scale=" + scale + " rotation=" + rotation);
|
||||||
|
assertEquals(isFill, VectorTile.encodeGeometry(rectangle, scale).isFill(),
|
||||||
|
"scale=" + scale + " rotation=" + rotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void testRoundTripAttrs(Map<String, Object> attrs) {
|
private void testRoundTripAttrs(Map<String, Object> attrs) {
|
||||||
testRoundTrip(JTS_FACTORY.createPoint(new CoordinateXY(0, 0)), "layer", attrs, 1);
|
testRoundTrip(JTS_FACTORY.createPoint(new CoordinateXY(0, 0)), "layer", attrs, 1);
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue