Keep variable-depth tile pyramids consistent when a zoom level has to drop features (#407)

* Only skip polygon cleaning if we are still at very high resolution

* Remove collinear points and clean polygons even at high resolution

* If we truncated but still have the data and need to drop, revive

* Deduplicate by ID even when the duplicate is clipped away

* Test that deduplication works across tile boundaries

* Write out children of a tile revived after its parent truncated

A tile writes the geometry for its children on pass 0 of its zoom level,
and the later passes, which are only retries with new thresholds, must
not write it again. But a tile whose parent truncated its pyramid is
skipped on pass 0, and is only revived on a later pass, once the zoom
has had to start dropping features. Gating on pass 0 meant its children
were never written at all, so a revived tile was always a dead end: it
appeared in the output at ordinary detail with nothing below it, even
though its truncated ancestor still held the full-detail geometry.

Write the children on whichever pass first tiles the tile instead. The
dropping thresholds only ever increase within a zoom, so for a revived
tile that is exactly the pass on which the zoom started dropping.

Also collect the three thresholds into dropping_features(), since
write_tile() and run_thread() have to agree about when truncation is
disabled and when a skipped tile comes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

* Treat dropping by attribute like the other ways of dropping features

--drop-by-attribute-as-needed was added after variable-depth pyramids,
and minattribute never made it into the test for whether a zoom level is
discarding features. A zoom that was dropping by attribute could still
truncate pyramids, so some of its tiles became full-detail leaves while
the rest of the zoom had features dropped out of them, and tiles skipped
because an ancestor had truncated stayed missing.

Unlike the other thresholds, minattribute starts at the infinity on
whichever side is being kept rather than at zero, so dropping_features()
now takes the direction too.

On tests/tl_2022_11_tract at -Z10 -M15000, zoom 11 was dropping by
attribute and truncating two pyramids at the same time; now it truncates
none of them and the tile that had been skipped under zoom 10's
truncation is written out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

* Delete the merged tile that the deduplication test leaves behind

overzoom-test removes merged-dedup.pbf.json.check but not the
merged-dedup.pbf it was decoded from, so the file was left in the working
tree after every test run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

* Document what variable-depth pyramids now do to geometry and to dropping

Truncated tiles are no longer left uncleaned: they keep every vertex that
isn't collinear with its neighbors, but their polygons are cleaned so
that overlapping areas are merged instead of stacked. Say so, and say
that dropping features at a zoom level now suppresses truncation for the
whole zoom rather than for individual tiles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

* Start minattribute out at the infinity that excludes nothing

dropping_features() reads write_tile_args::minattribute, and the in-class
default of 0 decodes as a threshold that has already been chosen. Every
path assigns it from zoom_minattribute before anything reads it, so this
changes no behavior, but a future one that didn't would silently suppress
pyramid truncation rather than fail visibly.

-HUGE_VAL is the value that excludes nothing for the ascending order that
drop_by_attribute_descending also defaults to, so the two members agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

* Regenerate the drop-by-attribute fixture through the test harness

The Makefile can't be asked for a target whose name contains an =, since
make reads that as a variable assignment, so this fixture was generated by
hand into a scratch directory. The output path ends up in the tileset's
name, description, and generator_options, and tippecanoe-decode is only
passed -x generator, so all three were compared against the harness's
.check.mbtiles path and could never match. make test failed on it.

Regenerated with the same output path the rule uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

* Restore the -z14 -M25000 variable-depth fixture

This configuration was dropped rather than regenerated when the -z17
-M10000 fixture was added. It still runs, so it was losing a passing
regression test for no stated reason. Regenerated against current
behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

* Encode the = in the drop-by-attribute fixture name as %3d

A test output name containing an = can't be asked for on the make command
line, because make reads that argument as a variable assignment, so the
fixture couldn't be regenerated through its own rule. Add %3d to the
punctuation escapes that testargs decodes and use it here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

* Regenerate the man page for the README change

The variable-depth pyramid option's description changed, and man/tippecanoe.1
is generated from README.md, so the committed page no longer matched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KewFM6XCt5W9QBZkTZDjCp

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Erica Fischer
2026-08-07 12:26:33 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 734bba7c78
commit 63fcac725a
8 changed files with 7413 additions and 814 deletions
+7 -5
View File
@@ -144,6 +144,7 @@ suffixes = json json.gz
# %22 for quoted quote # %22 for quoted quote
# %2f for / # %2f for /
# %3a for : # %3a for :
# %3d for =
# %5f for _ # %5f for _
# %7b for { # %7b for {
@@ -151,10 +152,11 @@ testargs = \
$(subst %20,' ',\ $(subst %20,' ',\
$(subst %22,'"',\ $(subst %22,'"',\
$(subst %3a,:,\ $(subst %3a,:,\
$(subst %2f,/,\ $(subst %3d,=,\
$(subst %7b,'{',\ $(subst %2f,/,\
$(subst %5f,'_',\ $(subst %7b,'{',\
$(subst _, ,$(1)))))))) $(subst %5f,'_',\
$(subst _, ,$(1)))))))))
%.json.check: %.json.check:
./tippecanoe -q -a@ -f -o $@.mbtiles $(call testargs,$(patsubst %.json.check,%,$(word 4,$(subst /, ,$@)))) $(foreach suffix,$(suffixes),$(sort $(wildcard $(subst $(SPACE),/,$(wordlist 1,2,$(subst /, ,$@)))/*.$(suffix)))) < /dev/null ./tippecanoe -q -a@ -f -o $@.mbtiles $(call testargs,$(patsubst %.json.check,%,$(word 4,$(subst /, ,$@)))) $(foreach suffix,$(suffixes),$(sort $(wildcard $(subst $(SPACE),/,$(wordlist 1,2,$(subst /, ,$@)))/*.$(suffix)))) < /dev/null
@@ -438,7 +440,7 @@ overzoom-test: tippecanoe-overzoom
./tippecanoe-overzoom -b0 --deduplicate-by-id -o tests/pbf/merged-dedup.pbf -t 1/1/0 tests/pbf/1.json.dir/0/0/0.pbf 0/0/0 tests/pbf/2.json.dir/0/0/0.pbf 0/0/0 ./tippecanoe-overzoom -b0 --deduplicate-by-id -o tests/pbf/merged-dedup.pbf -t 1/1/0 tests/pbf/1.json.dir/0/0/0.pbf 0/0/0 tests/pbf/2.json.dir/0/0/0.pbf 0/0/0
./tippecanoe-decode tests/pbf/merged-dedup.pbf 1 1 0 > tests/pbf/merged-dedup.pbf.json.check ./tippecanoe-decode tests/pbf/merged-dedup.pbf 1 1 0 > tests/pbf/merged-dedup.pbf.json.check
cmp tests/pbf/merged-dedup.pbf.json.check tests/pbf/merged-dedup.pbf.json cmp tests/pbf/merged-dedup.pbf.json.check tests/pbf/merged-dedup.pbf.json
rm -r tests/pbf/1.json.dir tests/pbf/2.json.dir tests/pbf/merged-nodedup.pbf tests/pbf/merged-nodedup.pbf.json.check tests/pbf/merged-dedup.pbf.json.check rm -r tests/pbf/1.json.dir tests/pbf/2.json.dir tests/pbf/merged-nodedup.pbf tests/pbf/merged-nodedup.pbf.json.check tests/pbf/merged-dedup.pbf tests/pbf/merged-dedup.pbf.json.check
join-test: tippecanoe tippecanoe-decode tile-join join-test: tippecanoe tippecanoe-decode tile-join
./tippecanoe -q -f -z12 -o tests/join-population/tabblock_06001420.mbtiles -YALAND10:'Land area' -L'{"file": "tests/join-population/tabblock_06001420.json", "description": "population"}' ./tippecanoe -q -f -z12 -o tests/join-population/tabblock_06001420.mbtiles -YALAND10:'Land area' -L'{"file": "tests/join-population/tabblock_06001420.json", "description": "population"}'
+1 -1
View File
@@ -357,7 +357,7 @@ Parallel processing will also be automatic if the input file is in FlatGeobuf fo
specified maximum zoom and to any levels added beyond that. specified maximum zoom and to any levels added beyond that.
* `--extend-zooms-if-still-dropping-maximum=`_count_: Increase the maxzoom if features are still being dropped at that zoom level * `--extend-zooms-if-still-dropping-maximum=`_count_: Increase the maxzoom if features are still being dropped at that zoom level
by up to _count_ zoom levels. by up to _count_ zoom levels.
* `-at` or `--generate-variable-depth-tile-pyramid`: Don't produce child tiles for any tile that should be sufficient to be overzoomed to any higher zoom level. Such tiles will be produced with maximum detail and no simplification or polygon cleaning. Tiles with point features below the basezoom or where any features have to be dropped dynamically, or which contain too many features or bytes with full detail, will be written out with normal detail and split into child tiles. Tilesets generated with this option are suitable for use only with tile servers that will find the appropriate tile to overzoom from and will simplify and clean the geometries appropriately before serving the tile. * `-at` or `--generate-variable-depth-tile-pyramid`: Don't produce child tiles for any tile that should be sufficient to be overzoomed to any higher zoom level. Such tiles will be produced with maximum detail, retaining every vertex that is not collinear with its neighbors, but their polygons are still cleaned so that overlapping areas are merged rather than left stacked. Tiles with point features below the basezoom, or which contain too many features or bytes with full detail, will be written out with normal detail and split into child tiles. If any features have to be dropped dynamically at a zoom level, no tile in that zoom level is truncated, so that the whole zoom level is reduced consistently; any tile that was already being skipped because an ancestor had been truncated is written out after all. Tilesets generated with this option are suitable for use only with tile servers that will find the appropriate tile to overzoom from and will simplify the geometries appropriately before serving the tile.
* `-R` _zoom_`/`_x_`/`_y_ or `--one-tile=`_zoom_`/`_x_`/`_y_: Set the minzoom and maxzoom to _zoom_ and produce only * `-R` _zoom_`/`_x_`/`_y_ or `--one-tile=`_zoom_`/`_x_`/`_y_: Set the minzoom and maxzoom to _zoom_ and produce only
the single specified tile at that zoom level. the single specified tile at that zoom level.
+1 -1
View File
@@ -357,7 +357,7 @@ specified maximum zoom and to any levels added beyond that.
\fB--extend-zooms-if-still-dropping-maximum=\fR\fIcount\fP: Increase the maxzoom if features are still being dropped at that zoom level \fB--extend-zooms-if-still-dropping-maximum=\fR\fIcount\fP: Increase the maxzoom if features are still being dropped at that zoom level
by up to \fIcount\fP zoom levels. by up to \fIcount\fP zoom levels.
.IP \(bu 2 .IP \(bu 2
\fB-at\fR or \fB--generate-variable-depth-tile-pyramid\fR: Don't produce child tiles for any tile that should be sufficient to be overzoomed to any higher zoom level. Such tiles will be produced with maximum detail and no simplification or polygon cleaning. Tiles with point features below the basezoom or where any features have to be dropped dynamically, or which contain too many features or bytes with full detail, will be written out with normal detail and split into child tiles. Tilesets generated with this option are suitable for use only with tile servers that will find the appropriate tile to overzoom from and will simplify and clean the geometries appropriately before serving the tile. \fB-at\fR or \fB--generate-variable-depth-tile-pyramid\fR: Don't produce child tiles for any tile that should be sufficient to be overzoomed to any higher zoom level. Such tiles will be produced with maximum detail, retaining every vertex that is not collinear with its neighbors, but their polygons are still cleaned so that overlapping areas are merged rather than left stacked. Tiles with point features below the basezoom, or which contain too many features or bytes with full detail, will be written out with normal detail and split into child tiles. If any features have to be dropped dynamically at a zoom level, no tile in that zoom level is truncated, so that the whole zoom level is reduced consistently; any tile that was already being skipped because an ancestor had been truncated is written out after all. Tilesets generated with this option are suitable for use only with tile servers that will find the appropriate tile to overzoom from and will simplify the geometries appropriately before serving the tile.
.IP \(bu 2 .IP \(bu 2
\fB-R\fR \fIzoom\fP\fB/\fR\fIx\fP\fB/\fR\fIy\fP or \fB--one-tile=\fR\fIzoom\fP\fB/\fR\fIx\fP\fB/\fR\fIy\fP: Set the minzoom and maxzoom to \fIzoom\fP and produce only \fB-R\fR \fIzoom\fP\fB/\fR\fIx\fP\fB/\fR\fIy\fP or \fB--one-tile=\fR\fIzoom\fP\fB/\fR\fIx\fP\fB/\fR\fIy\fP: Set the minzoom and maxzoom to \fIzoom\fP and produce only
the single specified tile at that zoom level. the single specified tile at that zoom level.
+89 -28
View File
@@ -538,7 +538,6 @@ struct simplification_worker_arg {
std::vector<std::shared_ptr<serial_feature>> *features = NULL; std::vector<std::shared_ptr<serial_feature>> *features = NULL;
int task = 0; int task = 0;
int tasks = 0; int tasks = 0;
bool trying_to_stop_early = false;
drawvec *shared_nodes; drawvec *shared_nodes;
node *shared_nodes_map; node *shared_nodes_map;
@@ -673,9 +672,7 @@ static void *simplification_worker(void *v) {
for (size_t i = a->task; i < (*features).size(); i += a->tasks) { for (size_t i = a->task; i < (*features).size(); i += a->tasks) {
double area = 0; double area = 0;
if (!a->trying_to_stop_early) { area = simplify_feature(&*((*features)[i]), *(a->shared_nodes), a->shared_nodes_map, a->nodepos, *(a->shared_nodes_bloom));
area = simplify_feature(&*((*features)[i]), *(a->shared_nodes), a->shared_nodes_map, a->nodepos, *(a->shared_nodes_bloom));
}
signed char t = (*features)[i]->t; signed char t = (*features)[i]->t;
int z = (*features)[i]->z; int z = (*features)[i]->z;
@@ -690,20 +687,18 @@ static void *simplification_worker(void *v) {
{ {
drawvec before = geom; drawvec before = geom;
if (!a->trying_to_stop_early) { // we can try scaling up because this is now tile scale
// we can try scaling up because this is now tile scale coalesce_polygon(geom, true);
coalesce_polygon(geom, true); if (additional[A_DEBUG_POLYGON]) {
if (additional[A_DEBUG_POLYGON]) { check_polygon(geom);
check_polygon(geom); }
}
if (geom.size() < 3) { if (geom.size() < 3) {
if (area > 0) { if (area > 0) {
// area is in world coordinates, calculated before scaling down // area is in world coordinates, calculated before scaling down
geom = revive_polygon(before, area, z, out_detail); geom = revive_polygon(before, area, z, out_detail);
} else { } else {
geom.clear(); geom.clear();
}
} }
} }
} }
@@ -926,8 +921,12 @@ struct write_tile_args {
long long minextent_out = 0; long long minextent_out = 0;
unsigned long long mindrop_sequence = 0; unsigned long long mindrop_sequence = 0;
unsigned long long mindrop_sequence_out = 0; unsigned long long mindrop_sequence_out = 0;
double minattribute = 0; // the infinity that excludes nothing, for the ascending order that
double minattribute_out = 0; // drop_by_attribute_descending also defaults to. dropping_features()
// reads these, so they must not start out at a value that looks like
// a threshold that has already been chosen.
double minattribute = -HUGE_VAL;
double minattribute_out = -HUGE_VAL;
std::string const *drop_by_attribute_as_needed_attribute = NULL; std::string const *drop_by_attribute_as_needed_attribute = NULL;
bool drop_by_attribute_descending = false; bool drop_by_attribute_descending = false;
size_t tile_size_out = 0; size_t tile_size_out = 0;
@@ -949,8 +948,26 @@ struct write_tile_args {
std::string const *shared_nodes_bloom; std::string const *shared_nodes_bloom;
std::set<zxy> const *skip_children; // what is being skipped at this zoom std::set<zxy> const *skip_children; // what is being skipped at this zoom
std::set<zxy> skip_children_out; // what will be skipped in the next zoom std::set<zxy> skip_children_out; // what will be skipped in the next zoom
bool first_dropping_pass = false; // is this the first pass of this zoom that is dropping features?
}; };
// Is this zoom level discarding features to make its tiles fit? The thresholds
// start out excluding nothing, and only move inward once some tile in the zoom
// fails to fit and the whole zoom is retried with a threshold for discarding
// features. For mingap, minextent, and mindrop_sequence that starting point is
// zero; for minattribute it is the infinity on whichever side is being kept.
//
// Pyramid truncation is disabled while this is true, so that every tile in the
// zoom is reduced consistently, and tiles that were skipped because an ancestor
// truncated its pyramid are revived.
static bool dropping_features(unsigned long long mingap, long long minextent, unsigned long long mindrop_sequence, double minattribute, bool drop_by_attribute_descending) {
bool dropping_by_attribute = drop_by_attribute_descending
? minattribute != HUGE_VAL
: minattribute != -HUGE_VAL;
return mingap != 0 || minextent != 0 || mindrop_sequence != 0 || dropping_by_attribute;
}
// Clips a feature's geometry to the tile bounds at the specified zoom level // Clips a feature's geometry to the tile bounds at the specified zoom level
// with the specified buffer. Returns true if the feature was entirely clipped away // with the specified buffer. Returns true if the feature was entirely clipped away
// by bounding box alone; otherwise returns false. // by bounding box alone; otherwise returns false.
@@ -1102,7 +1119,7 @@ struct next_feature_state {
// This function is called repeatedly from write_tile() to retrieve the next feature // This function is called repeatedly from write_tile() to retrieve the next feature
// from the input stream. If the stream is at an end, it returns a feature with the // from the input stream. If the stream is at an end, it returns a feature with the
// geometry type set to -2. // geometry type set to -2.
static serial_feature next_feature(decompressor *geoms, std::atomic<long long> *geompos_in, int z, unsigned tx, unsigned ty, unsigned *initial_x, unsigned *initial_y, long long *original_features, long long *unclipped_features, int nextzoom, int maxzoom, int minzoom, int max_zoom_increment, size_t pass, std::atomic<long long> *along, long long alongminus, int buffer, std::atomic<bool> *within, compressor **geomfile, std::atomic<long long> *geompos, long long start_geompos[], std::atomic<double> *oprogress, double todo, const char *fname, int child_shards, json_object *filter, const char *global_stringpool, long long *pool_off, std::vector<std::vector<std::string>> *layer_unmaps, bool first_time, bool compressed, multiplier_state *multiplier_state, std::shared_ptr<std::string> &tile_stringpool, std::vector<std::string> const &unidecode_data, next_feature_state &next_feature_state, double droprate) { static serial_feature next_feature(decompressor *geoms, std::atomic<long long> *geompos_in, int z, unsigned tx, unsigned ty, unsigned *initial_x, unsigned *initial_y, long long *original_features, long long *unclipped_features, int nextzoom, int maxzoom, int minzoom, int max_zoom_increment, size_t pass, std::atomic<long long> *along, long long alongminus, int buffer, std::atomic<bool> *within, compressor **geomfile, std::atomic<long long> *geompos, long long start_geompos[], std::atomic<double> *oprogress, double todo, const char *fname, int child_shards, json_object *filter, const char *global_stringpool, long long *pool_off, std::vector<std::vector<std::string>> *layer_unmaps, bool first_time, bool write_children, bool compressed, multiplier_state *multiplier_state, std::shared_ptr<std::string> &tile_stringpool, std::vector<std::string> const &unidecode_data, next_feature_state &next_feature_state, double droprate) {
double extra_multiplier_zooms = log(retain_points_multiplier) / log(droprate); double extra_multiplier_zooms = log(retain_points_multiplier) / log(droprate);
while (1) { while (1) {
@@ -1191,7 +1208,7 @@ static serial_feature next_feature(decompressor *geoms, std::atomic<long long> *
// XXX should continue, but affects test outputs // XXX should continue, but affects test outputs
} }
if (first_time && pass == 0) { /* only write out the next zoom once, even if we retry */ if (first_time && write_children) { /* only write out the next zoom once, even if we retry */
if (sf.tippecanoe_maxzoom == -1 || sf.tippecanoe_maxzoom >= nextzoom) { if (sf.tippecanoe_maxzoom == -1 || sf.tippecanoe_maxzoom >= nextzoom) {
rewrite(sf, z, nextzoom, maxzoom, tx, ty, buffer, within, geompos, start_geompos, geomfile, fname, child_shards, max_zoom_increment, sf.segment, initial_x, initial_y); rewrite(sf, z, nextzoom, maxzoom, tx, ty, buffer, within, geompos, start_geompos, geomfile, fname, child_shards, max_zoom_increment, sf.segment, initial_x, initial_y);
} }
@@ -1356,6 +1373,7 @@ struct run_prefilter_args {
json_object *filter = NULL; json_object *filter = NULL;
std::vector<std::string> const *unidecode_data; std::vector<std::string> const *unidecode_data;
bool first_time = false; bool first_time = false;
bool write_children = false;
bool compressed = false; bool compressed = false;
double droprate = 1; double droprate = 1;
}; };
@@ -1368,7 +1386,7 @@ void *run_prefilter(void *v) {
next_feature_state next_feature_state; next_feature_state next_feature_state;
while (1) { while (1) {
serial_feature sf = next_feature(rpa->geoms, rpa->geompos_in, rpa->z, rpa->tx, rpa->ty, rpa->initial_x, rpa->initial_y, rpa->original_features, rpa->unclipped_features, rpa->nextzoom, rpa->maxzoom, rpa->minzoom, rpa->max_zoom_increment, rpa->pass, rpa->along, rpa->alongminus, rpa->buffer, rpa->within, rpa->geomfile, rpa->geompos, rpa->start_geompos, rpa->oprogress, rpa->todo, rpa->fname, rpa->child_shards, rpa->filter, rpa->global_stringpool, rpa->pool_off, rpa->layer_unmaps, rpa->first_time, rpa->compressed, &multiplier_state, tile_stringpool, *(rpa->unidecode_data), next_feature_state, rpa->droprate); serial_feature sf = next_feature(rpa->geoms, rpa->geompos_in, rpa->z, rpa->tx, rpa->ty, rpa->initial_x, rpa->initial_y, rpa->original_features, rpa->unclipped_features, rpa->nextzoom, rpa->maxzoom, rpa->minzoom, rpa->max_zoom_increment, rpa->pass, rpa->along, rpa->alongminus, rpa->buffer, rpa->within, rpa->geomfile, rpa->geompos, rpa->start_geompos, rpa->oprogress, rpa->todo, rpa->fname, rpa->child_shards, rpa->filter, rpa->global_stringpool, rpa->pool_off, rpa->layer_unmaps, rpa->first_time, rpa->write_children, rpa->compressed, &multiplier_state, tile_stringpool, *(rpa->unidecode_data), next_feature_state, rpa->droprate);
if (sf.t < 0) { if (sf.t < 0) {
break; break;
} }
@@ -1644,7 +1662,7 @@ void skip_tile(decompressor *geoms, std::atomic<long long> *geompos_in, bool com
} }
} }
long long write_tile(decompressor *geoms, std::atomic<long long> *geompos_in, char *global_stringpool, int z, const unsigned tx, const unsigned ty, const int detail, int min_detail, sqlite3 *outdb, const char *outdir, int buffer, const char *fname, compressor **geomfile, std::atomic<long long> *geompos, int minzoom, int maxzoom, double todo, std::atomic<long long> *along, long long alongminus, double gamma, int child_shards, long long *pool_off, unsigned *initial_x, unsigned *initial_y, std::atomic<int> *running, double simplification, std::vector<std::map<std::string, layermap_entry>> *layermaps, std::vector<std::vector<std::string>> *layer_unmaps, size_t tiling_seg, size_t pass, unsigned long long mingap, long long minextent, unsigned long long mindrop_sequence, double minattribute, const char *prefilter, const char *postfilter, json_object *filter, write_tile_args *arg, atomic_strategy *strategy_out, bool compressed_input, node *shared_nodes_map, size_t nodepos, std::string const &shared_nodes_bloom, std::vector<std::string> const &unidecode_data, long long estimated_complexity, std::set<zxy> &skip_children_out) { long long write_tile(decompressor *geoms, std::atomic<long long> *geompos_in, char *global_stringpool, int z, const unsigned tx, const unsigned ty, const int detail, int min_detail, sqlite3 *outdb, const char *outdir, int buffer, const char *fname, compressor **geomfile, std::atomic<long long> *geompos, int minzoom, int maxzoom, double todo, std::atomic<long long> *along, long long alongminus, double gamma, int child_shards, long long *pool_off, unsigned *initial_x, unsigned *initial_y, std::atomic<int> *running, double simplification, std::vector<std::map<std::string, layermap_entry>> *layermaps, std::vector<std::vector<std::string>> *layer_unmaps, size_t tiling_seg, size_t pass, unsigned long long mingap, long long minextent, unsigned long long mindrop_sequence, double minattribute, const char *prefilter, const char *postfilter, json_object *filter, write_tile_args *arg, atomic_strategy *strategy_out, bool compressed_input, node *shared_nodes_map, size_t nodepos, std::string const &shared_nodes_bloom, std::vector<std::string> const &unidecode_data, long long estimated_complexity, bool write_children, std::set<zxy> &skip_children_out) {
double merge_fraction = 1; double merge_fraction = 1;
double mingap_fraction = 1; double mingap_fraction = 1;
double minextent_fraction = 1; double minextent_fraction = 1;
@@ -1680,7 +1698,7 @@ long long write_tile(decompressor *geoms, std::atomic<long long> *geompos_in, ch
int first_detail = detail, second_detail = detail - 1; int first_detail = detail, second_detail = detail - 1;
bool trying_to_stop_early = false; bool trying_to_stop_early = false;
bool can_stop_early = true; bool can_stop_early = true;
if (additional[A_VARIABLE_DEPTH_PYRAMID]) { if (additional[A_VARIABLE_DEPTH_PYRAMID] && !dropping_features(mingap, minextent, mindrop_sequence, minattribute, arg->drop_by_attribute_descending)) {
// If we are trying to stop early, there is an extra first pass with full+extra detail, // If we are trying to stop early, there is an extra first pass with full+extra detail,
// and which loops if everything doesn't fit rather than trying to drop or union features. // and which loops if everything doesn't fit rather than trying to drop or union features.
@@ -1825,6 +1843,7 @@ long long write_tile(decompressor *geoms, std::atomic<long long> *geompos_in, ch
rpa.filter = filter; rpa.filter = filter;
rpa.unidecode_data = &unidecode_data; rpa.unidecode_data = &unidecode_data;
rpa.first_time = first_time; rpa.first_time = first_time;
rpa.write_children = write_children;
rpa.compressed = compressed_input; rpa.compressed = compressed_input;
rpa.droprate = arg->droprate; rpa.droprate = arg->droprate;
@@ -1858,7 +1877,7 @@ long long write_tile(decompressor *geoms, std::atomic<long long> *geompos_in, ch
ssize_t which_serial_feature = -1; ssize_t which_serial_feature = -1;
if (prefilter == NULL) { if (prefilter == NULL) {
sf = next_feature(geoms, geompos_in, z, tx, ty, initial_x, initial_y, &original_features, &unclipped_features, nextzoom, maxzoom, minzoom, max_zoom_increment, pass, along, alongminus, buffer, within.data(), geomfile, geompos, start_geompos.data(), &oprogress, todo, fname, child_shards, filter, global_stringpool, pool_off, layer_unmaps, first_time, compressed_input, &multiplier_state, tile_stringpool, unidecode_data, next_feature_state, arg->droprate); sf = next_feature(geoms, geompos_in, z, tx, ty, initial_x, initial_y, &original_features, &unclipped_features, nextzoom, maxzoom, minzoom, max_zoom_increment, pass, along, alongminus, buffer, within.data(), geomfile, geompos, start_geompos.data(), &oprogress, todo, fname, child_shards, filter, global_stringpool, pool_off, layer_unmaps, first_time, write_children, compressed_input, &multiplier_state, tile_stringpool, unidecode_data, next_feature_state, arg->droprate);
} else { } else {
sf = parse_feature(prefilter_jp, z, tx, ty, layermaps, tiling_seg, layer_unmaps, postfilter != NULL, key_pool); sf = parse_feature(prefilter_jp, z, tx, ty, layermaps, tiling_seg, layer_unmaps, postfilter != NULL, key_pool);
} }
@@ -2248,6 +2267,12 @@ long long write_tile(decompressor *geoms, std::atomic<long long> *geompos_in, ch
sf.clustered = 0; sf.clustered = 0;
sf.tile_stringpool = tile_stringpool; sf.tile_stringpool = tile_stringpool;
if (trying_to_stop_early && line_detail == first_detail) {
// only remove collinearities;
// leave all other vertices for extreme overzooming
sf.simplification = 0;
}
if (line_detail == detail && extra_detail >= 0 && z == maxzoom) { if (line_detail == detail && extra_detail >= 0 && z == maxzoom) {
sf.extra_detail = extra_detail; sf.extra_detail = extra_detail;
// maximum allowed coordinate delta in geometries is 2^31 - 1 // maximum allowed coordinate delta in geometries is 2^31 - 1
@@ -2456,7 +2481,6 @@ long long write_tile(decompressor *geoms, std::atomic<long long> *geompos_in, ch
args[i].shared_nodes_map = shared_nodes_map; args[i].shared_nodes_map = shared_nodes_map;
args[i].nodepos = nodepos; args[i].nodepos = nodepos;
args[i].shared_nodes_bloom = &shared_nodes_bloom; args[i].shared_nodes_bloom = &shared_nodes_bloom;
args[i].trying_to_stop_early = trying_to_stop_early;
if (tasks > 1) { if (tasks > 1) {
if (thread_create(&pthreads[i], NULL, simplification_worker, &args[i]) != 0) { if (thread_create(&pthreads[i], NULL, simplification_worker, &args[i]) != 0) {
@@ -2883,7 +2907,6 @@ long long write_tile(decompressor *geoms, std::atomic<long long> *geompos_in, ch
if (trying_to_stop_early && line_detail == first_detail) { if (trying_to_stop_early && line_detail == first_detail) {
// didn't work, try a lower detail // didn't work, try a lower detail
detail_reduced++;
continue; continue;
} }
@@ -3146,12 +3169,41 @@ exit(EXIT_IMPOSSIBLE);
long long len; long long len;
struct zxy parent(z - 1, x / 2, y / 2); struct zxy parent(z - 1, x / 2, y / 2);
bool skip = false;
// Normally a tile writes the geometry for its children on pass 0, and the
// later passes of the same zoom, which are only retries with new thresholds,
// must not write it again.
bool write_children = (arg->pass == 0);
if (arg->skip_children->count(parent) > 0) { if (arg->skip_children->count(parent) > 0) {
if (dropping_features(arg->mingap, arg->minextent, arg->mindrop_sequence, arg->minattribute, arg->drop_by_attribute_descending)) {
// The parent truncated its pyramid here, but the zoom has since had to
// start dropping features, so the truncation no longer holds and this
// tile has to be written after all. Its geometry is still in the stream.
skip = false;
// It was skipped on every earlier pass of this zoom, so its children
// have never been written out. Write them on the first pass that
// actually tiles it, rather than on pass 0 as usual. The thresholds
// only ever increase within a zoom, so that is exactly the pass on
// which this zoom started dropping.
write_children = arg->first_dropping_pass;
} else {
// skip if the parent tile finished at the last zoom level and we aren't trying to drop
skip = true;
}
} else {
// do the tile if the parent didn't finish early
skip = false;
}
if (skip) {
skip_tile(&dc, &geompos, arg->compressed); skip_tile(&dc, &geompos, arg->compressed);
len = 1; len = 1;
} else { } else {
arg->wrote_zoom = z; arg->wrote_zoom = z;
len = write_tile(&dc, &geompos, arg->global_stringpool, z, x, y, z == arg->maxzoom ? arg->full_detail : arg->low_detail, arg->min_detail, arg->outdb, arg->outdir, arg->buffer, arg->fname, arg->geomfile, arg->geompos, arg->minzoom, arg->maxzoom, arg->todo, arg->along, geompos, arg->gamma, arg->child_shards, arg->pool_off, arg->initial_x, arg->initial_y, arg->running, arg->simplification, arg->layermaps, arg->layer_unmaps, arg->tiling_seg, arg->pass, arg->mingap, arg->minextent, arg->mindrop_sequence, arg->minattribute, arg->prefilter, arg->postfilter, arg->filter, arg, arg->strategy, arg->compressed, arg->shared_nodes_map, arg->nodepos, *(arg->shared_nodes_bloom), (*arg->unidecode_data), estimated_complexity, arg->skip_children_out); len = write_tile(&dc, &geompos, arg->global_stringpool, z, x, y, z == arg->maxzoom ? arg->full_detail : arg->low_detail, arg->min_detail, arg->outdb, arg->outdir, arg->buffer, arg->fname, arg->geomfile, arg->geompos, arg->minzoom, arg->maxzoom, arg->todo, arg->along, geompos, arg->gamma, arg->child_shards, arg->pool_off, arg->initial_x, arg->initial_y, arg->running, arg->simplification, arg->layermaps, arg->layer_unmaps, arg->tiling_seg, arg->pass, arg->mingap, arg->minextent, arg->mindrop_sequence, arg->minattribute, arg->prefilter, arg->postfilter, arg->filter, arg, arg->strategy, arg->compressed, arg->shared_nodes_map, arg->nodepos, *(arg->shared_nodes_bloom), (*arg->unidecode_data), estimated_complexity, write_children, arg->skip_children_out);
} }
if (pthread_mutex_lock(&var_lock) != 0) { if (pthread_mutex_lock(&var_lock) != 0) {
@@ -3335,8 +3387,16 @@ int traverse_zooms(int *geomfd, off_t *geom_size, char *global_stringpool, std::
size_t zoom_tile_size = 0; size_t zoom_tile_size = 0;
size_t zoom_feature_count = 0; size_t zoom_feature_count = 0;
std::set<zxy> skip_children_out; std::set<zxy> skip_children_out;
bool was_dropping = false;
for (size_t pass = 0;; pass++) { for (size_t pass = 0;; pass++) {
// Tiles that were skipped because an ancestor truncated its pyramid are
// revived on the pass where this zoom starts dropping features, and that
// is the pass on which they have to write out their children.
bool is_dropping = dropping_features(zoom_mingap, zoom_minextent, zoom_mindrop_sequence, zoom_minattribute, drop_by_attribute_descending);
bool first_dropping_pass = is_dropping && !was_dropping;
was_dropping = is_dropping;
std::vector<pthread_t> pthreads(threads); std::vector<pthread_t> pthreads(threads);
std::vector<write_tile_args> args; std::vector<write_tile_args> args;
args.resize(threads); args.resize(threads);
@@ -3418,6 +3478,7 @@ int traverse_zooms(int *geomfd, off_t *geom_size, char *global_stringpool, std::
args[thread].shared_nodes_bloom = &shared_nodes_bloom; args[thread].shared_nodes_bloom = &shared_nodes_bloom;
args[thread].skip_children = &skip_children; args[thread].skip_children = &skip_children;
args[thread].skip_children_out.clear(); args[thread].skip_children_out.clear();
args[thread].first_dropping_pass = first_dropping_pass;
if (thread_create(&pthreads[thread], NULL, run_thread, &args[thread]) != 0) { if (thread_create(&pthreads[thread], NULL, run_thread, &args[thread]) != 0) {
perror("pthread_create"); perror("pthread_create");