From 0badb242bea6f77c8e388898868801f3f3a9088b Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Jul 2026 12:02:52 -0400 Subject: [PATCH] Variable-depth pyramids: don't prune children while a minzoom-gated feature is still pending (#399) Don't prune variable-depth children while a minzoom-gated feature is pending The minzoom_feature_pending flag from #397 keeps a variable-depth pyramid subdividing until explicit per-feature minzooms are satisfied, but two gaps let features still be dropped: The flag was only set when tippecanoe_minzoom > z + 1, so a feature whose minzoom is exactly z + 1 never marked the tile pending, even though a leaf at z carries only z-visible content. The early-stop commit never consulted the flag: a tile that succeeded in stopping early inserted itself into skip_children_out unconditionally, pruning the children the pending feature needed. The flag only inflated estimated_complexity_out, which the pruning ignores. Set the flag for any feature excluded below its minzoom, include it in the early-stop veto, and skip child pruning while it is set. Adds a fixture covering the minzoom == z + 1 boundary; make test passes with no diffs to existing fixtures. --- tests/minzoom-variable-depth/in.json | 2 ++ ...-generate-variable-depth-tile-pyramid.json | 28 +++++++++++++++++++ tile.cpp | 15 +++++----- 3 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 tests/minzoom-variable-depth/in.json create mode 100644 tests/minzoom-variable-depth/out/-Z10_-z11_--generate-variable-depth-tile-pyramid.json diff --git a/tests/minzoom-variable-depth/in.json b/tests/minzoom-variable-depth/in.json new file mode 100644 index 00000000..a5a2bfd6 --- /dev/null +++ b/tests/minzoom-variable-depth/in.json @@ -0,0 +1,2 @@ +{"type":"Feature","id":1,"tippecanoe":{"minzoom":10},"properties":{"depth":10},"geometry":{"type":"Point","coordinates":[0.05,0.05]}} +{"type":"Feature","id":2,"tippecanoe":{"minzoom":11},"properties":{"depth":11},"geometry":{"type":"Point","coordinates":[0.06,0.06]}} diff --git a/tests/minzoom-variable-depth/out/-Z10_-z11_--generate-variable-depth-tile-pyramid.json b/tests/minzoom-variable-depth/out/-Z10_-z11_--generate-variable-depth-tile-pyramid.json new file mode 100644 index 00000000..225bc76f --- /dev/null +++ b/tests/minzoom-variable-depth/out/-Z10_-z11_--generate-variable-depth-tile-pyramid.json @@ -0,0 +1,28 @@ +{ "type": "FeatureCollection", "properties": { +"antimeridian_adjusted_bounds": "0.050000,0.050000,0.060000,0.060000", +"bounds": "0.050000,0.050000,0.060000,0.060000", +"center": "0.060000,0.060000,11", +"description": "tests/minzoom-variable-depth/out/-Z10_-z11_--generate-variable-depth-tile-pyramid.json.check.mbtiles", +"format": "pbf", +"generator_options": "./tippecanoe -q -a@ -f -o tests/minzoom-variable-depth/out/-Z10_-z11_--generate-variable-depth-tile-pyramid.json.check.mbtiles -Z10 -z11 --generate-variable-depth-tile-pyramid tests/minzoom-variable-depth/in.json", +"json": "{\"vector_layers\":[{\"id\":\"in\",\"description\":\"\",\"minzoom\":10,\"maxzoom\":11,\"fields\":{\"depth\":\"Number\"}}],\"tilestats\":{\"layerCount\":1,\"layers\":[{\"layer\":\"in\",\"count\":2,\"geometry\":\"Point\",\"attributeCount\":1,\"attributes\":[{\"attribute\":\"depth\",\"count\":2,\"type\":\"number\",\"values\":[10,11],\"min\":10,\"max\":11}]}]}}", +"maxzoom": "11", +"minzoom": "10", +"name": "tests/minzoom-variable-depth/out/-Z10_-z11_--generate-variable-depth-tile-pyramid.json.check.mbtiles", +"type": "overlay", +"version": "2" +}, "features": [ +{ "type": "FeatureCollection", "properties": { "zoom": 10, "x": 512, "y": 511 }, "features": [ +{ "type": "FeatureCollection", "properties": { "layer": "in", "version": 2, "extent": 4096 }, "features": [ +{ "type": "Feature", "id": 1, "properties": { "depth": 10 }, "geometry": { "type": "Point", "coordinates": [ 0.050039, 0.049953 ] } } +] } +] } +, +{ "type": "FeatureCollection", "properties": { "zoom": 11, "x": 1024, "y": 1023 }, "features": [ +{ "type": "FeatureCollection", "properties": { "layer": "in", "version": 2, "extent": 4096 }, "features": [ +{ "type": "Feature", "id": 1, "properties": { "depth": 10 }, "geometry": { "type": "Point", "coordinates": [ 0.049996, 0.049996 ] } } +, +{ "type": "Feature", "id": 2, "properties": { "depth": 11 }, "geometry": { "type": "Point", "coordinates": [ 0.059996, 0.059996 ] } } +] } +] } +] } diff --git a/tile.cpp b/tile.cpp index 8f9b1bca..ede49e89 100644 --- a/tile.cpp +++ b/tile.cpp @@ -1205,9 +1205,9 @@ static serial_feature next_feature(decompressor *geoms, std::atomic * } if (sf.tippecanoe_minzoom != -1 && z < sf.tippecanoe_minzoom) { - if (sf.tippecanoe_minzoom > z + 1) { - next_feature_state.minzoom_feature_pending = true; - } + // a leaf at z carries only z-visible content, so an excluded feature at + // any deeper minzoom (even z + 1) must block leafing here + next_feature_state.minzoom_feature_pending = true; continue; } if (sf.tippecanoe_maxzoom != -1 && z > sf.tippecanoe_maxzoom) { @@ -2684,7 +2684,7 @@ long long write_tile(decompressor *geoms, std::atomic *geompos_in, ch oprogress = progress; } - if (trying_to_stop_early && line_detail == first_detail && !can_stop_early) { + if (trying_to_stop_early && line_detail == first_detail && (!can_stop_early || next_feature_state.minzoom_feature_pending)) { // didn't work, try a lower detail continue; } @@ -3036,9 +3036,10 @@ long long write_tile(decompressor *geoms, std::atomic *geompos_in, ch exit(EXIT_PTHREAD); } - if (trying_to_stop_early && line_detail == first_detail) { - // We succeeded in stopping early. - // Prune the child tiles. + if (trying_to_stop_early && line_detail == first_detail && + !next_feature_state.minzoom_feature_pending) { + // We succeeded in stopping early (and no excluded feature is + // waiting for a deeper zoom). Prune the child tiles. strategy.truncated_zooms++; skip_children_out.insert(zxy(z, tx, ty));