Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cheap isFill check and add --skip-filled-tiles option #234

Merged
merged 7 commits into from
Jun 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,34 @@ public byte[] encode() {
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 {
MOVE_TO(1),
LINE_TO(2),
Expand All @@ -519,12 +547,63 @@ private enum Command {
*/
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 {
if (scale < 0) {
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. */
public Geometry decode() throws GeometryException {
return decodeCommands(geomType, commands, scale);
Expand Down Expand Up @@ -566,6 +645,95 @@ public String toString() {
"], geomType=" + geomType +
" (" + 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);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public record PlanetilerConfig(
double simplifyToleranceAtMaxZoom,
double simplifyToleranceBelowMaxZoom,
boolean osmLazyReads,
boolean compactDb
boolean compactDb,
boolean skipFilledTiles
) {

public static final int MIN_MINZOOM = 0;
Expand Down Expand Up @@ -142,7 +143,10 @@ public static PlanetilerConfig from(Arguments arguments) {
false),
arguments.getBoolean("compact_db",
"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)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public class MbtilesWriter {
private static final Logger LOGGER = LoggerFactory.getLogger(MbtilesWriter.class);
private static final long MAX_FEATURES_PER_BATCH = 10_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 memoizedTiles;
private final Mbtiles db;
Expand Down Expand Up @@ -258,7 +257,9 @@ private void tileEncoder(Iterable<TileBatch> prev, Consumer<TileBatch> next) thr
*/
byte[] lastBytes = null, lastEncoded = null;
Long lastTileDataHash = null;
boolean lastIsFill = false;
boolean compactDb = config.compactDb();
boolean skipFilled = config.skipFilledTiles();

for (TileBatch batch : prev) {
Queue<TileEncodingResult> result = new ArrayDeque<>(batch.size());
Expand All @@ -270,23 +271,30 @@ private void tileEncoder(Iterable<TileBatch> prev, Consumer<TileBatch> next) thr
byte[] bytes, encoded;
Long tileDataHash;
if (tileFeatures.hasSameContents(last)) {
if (skipFilled && lastIsFill) {
continue;
}
bytes = lastBytes;
encoded = lastEncoded;
tileDataHash = lastTileDataHash;
memoizedTiles.inc();
} else {
VectorTile en = tileFeatures.getVectorTileEncoder();
encoded = en.encode();
bytes = gzip(encoded);
if (skipFilled) {
lastIsFill = en.containsOnlyFills();
if (lastIsFill) {
continue;
}
}
lastEncoded = encoded = en.encode();
lastBytes = bytes = gzip(encoded);
last = tileFeatures;
lastEncoded = encoded;
lastBytes = bytes;
if (encoded.length > 1_000_000) {
LOGGER.warn("{} {}kb uncompressed",
tileFeatures.tileCoord(),
encoded.length / 1024);
}
if (compactDb && tileFeatures.getNumFeaturesToEmit() < MAX_FEATURES_HASHING_THRESHOLD) {
if (compactDb && en.containsOnlyFillsOrEdges()) {
tileDataHash = tileFeatures.generateContentHash();
} else {
tileDataHash = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,25 @@ void testFullWorldPolygon() throws Exception {
)).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
@CsvSource({
"chesapeake.wkb, 4076",
Expand Down
Loading