From 10f7f0a3c26e14ee97fbc55166b214d66c5c9111 Mon Sep 17 00:00:00 2001 From: Erica Fischer Date: Mon, 13 Jan 2025 12:25:00 -0800 Subject: [PATCH] Support joins from sqlite tables in tile-join (#308) * Sketching out sqlite options for tile-join * Enable sqlite3 serialized multithreading * Fix some unnecessary round trips from std::string to char * and back * Gathering join keys for sql query * Make a query * Actually open the gpkg. Fix the query quoting. * Actually do the query and get results back * Join the attributes onto the feature * Set matched if the sql join matches * Add a flag to get the feature ID from the query * Observe attribute exclusion when joining from sql queries * Add a flag to exclude all attributes from the tile side of the join * Make tile-join bounding boxes reflect feature bounds, not tile bounds * More tests * An empty tileset has empty bounds at null island * Gather the results from each thread *after* the thread finishes * Missed a test * Fix accidental inclusion of the top left of the tile in the bbox * Case smashing and prefix trimming in the select * Add test of sql join * Make the join column option a join expression option * Fix antimeridian adjustment. Z0 can't wait until the end of the tile * Checkpoint on accepting multiple joined rows per tiled feature * Adding the joined attribute should be per-feature, not per-attribute * Forgot to update the test. Order of joined attributes has changed. * Get the attributes back in the right order * Add test of sql join with limit * Allow multiple tile features to have the same join key * Update tests for a country name with two distinct geometries * Update version and changelog * Forgot to mention the bounding box improvements --- CHANGELOG.md | 5 + Makefile | 14 + .../feature-filter/out/filtered.json.standard | 4 +- .../out/places-filter.mbtiles.json.standard | 4 +- tests/join-population/concat.mbtiles.json | 4 +- tests/join-population/joined-i.mbtiles.json | 4 +- .../joined-no-tile-stats.mbtiles.json | 4 +- .../join-population/joined-null.mbtiles.json | 4 +- ...d-tile-stats-attributes-limit.mbtiles.json | 4 +- ...ile-stats-sample-values-limit.mbtiles.json | 4 +- ...oined-tile-stats-values-limit.mbtiles.json | 4 +- tests/join-population/joined.mbtiles.json | 4 +- .../just-macarthur.mbtiles.json | 4 +- .../macarthur-6-9-exclude.mbtiles.json | 4 +- .../macarthur-6-9.mbtiles.json | 4 +- .../merged-folder.mbtiles.json | 4 +- tests/join-population/merged.mbtiles.json | 4 +- .../join-population/no-macarthur.mbtiles.json | 4 +- tests/join-population/raw-merged-folder.json | 4 +- tests/join-population/renamed.mbtiles.json | 4 +- tests/join-population/windows.mbtiles.json | 4 +- tests/join-sql/bboxes.pmtiles | Bin 0 -> 35150 bytes tests/join-sql/countries-limit3.pmtiles.json | 33 + tests/join-sql/countries.gpkg | Bin 0 -> 73728 bytes tests/join-sql/countries.pmtiles.json | 29 + tests/ne_110m_ocean/join/joined.mbtiles.json | 2 +- tests/raw-tiles/raw-tiles-z67-join.json | 4 +- tile-join.cpp | 623 +++++++++++++----- version.hpp | 2 +- 29 files changed, 564 insertions(+), 224 deletions(-) create mode 100644 tests/join-sql/bboxes.pmtiles create mode 100644 tests/join-sql/countries-limit3.pmtiles.json create mode 100644 tests/join-sql/countries.gpkg create mode 100644 tests/join-sql/countries.pmtiles.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fd1024b..78bf5b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.74.0 + +* Add the option to join attributes from a sqlite database in tile-join +* Improve tile-join's tileset bounding box calculations + # 2.73.0 * Correctly clip features down to nothing when the clip region doesn't intersect the tile at all diff --git a/Makefile b/Makefile index 2f522d00..fc40467b 100644 --- a/Makefile +++ b/Makefile @@ -578,6 +578,20 @@ join-test: tippecanoe tippecanoe-decode tile-join ./tippecanoe-decode -x generator tests/ne_110m_ocean/join/joined.mbtiles > tests/ne_110m_ocean/join/joined.mbtiles.json.check cmp tests/ne_110m_ocean/join/joined.mbtiles.json.check tests/ne_110m_ocean/join/joined.mbtiles.json rm -f tests/ne_110m_ocean/join/ocean.mbtiles tests/ne_110m_ocean/join/countries.mbtiles tests/ne_110m_ocean/join/joined.mbtiles tests/ne_110m_ocean/join/joined.mbtiles.json.check + # + # Test sql join + # + ./tile-join -i -f -o tests/join-sql/countries.pmtiles --join-sqlite tests/join-sql/countries.gpkg --join-table countries --join-tile-attribute ne10-admin0:name_en --join-table-expression 'lower(country)' tests/join-sql/bboxes.pmtiles + ./tippecanoe-decode -x generator tests/join-sql/countries.pmtiles > tests/join-sql/countries.pmtiles.json.check + cmp tests/join-sql/countries.pmtiles.json.check tests/join-sql/countries.pmtiles.json + rm -f tests/join-sql/countries.pmtiles tests/join-sql/countries.pmtiles.json.check + # + # Test sql join with limit + # + ./tile-join --join-count-limit 3 -i -f -o tests/join-sql/countries-limit3.pmtiles --join-sqlite tests/join-sql/countries.gpkg --join-table countries --join-tile-attribute ne10-admin0:name_en --join-table-expression 'lower(country)' tests/join-sql/bboxes.pmtiles + ./tippecanoe-decode -x generator tests/join-sql/countries-limit3.pmtiles > tests/join-sql/countries-limit3.pmtiles.json.check + cmp tests/join-sql/countries-limit3.pmtiles.json.check tests/join-sql/countries-limit3.pmtiles.json + rm -f tests/join-sql/countries-limit3.pmtiles tests/join-sql/countries-limit3.pmtiles.json.check accumulate-test: # there are 144 features with POP1950 in the original dataset diff --git a/tests/feature-filter/out/filtered.json.standard b/tests/feature-filter/out/filtered.json.standard index 73fb672d..c391431c 100644 --- a/tests/feature-filter/out/filtered.json.standard +++ b/tests/feature-filter/out/filtered.json.standard @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-180.000000,-85.051129,180.000000,85.051129", -"bounds": "-180.000000,-85.051129,180.000000,85.051129", +"antimeridian_adjusted_bounds": "-100.019531,0.000000,0.966797,0.966751", +"bounds": "-100.019531,0.000000,0.966797,0.966751", "center": "0.000000,0.000000,0", "description": "tests/feature-filter/out/all.mbtiles", "format": "pbf", diff --git a/tests/feature-filter/out/places-filter.mbtiles.json.standard b/tests/feature-filter/out/places-filter.mbtiles.json.standard index 9936c097..ab8b5a1f 100644 --- a/tests/feature-filter/out/places-filter.mbtiles.json.standard +++ b/tests/feature-filter/out/places-filter.mbtiles.json.standard @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-175.781250,-42.032974,180.000000,64.168107", -"bounds": "-175.781250,-42.032974,180.000000,64.168107", +"antimeridian_adjusted_bounds": "-175.220947,-41.302571,179.217224,64.168107", +"bounds": "-175.220947,-41.302571,179.217224,64.168107", "center": "-62.578125,17.307462,8", "description": "tests/feature-filter/out/places.mbtiles", "format": "pbf", diff --git a/tests/join-population/concat.mbtiles.json b/tests/join-population/concat.mbtiles.json index 28a2a402..156596d7 100644 --- a/tests/join-population/concat.mbtiles.json +++ b/tests/join-population/concat.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-180.000000,-85.051129,180.000000,85.051129", -"bounds": "-180.000000,-85.051129,180.000000,85.051129", +"antimeridian_adjusted_bounds": "-122.255859,37.718590,-122.080078,37.857507", +"bounds": "-122.255859,37.718590,-122.080078,37.857507", "center": "-122.104097,37.695438,0", "description": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "format": "pbf", diff --git a/tests/join-population/joined-i.mbtiles.json b/tests/join-population/joined-i.mbtiles.json index 35a15fd7..6d8dd763 100644 --- a/tests/join-population/joined-i.mbtiles.json +++ b/tests/join-population/joined-i.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.857507,-122.255859,37.926868", -"bounds": "-122.343750,37.857507,-122.255859,37.926868", +"antimeridian_adjusted_bounds": "-122.309418,37.881357,-122.280579,37.900865", +"bounds": "-122.309418,37.881357,-122.280579,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/joined-no-tile-stats.mbtiles.json b/tests/join-population/joined-no-tile-stats.mbtiles.json index 6f7cde11..4e5b7ec0 100644 --- a/tests/join-population/joined-no-tile-stats.mbtiles.json +++ b/tests/join-population/joined-no-tile-stats.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.857507,-122.255859,37.926868", -"bounds": "-122.343750,37.857507,-122.255859,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.874853,-122.280579,37.900865", +"bounds": "-122.343750,37.874853,-122.280579,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/joined-null.mbtiles.json b/tests/join-population/joined-null.mbtiles.json index 8339f194..c8a71676 100644 --- a/tests/join-population/joined-null.mbtiles.json +++ b/tests/join-population/joined-null.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.857507,-122.255859,37.926868", -"bounds": "-122.343750,37.857507,-122.255859,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.874853,-122.280579,37.900865", +"bounds": "-122.343750,37.874853,-122.280579,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/joined-tile-stats-attributes-limit.mbtiles.json b/tests/join-population/joined-tile-stats-attributes-limit.mbtiles.json index 37b5326a..c6f6d52c 100644 --- a/tests/join-population/joined-tile-stats-attributes-limit.mbtiles.json +++ b/tests/join-population/joined-tile-stats-attributes-limit.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.857507,-122.255859,37.926868", -"bounds": "-122.343750,37.857507,-122.255859,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.874853,-122.280579,37.900865", +"bounds": "-122.343750,37.874853,-122.280579,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/joined-tile-stats-sample-values-limit.mbtiles.json b/tests/join-population/joined-tile-stats-sample-values-limit.mbtiles.json index 4b217de0..b3ce3787 100644 --- a/tests/join-population/joined-tile-stats-sample-values-limit.mbtiles.json +++ b/tests/join-population/joined-tile-stats-sample-values-limit.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.857507,-122.255859,37.926868", -"bounds": "-122.343750,37.857507,-122.255859,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.874853,-122.280579,37.900865", +"bounds": "-122.343750,37.874853,-122.280579,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/joined-tile-stats-values-limit.mbtiles.json b/tests/join-population/joined-tile-stats-values-limit.mbtiles.json index 9fa16025..7999ec3c 100644 --- a/tests/join-population/joined-tile-stats-values-limit.mbtiles.json +++ b/tests/join-population/joined-tile-stats-values-limit.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.857507,-122.255859,37.926868", -"bounds": "-122.343750,37.857507,-122.255859,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.874853,-122.280579,37.900865", +"bounds": "-122.343750,37.874853,-122.280579,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/joined.mbtiles.json b/tests/join-population/joined.mbtiles.json index 16ad55ee..4d416618 100644 --- a/tests/join-population/joined.mbtiles.json +++ b/tests/join-population/joined.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.857507,-122.255859,37.926868", -"bounds": "-122.343750,37.857507,-122.255859,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.874853,-122.280579,37.900865", +"bounds": "-122.343750,37.874853,-122.280579,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/just-macarthur.mbtiles.json b/tests/join-population/just-macarthur.mbtiles.json index 97b1b7e1..c1a48b33 100644 --- a/tests/join-population/just-macarthur.mbtiles.json +++ b/tests/join-population/just-macarthur.mbtiles.json @@ -1,7 +1,7 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "237.656250,37.857507,237.744141,37.926868", +"antimeridian_adjusted_bounds": "-122.294655,37.694688,-122.103424,37.833649", "attribution": "macarthur's attribution", -"bounds": "-122.343750,37.695438,-122.104097,37.926868", +"bounds": "-122.294655,37.694688,-122.103424,37.833649", "center": "-122.299805,37.892187,11", "description": "macarthur description", "format": "pbf", diff --git a/tests/join-population/macarthur-6-9-exclude.mbtiles.json b/tests/join-population/macarthur-6-9-exclude.mbtiles.json index 0e191929..f079ea24 100644 --- a/tests/join-population/macarthur-6-9-exclude.mbtiles.json +++ b/tests/join-population/macarthur-6-9-exclude.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.439974,-121.992188,37.996163", -"bounds": "-122.343750,37.439974,-121.992188,37.996163", +"antimeridian_adjusted_bounds": "-122.294655,37.695231,-122.103424,37.833107", +"bounds": "-122.294655,37.695231,-122.103424,37.833107", "center": "-122.167969,37.833010,9", "description": "tests/join-population/macarthur.mbtiles", "format": "pbf", diff --git a/tests/join-population/macarthur-6-9.mbtiles.json b/tests/join-population/macarthur-6-9.mbtiles.json index 3f79712d..0452f638 100644 --- a/tests/join-population/macarthur-6-9.mbtiles.json +++ b/tests/join-population/macarthur-6-9.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.439974,-121.992188,37.996163", -"bounds": "-122.343750,37.439974,-121.992188,37.996163", +"antimeridian_adjusted_bounds": "-122.294655,37.695231,-122.103424,37.833107", +"bounds": "-122.294655,37.695231,-122.103424,37.833107", "center": "-122.167969,37.833010,9", "description": "tests/join-population/macarthur.mbtiles", "format": "pbf", diff --git a/tests/join-population/merged-folder.mbtiles.json b/tests/join-population/merged-folder.mbtiles.json index 86337ba1..3e5f1cd4 100644 --- a/tests/join-population/merged-folder.mbtiles.json +++ b/tests/join-population/merged-folder.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "237.656250,37.857507,237.744141,37.926868", -"bounds": "-122.343750,37.695438,-122.104097,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.694688,-122.103424,37.900865", +"bounds": "-122.343750,37.694688,-122.103424,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420-folder", "format": "pbf", diff --git a/tests/join-population/merged.mbtiles.json b/tests/join-population/merged.mbtiles.json index e2106996..17d42609 100644 --- a/tests/join-population/merged.mbtiles.json +++ b/tests/join-population/merged.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "237.656250,37.857507,237.744141,37.926868", -"bounds": "-122.343750,37.695438,-122.104097,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.694688,-122.103424,37.900865", +"bounds": "-122.343750,37.694688,-122.103424,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/no-macarthur.mbtiles.json b/tests/join-population/no-macarthur.mbtiles.json index 80a22b9e..5d33240a 100644 --- a/tests/join-population/no-macarthur.mbtiles.json +++ b/tests/join-population/no-macarthur.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "237.656250,37.857507,237.744141,37.926868", -"bounds": "-122.343750,37.695438,-122.104097,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.874853,-122.280579,37.900865", +"bounds": "-122.343750,37.874853,-122.280579,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/raw-merged-folder.json b/tests/join-population/raw-merged-folder.json index 2c7607ce..a61b2cee 100644 --- a/tests/join-population/raw-merged-folder.json +++ b/tests/join-population/raw-merged-folder.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "237.656250,37.857507,237.744141,37.926868", -"bounds": "-122.343750,37.695438,-122.104097,37.926868", +"antimeridian_adjusted_bounds": "-122.343750,37.694688,-122.103424,37.900865", +"bounds": "-122.343750,37.694688,-122.103424,37.900865", "center": "-122.299805,37.892187,12", "description": "tests/join-population/tabblock_06001420.mbtiles", "format": "pbf", diff --git a/tests/join-population/renamed.mbtiles.json b/tests/join-population/renamed.mbtiles.json index dc74bcfb..58bcc153 100644 --- a/tests/join-population/renamed.mbtiles.json +++ b/tests/join-population/renamed.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.439974,-121.992188,37.996163", -"bounds": "-122.343750,37.439974,-121.992188,37.996163", +"antimeridian_adjusted_bounds": "-122.294655,37.694688,-122.103424,37.833649", +"bounds": "-122.294655,37.694688,-122.103424,37.833649", "center": "-122.167969,37.828608,10", "description": "tests/join-population/macarthur2.mbtiles", "format": "pbf", diff --git a/tests/join-population/windows.mbtiles.json b/tests/join-population/windows.mbtiles.json index ef2dcc14..570b1710 100644 --- a/tests/join-population/windows.mbtiles.json +++ b/tests/join-population/windows.mbtiles.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-122.343750,37.439974,-121.992188,37.996163", -"bounds": "-122.343750,37.439974,-121.992188,37.996163", +"antimeridian_adjusted_bounds": "-122.294655,37.694688,-122.103424,37.833649", +"bounds": "-122.294655,37.694688,-122.103424,37.833649", "center": "-122.167969,37.833010,10", "description": "tests/join-population/macarthur.mbtiles", "format": "pbf", diff --git a/tests/join-sql/bboxes.pmtiles b/tests/join-sql/bboxes.pmtiles new file mode 100644 index 0000000000000000000000000000000000000000..12c79e0fd9b28fc90694ab9a6131d1251bae4fe1 GIT binary patch literal 35150 zcmZU4V{m3c!)$EZwr$(C&5do_$&+Mb+qP}nwv&yscfWeyukNk7^QU`yx~8V)oHNsP zYDP(3&H9(Q3upk)e+uq@G4sDD4*DOm_h0<44D`Qshp_)B!2fOd{}2S^eL409}a42X5_m!G7JTzVf0K4`A_`6O>yc|_FFtiJD;>*_T9+n z{B&y^`|?|E%K7b86i8G~bgcWL5ve_A|mI#&4hzdGWm;8T}!%;obXGKz2lK@-kB*lxqB2fTeVqk)bs!K;!O zB4+O-Pj?nr$eBjnk!zL`aSiya1k7C3uIssoPZFz9JN?X?=}%Ld%%LKKr|gnQTI#fU zE(;TySs&L)I4KpFs6<*i+O+n+hyV7j3xgNj@4AEFLhyt1cMS;+wOS{q6CTNPQE`y-GYogh3h@F)!MJkb)n-a zAetgl(*L%MPzC8r{)2WJVa*5vfhngMD+n3iT{%P!R|KHY&ZxuGmeVv(PSZ}SJ1W$p ztSZ#1Y4TJjn=(l%*=X0!WQuIha2$!95_4=M_IMIK{LB%*7lP(zh(XKSnA&<$2!vys zy+a@Fcjy^>A8>@%mcf<;xRPd9Xc-I;;QPbz-zSMa5F!{0Yv$LTPBmW+HXos2|JhPH z!c>D^bHPYiOAb&-LyWekICG1_MXnPLYLk5a6o|+6?$$pH?P}-7Y1lJhzU?7d!+Utn z3qIG38Xbus+E^lb-49iX@cRtS+c5vbxI++ClnwF!enhc7kiKe?nde(OHqgtFTu0FS ztXBjU9{d9)mX_{kx3lnFuRR_^TOLn6ce{R^GyFFAEeJWs0y~3?@+73r@gT&FKtVzz zG3$X&@m#?J95U+=A@ZZAeg%UU90Zenc^7rPm47Q7D|^w)sdu!TI2n_{;O`5d0&**k}FmO8vnp`nU7hA?;^o zp%3+Zuk2z^;;U`4Frez(aIz};v9hqclB%v!=Of*@8b|T3O0PP=K|T7XWV?!Lk7lwh z`mv@EuL|d%msy5eg1V0C1f@i;5FiV1jvBo%oFtd&5Tj22kEm8;#s4k)pSp^COL^&{ z#?ngV1<#6Wn2n676nh^Q5wh9c6mBMftby3Z#f~uAI6`OM#c7TAyOSH1= zXt_Icr*mvAUJ>osIS+HCW^64|k@v`X?Z_Jsz#;ZAP8r`+tqW_VHqE(?yftlIr_hGl zkG{l%$|J(rgNiX?kr%LAz`_eC60H{H=5Y;s5Q{5&KB7K(SNbH&{4PeJsF0ARu}pTBQ;C6*X9*o6<4Uaxwi0hD&Xsv#n-Azwv-p? zEHA8Fa`CSe#@A{{lGha_uPIw!m1iq1u(JYu!WYe`_TWmKs8>t6_EqThY^2jC_f>8g zPHt87^=Lw5%^fS?7*YpHuDaAv(w&_u$QW93Wp7SZy!0v^(jF&O474k)|ARQ%+LPq6 z9cI+&bSKFrdX28%bozp_ZWD{CE^0-1`t*{&;wMvzwp{?Fxr@uH*q0ThFM!_MOD#pO z6;)dK{6_hFT0VI6I6uVZ&8w$*V1MfuE6Vc7%+cgE zpA=&Q4!?$kGNaLIOo>EaVoodHB5=4j`=xO%&m(jTH%nDC6Dad}q{*uk7D}q#Ggsn@ zn~MO85VFyN*&#QpkBazzl}NX=RaUi@E!Yv%OTt~srRPg*+r4a9T!(Dmen#kM?5m?~ zaGR(ZM&Czlpd-1nL+h|*ovFzEa|`q200EWn5t)=}CwR_>5w+U5W#QHGDw8_5MlIMj z1DkZ{Z5)!=+$&!x$Ak(O#TBgxXZ!c`?ks9hW9B8Y>Q3#Z@oiME4;&`~0`g8rSCKKg z%Tb5h=ts;5D=0`FROWo*no+Enc|skObT%bo>d^r#U!vS(gatz3b)_B45Ov2)GM{}4>KhF_nv4@{X(2#)vJ_B(bE#5+l}dLa_8xJWE~BKavTaep zbIO9ZCRM<;x6DI3D~K2UhXQo7Bm*9W%+vPM!e^*w!Jus?eF7NOGl3q_A3!hZ0SuoIY}t#%4@wdRPhmA_V2 ze&6EVgj+$I@Qkqc6s&h0tP}BeDVFo!Py*>*OMU?u(#%D3PJ}b?6)~qqqoydz+c((s zd9qFbeDRIhJ{*!IvkFG!Tj_XveR|o+$H~jp-1g(yQ%-SeMc($~A5?!GO;^Ua;>TqC zq9;}6Gy+6|?fbzvSm8Oo*_;_2%y1W>elP(?hn-B>`d{)o(47RXriY8t3V8~r zYe$T((|A7?OOBNL9>M5kuJ0F0x5Nc1v>}*az^5FJhGB^$#Ocy1YG|iZaL`KDS>V;R zNO;7>Tn(y_tvKMEdlK5~%#>Lc1;AfaNH%!c5<)oatB6=7EbQFe=p%564_|(MDj-*Ca9Lf7?58%Q^ zFrg6SN;?BkI@Z+3T9LByB(~DRWD3W;TI0nrXe z!A*gm8G=M%A!Nm6JMR~R+>0g*&ffbOYsU%C+I^Qq!rE)9LlF{H! zOOA|O5pl0pJ&7^DE|*GgueX zDq&smeLcEF7+tdOG>{!0b$L-jCEkD+$s}@xrq!K0kzKdXyu*OErRILO6S3KU9DqW9 z1%-6(jUmVKj0aq%vIUWUT2p4GsbrV9PDay6pFi4JAqfQ!Ah1YV|6Z)TJsb-V8dqHxvib+H0lKop3?{dnz+3Jv&s3D`#7ylG2$* znos&fRYhdIlJ=z7On^sxY>TEe)}X0$w+VqHwp|nz2Gp}%FoDq2{MN}+#L{aCKz|t0 zO)_XmT0s?C`NL|y_pc(G772C{Ty^Ou)sye>xdLgJV%g6#z0H8Y63`Gx&g5m*raE(M znVuycpRPa2Y%X?p#yBB8E@|+!216=a`@4e_A5S8O|SSHe6=&q1nF0^@Ls)ZG=#yJuvuM!}2yxeT^GUZKf zueFGsF0?2BI_j!aFaHQZ^GNeF3O7o2LZovPEcY47-z|Tu=qU0R20T3u#OIK?DPHKd zMwPtJOOf4d?e_hSB6Sfb08Mc>;E}rFV{+-D8tAXqXK|kusL-9Kd&JkOuXF%j@i2OK zb<%`v#YcWexHf=qi!bhBEpc^K4S2-^K%}=sV&Lfcqh-bv%hP`Rxx>>Xz%fx5r^l-% zTh)n!7s0D*hGtSTSg=5@4p|=Trma_V<4Jjl%;o?u6&%=o?-ct}IREryDBiWdKE`e;YC z8K(hS4XY%k7?>5B6AZv;;g~U@uoLg-rn2ZMq#uzem@yx>Lo+Et!nJc$rV?(!Pv?Z4 zhVzVZLc6h=4c3)4={AT`w5InW4BarRv!gDxN#*F1jnrn*qbn5;Fj~+c;RNKkExKjr zL-5V^aGXFO@U81bi~zc^-uX%BBmX_~WQQTln2ig$X=D=!UZyDQ+!N_$Sn;e8->!Cs zNJw$nM#~}S$E$IL5G_~&n{ZoY+14K6=)$^?HR`FWb!_r@kJR4_&{VsNB>L$&#Ii9W zF$f7LPg~`rjkW+4k^~^fzRh8N0z}ZcC>v&mVID8g!_KnNr9WV}*|;d`d9h`b>-!@G zzv~xzq#K|F6zwv_-MWMUZCPTo!99flq>?aVWr}ef?pb(0GS>pB>k65Neenb-Ee52j z9Buzfx5!n=Aa+N2nTDwZUmHFJ@y+2vwXC*srV-^;68 z?=w`TH@~mRFS1c4GE7ZDclC7ORMAH4h?AN;X(DK`QCQonUn8@LQkFKeFZ-B8^qd%t zVnrgX$rcoZgHY%Sx6)Bedd^d>-?Vj#lMB8Rtq%+iT5+Omgo4+JAv(9lbIGHw36jSN zaHIEIV7hW!aLl}#>LD`vcc3&<aKUY2sl$`BojKUl8Gj-03I?#&pCZ3D)1(JRO*~Bc z`dPJLDL@dV)T!V%wOo5XAGP4Adh@0zlr?8Bu~hqL)mBxvv1%vVKHaeSD?wz5?x;;= zJMiEqaNTkxvtIb>sucr|fsgv)m}`-29vA2T8rrsT!ErQIBP_2)7l5BU%Bf_lZBkO8 z%mI`$PWc*E;_4g31pOMraaKjkjiKe%otlN6AUfqO-cGEw zO?2*+Ev{$?jRanxII2#Ya=&G^O#JmIQ&yW6!p)KB|medG1|PtSUo4}S*}=Dumy zhP>z25>9PopxuV#XFHU|6zXLu)UOFUSMsDE_{aPx;Qdfy_uwf@{*mKi+*PDbyvRJ( zDejndq6Lj#o6txeNK@0^{&{dEq5Op4#@|0{y5=aoGPaS(;NJnHb=>Zon3?MqWMh~k zHPV~85e^(FwSw!#yV`>oq8?|LEGGVf?Z`b9Nn8LefEFD;3d<4e7*JYU z@LqJ;uR_+c(H-v#iui0k35Z*CYbhJz#GU*MFf-gkwE?;9guH7fu_1HM0%7i|TG=sO zn^N6aNwH2B0%|zszAGP?g?V<>w4HCpd*W$UGFF68N&3mUoy(A>y$yS?;ArroK>Eg5 zJa<@H(@rrSC*D|NK1-_$tJ+>p`bOVMI|}me9l=PiC{!%mK9Iro#bmaa@EX`*NgOa& zc433N($U@aIPfbj!IR{KHUA^{i~MYB+=4^J=W%U$O*Z_3hMrEZ>W=b$q1lN2&;Ok#AtH5>;vX~GZJlE5H16({2ef5 zZ~=29^?spV(PL7}O8_O)Zd#D9Zg|Q-sIB><4;SHY`KKK-0pdV+%)`Q6p={7~cy+j$ z(DPa&p!5wkOcSCrGcP7%qCVm2ZlrqE6{Y|-=N3D53Z{M+qBTg$7M}4b^&p;wLX&7y z2+TXDayc0CBkS-6l7A1(2^B9gk{(c8CbeGMjK(4~kxv@!1_9wLjm1`2 zV+;b~K|v0}wzR*Hhj7fo1GI|)?s!;GQVLZrYZ|ylj~7@=`9THoE7f{5O33HF+@tA; zBr4m=GaJj61tAOQ-WV5u!@j;cyo8qkM_Fjr5GxR`z>O|`;`RLBKKxTJ%@ z%UKLCv)NnB!8*@b*llxRw!bztI^FEmmh|ELRoZ`z{XIi_-wy)nlJ;V^y|mV&3pN}s z0C8#;DEsPBSJeM?y8E5-Kotz~^oeOVxZ@8;m&sh;tPjZduwL39hzp@Db29STyA^E& zqp#_KY&-B5goMcK^e($S5rQnn&Th^Hv5R4*b!us4@A>5Yu*56q@1zICO*ni&fZ(tX zZycxjl>r4rKx33&luEND7lc3K$gXI#Ac2pS-7aO&=~7Zw@h=Q_T1@`-+fnKpklzvq z+UhJ31&_bPqRXNJ26L~w-KkFofsB7v*r8+C;WS6r!&Fpds?YDK^GPj71pDYZ8}Cm( zQDU8-yBS7pSg|7kdLM_a-d*VmaNA>;Y_4wZ60mI>~$DKLg&D#O>SQ)(v@SP0y zXIJ##Klymv6!ssUbc%m!;$38QiWwNKoc5YS@&~=}datQ$d~h-b@^0+;3W4RHz1-x= z1=%%0XkOp!G(>~=@Omd)~MEEtg#@U>-yxs&I=$1CWT67V68XfXK6BJwGL5r zxLQ$zBg~9c+1V26BEx92GyjI0pzKHb2v>ggSEq{Kav%Xiz~YL>BQZ=OxGhkE=^p$= zf?1aMjG?-Vq{WJDaur?R9sg?|$=+UP)>9b+hFy%iSZ_+l;ashX65!y$jMh@QI7pkX zl%tPC6KNaBj&{>)1f|c;8gf8~uceK;cb_W>S`{izGS- zKZNxDyA`#B1GH~Z+RTn`nNtg5h_2D{VO9!xh-Wbo{0+dT4)BJ5slo(5_o zSE(ox)N;W^9&`1EW*C7$z^I>iEqZmbakyr*P^p`#SOy>SHPXkHZ_SER?7mg0oEX*C z3P8QlCR^lECW0ho3YEt?Rc=dBV?%4vk^dNa$O4EC9OUna5#@fIZ*Qd$>!8Hf%4pb< zEAW({BVz(6m@mAJ7n_fd=mJiV-o4IwM5)Zr^s*9Wm_l<6df|I`p-KvFQ!Ceu#}yV1 zK}3#6*pqQ3iSM^@CK8U6{6nfl=W+GDM`1;+v0&HFU`3qpLCUCviM7Thjrh2zu_pzL zDS!1+tWYG^FOe)G)wG~O-^$V)N6%Miz$xQfIf`A#fr#$&!13HQp7##gqsx62fH#1{Mwzs+wyozTyqaocOsSdQ`$X$Vd{!Zy{m?dGi2cn`vx zygSyHhuJ`WV8aQ|?9!I>#C(!%s23`KIB?2d|V~Rw@wRVp?E6-$tjv%*@#;`3MFV4B|N53%=(2^ z3iG(6jdffDsvTBtyEeHgTE97K{IsMrX!K!btFZnHYBMTwlWT|w`;?<`32C^;damRx zTmU;LOIDcaC3QaT9@-hs0*{wL;{pv$*1Z=EYX{J*j8q)v-hwafoUsA5wtvbD?$mG- zMecGL*Ft)MgRYx5%hrDr)&UjbG*vkyt+-9fmen#y61nf`_j<%sUrawhR9mUPl8iNd zG2A>=txF2bDYU5qcj|LU5-ZG-_s-E+iZ*S2T}{rL%#xueh44rt1GzRqq^*|g$wU=r z)f*G?KC|eka&KnKQ&5@Mcwn&*y>YT1Dph1R;R7~;d}VFov_U6{1Hgpr$k6o+-Q*ZMczE_ z5u9R&=7WqC%I?N-NVb@*yjrP|mK+SPpyxrx7XJuM4QKLNHOvpu3A;!XJ=}gqR%U>W zrZ^XEL@o!)oW&!ER;iM^Et*JI%Jj)Xl{-C5RP64CZgld?T>qB^jk-T|@7y#3IymgD zzyHbGq{KZqc^7DSF;+dP2)afbeJtI&0E}v)Bysoz#;J!!LYwQldnK;878`;t4jgZy zx(;7h^g(l0<9@G&9{&n+wA1`s?&a`eZ;W{vu>^j?1142_2aTwyR|_N zb}O1b669{|E&vA`Q%1;pyl!{yx?uft8&Q?E@jV*HWxefC~a9xd=4Ie3*~!0 zHAOMda`)hv+rwI1%b*Yy^5S$ANxH2}9hNv6mVysfvb=dUFCPyeHkW6V#Js*+*@<56 zZW15<{f46-``9Nyi1_qyGxofsLvSGYrC+>N&43dveS)KyVqv~e_LtnIo3Te z5kX8eC8;^8KP*m_OwGpvv*wd-gX$gPveN)^Hc-W)!9OCGb;Y* zU+o{ZudoEbgeAB>{7D|~3wZ1f+UWAJ{Dt=ZGRF^=)amQVs>|br8Zk(VaK*0;S9f{4 zBZxg1LYqm|?M9-8F-iJ<%RT?|;s3{@Zs@1z2@++n(hq`yA&lb2a2^H#g?PQO%4=)9 zS6xKHx8A60NI29#r^j9&_c7GJZI}Ls^Qe?)&C-avmC=0wo|byMmEg5W&8z+dgW+1A zO>`NNVX4nLcing7zP9z}6#Z@b!CZs{a~Y4>m1>@NJ)XgnNzo1RY0JcC#?8XIv!oxQ8mC}^0LQzYeI zsUUy8>2@i7o`wD` zjQo9@$r1S=lD$%Ncjai`F3kZ2A)IM%Q2pfr^YZKuEn!{7v=p(fIG9~d5%4{T6R7!# zA{D76SQsZHDqmW)z&d2!4p}bHe5w385LDE57X<2Ljp_jvSTm*KYdyTV_r+#D;t?<^ zaNF{8q@*mR4JvmtrKF}SdUo?f68++S4}->By~}Ff2v~ZcZ#wxJg(iFQna*tbKBD7} zRKA4AX(>!M?x0)3uiC^i-gYdrx%{+Ev{Np=$M~v0^C@Lk%qsf|Q8GaZ^5yU7Xzgqb zRisGMn`*9)#T|NHILh5)MDak<3m`7e(=LYd;^E?Sy_TsCguFCtAUmL1%@db}6xNnj zxuQ?_1^A_#RHlU zB-mXHnyHS3#S1*?gU5m9ln1dtkN4ual~3Hw9KVk8snHX>x0>_g5O_$_ zDk~;1c(Pspv5CW}kr8dfilDWbx5KQ9H>0bd?rfa*td+|QrcDX2+a_l_-=ET0%pAPR zFTLj}Pdb|~2ZrQY4NTKp(Kiro^R>adu@vb_MwftpP9l?o9we%VfRERU@Mbfiu4H$X zAcD^VYhV(Q7iF+8Cx&)-2%?ZT)z$W#x8gnEoB5VpRZiD_Ui1vzLwU8i??C9}ZAVuE z4+mQN;ycuXUF{t_?5`(>Z-2Qc2fga87O#;(BroZqKeeEJU}<5BS)YJd4aD!qr}7uZ zVQ*G@UInTAEeI8JC)ZmOeKi}gYwf!)c^no#7Pmh*8|5tDwCr<~yB&5BbbvpUP#9QB zK<^&}-Y>7po05EXlusOCu>K6OcucuyBc>y>EQc+vMUfi<=Y7-*cpMtVydjw z9fIvV7LO1|R)yY{9qCzyqHeZSz-Qg8%TZNM44?)@uX}6}4WxNcYPz<{M}bh(hEQAR zq5(GwnmEF1Dce_0S}1Z-J0jz&WUERpuWU^@8t;v&Ay(Rm!r@g1f3oD;J9=!f!Sk(b z&qs2cusKt!3gHCVQVGVOj zrz5k4YwD`F)R!mKN?0%Ss#{r(mJ_3Mzv-+Pd*sJOLDikdW7bxBXdc43DxGZzgY+Pk z7LIUclvAco+v7!6_LWc47jY5c z7bEjw+Rk8jQ4>smS1fXG7K5W#A3|yl4SHza4Q{kMf}=Ai8x?TWL1P@YuL`Zin)}6g z{Gs0fvrYOzkC0mFu5^%>CFZik7O-7xEp^!S)eB98F%(|C;!vaCjEvFIq90mBPFpMw zc9Xjj)|x-SY4X9ZIBp_pr!Z09%l6Y^bBOOQ9Vx@}%!r_oLY&HN9eMKwt3Fm9!v!`P53vNDPPJp&0rar0cx5hcmXS9$B!Uaz-#tc;%#ldhQ{ z6li1gM{T<+P^8=$Ug#nQhPmDm{5UcTA%}les{7FgZlO&I}RF^}MEO#5G5a1m8Tz_NY*- zCp@UL85V;im{q1G#~M$P$NyQqqpCcHXTz75r|_|ca=9W`dqOulKV>akn+_bAd)cei z*vrVdF8q<|&DJ34R!QiBTVJ~I;u4ah?&(+MsD~a+)yawS7t`glM)joVWw+P3w@Jap5!49J*QH`oynwP=anT$C`o%dYvN+GjG}6^0 z!8%y#aRs-eg97QT^j~%*}S@Oo@&so+Z8`rHl0}E1_&`isMLyTPQ@PRv~IXAGO6wR7uYw^*f;evy$Dp zgIzG0c5!wlnnq>LoXOPzc~WwLM(AE9GE6^YfQ@gJ#4FWH4mi2r=>!CAaX2B(g$S~5 zQw}42s*$6sfBA74OMa1;(+8azj0Pyj7wiukd!uD}v?3Hb%?PI^OzPnhY5A}QP4CFF ziks8Hiq`9@J`L<;uFTnG!`I_ioQ{E~=6P)t;&Z-AyxkppN#lN|+dBsv{Q2$G)&YL} z7knKFc|dcO)WqHvjml$NTQuMczxJObTcsuK@0pY=7^Pl(>|lgWd%>zDF7pGVk(AuW z4K8CKBW$T;E2=`%MT!xkqOD#1tcBgiL4U6Y;=#BcI&Zlm6lGbhfRSg^-#X;bTEG%l z=Z*f7_-j;}dqsmC?(J76jG)$~I)^7$^RW^?k&2h;K>3m>NQ&JMa9oY9IB*Q#G( zAyFXS^t<`s=RpD1YNb;7js9k49(^uiSeH8z5?Gduj9^lMs1_|WQ4JI54*)GDnjQSsj&5@eCAMwl|7yg z!k;2+G^~}A+FG}J)4NLxHl80ryQZe?L79LO0s~Qxl6LAFZU*-GQ7yZv38%vTEf=)< zy!7rehf-6&Jj@L~$NK=tBEBzL40X-Hlo2exB(_TU(3=-27YP9Qgs3?%Eg*_<5Y`P ziqdEoE0!41&zI^q{~T<>Nd`TwbKd+C-8X3tltS4Ww0;AQQc(m3#dU2x-p|r+zF3M8 zcISA-V|SosRxAspsL#Fv;YUG=<5pO~1naKRXOO;i;euIX-h8x0SNCuuH>Wp0t!01D zvZ#|}L**$~Z`bd_321+(uDq%;k+3p6zSx1pj(a>P(O|J45%-B4i5>}3!1U`OshSqfB;;D-P3h|fPT`!7!prT{r* zLLonM+stC``8m^PK;8r|cF7_?=<`F~f{A%b#_M5d(}-|yJ*IvZLVXYVDu?>R_fU!S zaT=f%?Ak{BVIAC@q_NzK zBU|P2BUeb}=;ufe>B}5i4Kfxwis8h1E|y+FRay9uID*o?+(SLL!;&_j{^ICv8DSp5 zMeoj_G11Egufgj5O`U#+BbZwzCE3P?5L4y^vT}LDc0Yi;U6nC`o}Qq|FLlWf2^qX zAA`Ujo(FXtv`emX$Ck#0g_Xm zz1|$$6>#oRYXD(_=H@8Pj;4eb=8rH}2F+Zcg694caVD-jUt^@llWPe}H0cAh@0SNu zWMx~!Vq13K6|w0NPDNFLexT_JGfgLko&fZ&Z4pUR&z7tC7G2m~H;OLqW#~RgRTo2f z9X9lG2!G4t3l4%CLRsq?U9^vIkQMP>OLPAh*J!hKLGz@%`Z@wW>>FuK{LpzF9hRY- z*TGNuj@+_7*_+CT%OnA%@LU<1idDcMb&3LKUt&DIrY&EuS&QfaS00HxFR*}tpGOIo?>JPR$PyyzV>P%FIO2y9>vCk7t68@5E#KV++L+f~MG)7YMlr$4 z*JoMmvd&+ZxJUC|G?7EMlsvBdlg6<x6MJ}r(FyYH;``pFer3#nhi=bz8_(vy{KD9>=wyUxSohe5@CR}e|;@853>)bid* zd6f~#W8ZM34_OB*Mxcc)X14r>^8Mq3e)5y}hJG#7$$3u!k?;{x2f`)GK;p7D3i)_V|j1iP|p+%%8cq4yk#Pd4C#_RGj$0N$PRrr4e9kJCw z(1>BcwSx@RkPHNfBZR{T{KB_H!lsTey)`b59Y$6`y6{?HcwD;F!yr5eLfT<@;ezOt z0IzE|CPV1*hKw;)^`fIe$8X_PL4pTMnlD@ zZdxYUkyav$BlgFeKY0)*jCL9OVM0qAQjl|CyAP#~(DKWKUTuqS<>&{M-6s?Y)#^zh z;t=?EJaOAR&=|hq%_e4wU(WB2V({PbPaU-VD>#z?dJ9eN=Vy@N!#sqlriL~P(#nQ+ zF-#NKKNky1mz*=i7;VcgZ}?*}E$EDWbqNjboeai;2N=^r-Hf0OhF9UKm4qhMW9kpn z6zgk5tDiPhaLy0Qm~PmS=;5ec*t!aKAjD}7%KXiK)9x$%1wPM3^N7z&PKJg77gm&o zcar`lAcQi}s?VZ14)zR+u04+&Zf#2ogU71foOdQoXMTM@#0$HxbJU&Z6*-aKRKP_S zFVwB}hF8x>GhORZ!MxOeF@`wBrV#f7M4P`H`NTq?tHW*)_~(|kh7DbUt$5_pW7Hbp z5>-(pQfO5XkAQ!bw->!7g;A}27UU>>Su;m6P&qB2E{iX$=kGES2)Pcp;EOZL3N`;O zflc>69I8ixZ#pj#mG^_)bb2*a&~=b4^LIxre7MkgPihasUV6}VwR=OZ-;G;h&h!wu zp97NL4c5hduHb3V1YTfz`ct>Pu*HL|oY@1Zsj!4OCdjtbiX{lrn%@&}_|eP%34t(k zVaFf&Oc?4<)d!E*7L|Ra6>TBHB+x4Uz=uGxX{EmP@`gz|uXpjbEc$I0S=VYjyx$P% znObBd@iK}@CpltEi1e3fPs6Wj9kmWF^A(l}iP3PLsy*<_QE(V@*7e1^a>(I*{B6>T z-aF}{Q^&bPMWMGxpl{^l4Un0RL$WT+?}9qI1d+>!TNncWElwU;HbC3UN*IKW7>mIx znp-6ATku!2IRxw>SJca22|r8uI&I36PsCfwwUQ~fQR;wLS8Q7?*5TL<0XOzgK4eT( zP^C0Bj%kC%E{qsBhh>4GOEu#zdXvj!fh0(}rRjHITrYW?%0ep!Sz?@~1Ln5a!|M-G zkatFOn!nLUqjq;`NZQZ<*of@ipa&JP@Bqk{?Hr2;2q!)`_;XM&$++O95VRvd=}%zX zS$>iO8W~k~T;)N)9VQP+fnZ`+=m;gspsOg%0uuNXu%bgS8X_)zZk>-X3{RU?-DfE_ zkjgUo`7dhOnX<9euOivRL0*Wsk*8tXh*uFPjpAp5%mI;rFAWk@b_R(G^ zTaDm~XhJw2&1+!0XkVo)mFJw(BE2k;Jz-ZCWVLT z)FguRzFseuza#x<{ROf)w2w*Dxyxvf4NE<#r*5F4dK==F;e*8d=&K+-uJ@lvzlJZ9 z{v3*y3c|&y8|r_aN`t6e*n#k!S2aH3KkrAJo$$(#b3WNLoGdxDmvM7K^7k>Aj&uYk zUS{DeKJ8FQjck$fr96meu!ZFA5TOSkB@v*Ea8f+^T;0&suHyCR1ge2=HN3kg`3<~( z5{CH~x^t=^=7Hj!vn0w1S`J>dMd0h2uo%!TbccR=BI>f3G=A;ce2EHRf2$c53&i>S zdWijUr65Zg6Wh$0>KJi>B%^&+is5+_(|-tU;thzK}FyE9gEgujd0P}%}u;X zZX!AS`gdVF(y|3lPV=11&+h6s?Wfpz0t`o%mA;`lx4V;MaA+>Wqf*M0V!h~KL~TXi z9}@%$I-PE#KT2zbJb&4q$98iWqP0E~cM+&zZ(!Y(Wpy!kWCJj~MHr@f3(l(gLO`Xx zHpu^?#Cbxp1t-LpRZ6nPc~3zJNZ|-PYR(ne*z!itS{=EB)56KP@CU$Anb=s zt%sWlLLQ#YiWyKxn!&4j_S%*;tqVG`n|D`>6t(8hwfydm?VJ0^jg#uSVT-Okr1JB`C(wg?ESf>uwqPcM=RI`LYgCNs8W}DuOv}==hDA zSBw2@i|}woGu)pz4)aCHZbB8Y>mpGP(W$w$1*6v?p)A(%6L(-FR!DEr8TV?jf{r5H z;V&O{g*Yj|sR32<$YZIztJJQWzpzfkIZ4p_D))w|k(H36W?vT#Ip9K=PlE%Hkv=DPFi z8(pgIhaqyJJmMMAa4;%G-OEey-P--p-H^A+LTlUnnJ>0v@oAsRekLL|K@eKqTQKXD z>^Cvf6GLix^r&H^xC2$@P~Y6^)O>ryD|<}ZvtvRGpBq{4fOEi&qa0r%7#NQBd)xH# zAbjdGmb(l)U>z8wu>ZHfW(QcXz!>QrC?f&C?j{Rli|I$zJmUxgkiwTjs9;{u4i5zD z_)uQ{hugX05j3u$XM>H&{2&vx^+T=nSq-3B%UC|cXSVrG!-a!R?g z5ik^V60KDzT?b==`&=OHmjP4_&%o38lSLtty4Rqs)aKHsdWV`>3TyFrn$2?`+4AN^0c0Hf^lZ))gbxw%V zuF(Gqto3#HlQNJYi{Sw|^eW)+9X03C{r83PwumgKst6s41EiNuvmY@w5Vz!Ku^%49 zAF4ksEddc+P*1?gM*8hMNxEd7RlyaWH#`~4-ezA?Lg*C<{3{5#UJ^6`7ubg#i5cXN z_5PSde^4UGC=PI&1kFCBM1KJ?%-fJ8&=FL->g6lAJ<7c#Q?X{RfVEA%%iTd&(EZb# z!sDkeZ7I2-kZ@^V)zaSv>5q&e^$$w9#Q^o%#lO$5WoIyb0-JY;+l9}9AuxOXJuY?^ zmUf&7>7nWR{yyz&*$s4 zX%wRpi85%4MKu}S57U5JR$gj@To`V@^II4H8gla>7c}rCVmA-f6d1r2XiII%xgyFq z3I9F%-`Z4dw*1QyPdiptz+gA=Wp_X}lvz4f(#N4y8CQhm|k^ktUUW`iT`R`)<>qMn12aeF+`@kb9LqEKDOLfKw z{cRsgb=I%;Un7|gF~@bYO09_^xxcjyKXAEo$`be=m1q=#{T=@TSCY7& zryON0u3FpxK_SJB=j|Nkee7wU4?k?wPI6Q}WeYCG9!@9olQF*8Q1aDX zyhokOMZWH55W%L?bhL+J-NnJczmSn69wxEVubp(O_k>;_vm@4eqN%@7(zj>gNH5F1Fw^|II_UYr=il3&H$-Tg8*cRXv3sqZO++2~iw47j z^_5vSH}J2AZyp3izU~hb_&vjZ;g#~014dL+8sW;pQ|ND~Sq`PY5Z%JM$<-TfQ1qH& zLp@ov8O0z$!*tO9UtL*|L*R1FKC+j0R%XJ2eSYU#daA^Uhz9eRxYh+q>*agQdQCx+ zRKGm0H6p1hp8sxyTzKV?D-8YpsYp2JP_2RDya641W%Bx(UgId;z=nY*aW(LQB zHs6To{krU}cF_~nluo$f-F4ZRNl&5ea@A8Z#oSW-g2;a^q74XSmH%v;An$Csn&`E? z!)b>@@7{(7HZ=S9BGXFGGD{m3-%U(?h` z!0;Q*MWJP3I*Y1+H5s{?JpDd=4L0V)U-uM#I*ZABmuU z5{MD}630iZ1BMik*6xAK@{Hrb5JBw%tb#OqfJ&)x?iZ1Lw6A2{XPvkwJ?)nn{To!Jrca!u zG-8&(AF;U@(lu=%;A)0u>;EEy#Yy}#n(Qo6R z65=Yc#Klh3RuN>LijdN!FRZU#_!hI-W=sD@Zl*4t>+1O1m@>I!V@H$|J%3g&o@ll} zd1fgBT8$>w9{;FT=%0Ve_h#0wdzy1~r+%-i=A6tp>11OURNZ|Q5PJ`{`hHX-hU9oZ zN*hB8M}&_#p-c-Ok`mNF@v>@<4}w^UYAP~v&r6mEvfP7*1+O=88-^AEmQpXvXQcVjo| zG-Uf*cacY&N8tT>QLwpJ4seWo=bKA1;i*oR%|bjsCXwIW@spKTia4L=c#T%l#Qh9s z{e}0KeWJL_wMRYX`j}wn%7BkOlZfzfmo8bO&%}#S81KvJSB`?1j1_tmIo=5N3*FP- zpX>vUE4|$@Do!}?7uBuix`&H_EhO@jqamfvUwM3cuu__Au48zj$wH89Ni-IU){^n^ zdp_%r1OSqW0m zYw-f)9LUxYQxL2Ki`7TVO3inyevc@=Hl|l>L70#9s`>mAUPn(h46F) z^e0}A1VLIoLWBB~LGEn_lshFv>|*rTFWJ*vOU<(p8Qxi+#6=3BzjEc9&rC*C3@jm1 zkFNNqcjCuB;1YYW|0K4;A+8!M#mS&WiLGP^Lcelk^_?VF zCRXZ}>Pa%zF61nOI)Tacr#wLUzWxgHYpJzmtKA9*KN}wWYuZkqawjJ>y@=uc`c+HO z<)*Tfr;ZCmIb{Bd@5Fy(L!umsqGHLfxM3+MRKGjB@_RagKjTvKtLbXf*_{XO&U*v3 zclc#2K4V|?Vv9cTOe3KSt9ofjsQJy$Sl~Is&7%JGVym-RUBBZGyoo#_$)HNfEnpty zn}WEKK^v>dLA!Uzx<32-TNK$6JXZ6pR9+y%)g2{Q;{U7KI(PQPsTpv#BqLRRw2@O4 zCqctDiyMUY&lwvhV;4qh z-LC<5^0R@KQw&@j;QO(Ya<`ND=#z>f7rU~ihfW4qdE>p|KwjHG7vf5d6@zD_-oRsP zd%`<8Pg-g|AnTJ2jqx1Cq)Z_b?V`Et}gV`I2t zA5s2D&rdM-`%Ad3~*zeEtDgzG5Y*sKWahaqh} z3$JoP_1yY`745}3vV|XXm3(=36a+F>CtZY16YtuoZRY-A--v%J#dShPWVn2P(@hY* z!>=!=0@A(!CBK2glR=tYtcXs0)u$bz6RKxWGQ$d0SEMO9f;X1Y01fwG9p}S|&M1$_ zEdUK{8S&h%b6uyEA;`YQ8h&tkRmq-|*tZ7H#eTj_7|yJP4WOmdh)<6M^ly|QNeD^S z)b6vh4I=(~^lUD2@>l6_VWhDBRc{wflaX;`$-9UnAPs+haTZ zw|nK?zN_#4Nv;P6dOjb@o0$N{a056K+lnFO5WcKBu9Cqg6blRGUQ@L&5~m8Uf(Fx@C0f=cWlAGrLZphwSFB_~r7D+5V?LPdMuO zPQF?M1;;$OVxKjFKY0F$=Vvk~rJHRKgX$UeDMUgmmSlgXfL>&?mVU?I)6c{Lz$?E^ zsZwA+zZW}0S-io5?2T0qnfC+s^%g&+$^32pwJd;+SsEo_kmT}5;bT@PC(cZ)7m}=p zqv%qt9#m1|B=v82Q0utN1l7RmeAmN*e($`fbC>;kiC5ipgmf2aF zk5>|CwTrcM0#B4u0-0e*s@fhYpp67Ut0+jRY2}DHl3dqkodIz*8MSVN3})gkVn}DV z&*bTy7Fw8Lee3rcCB6NQ;4B66n2r5ni>3iOCA%gtW1jvPu zLH*BIsa|ZuqqD}#PJgVKVHHk3>o0#VM6g>3(alVZv1}hY?~1Uyaq)h8!%>8-?8S0I z*|y$85Fc;=#745&w$dT47NaMr5LW}1#a^tPU&VKn;Z4P;9x^nVD`I1Pe~_0hu6 zPV(}eW}D=nxc-E<$Hn!Orx_bN>RhK@|M9AGaAaW_d$G#N2OapOK@u;LTmtv)#U9rz z1Yk&y($pFurG;7`;JqubW<}V0%cJg)%OIttJ(LLr+KdS9!PAqRFTs*hKfnL-=`-4( z;QIfaBx=tjN5tHp947GrlSAA8Im%yJ4+Pv1#%?y(5&S)qOl*G-HX{2ZNVFtGS26nD z!Q@o%Z@UbC|F5vRt?k27Ig7yMyr2>~UQgPP@G57N)*xvSP=Ir49XMfmoQdZ*61aDx zlCItOD)W!n2@EMWGrY#3*JH?U7xR_tD{Q9Is4C&i#0BZOpU@4--sH}>qO2mUfhgVd!B%l?HJCsuezKkGl z*G510pw6)R{zc7ZP58{QNYA5)?I>YY!11mck!+Aw<7iba1k`+pWPr?a$Mbwl0^RU< z@G%8+qni!Uf;TQ+1+ZoMGdI}*RXFtOCBWNnN}%#y?7ld~g2rnLc#=|f&MBO-?m=ho zV>UyOERg$sZ=Umf1=b91fj#UGsw8j8L--9(%R27ndE-*YI@gS`t zMMkWUWb#W@Aq4d3oq$#TEGGj$0I$;e9?GkWwTCAmM~@eWjNYVaYadh1_A}m%+B$q+ zeEf3k#-03ALG3~@sF%G}F5rAD1wGhRW!6C}l)t1Nr~nupY~jY%sMHaa0*I?M7J!7A z|5-3;ztR^IcTMP*TRE)mXrMs;-{GbHp5v{XaWF)-Dd+`8#V(2GXA(%tGarCkr3ApX zsxLSIZQa(arJZWqLE8QjU5!Y>KzP+FR9$x#%@W0MW9-nckTHA*r~ z`&fSeBRA9MK1zASrx8;nr2?Ah!J5p6k2$02Kzb@+5LZJMnI2%s-jeCX*0-<@I-q31 z`B*cIic3C^mwY%A;|rQo%Ds||etC^HIrFpe!UQ9#jH_R7(Y*_>NNgK`B$9AlU+G1Owc|Gv5P?~`bB1r&tLgvmmcekJ#8DfHN`}W&-@6wa^Iez{_*Dz z%9og4g)J*m9aYJhx@kS0j0Oy?;8Wexw2 zGSNMX_HW>02Hd5878WbtAidJ6*)RXw7vkSJyw)qnxu&fCZRDY9ojr=-IT;{88QlCX zRBm+7yA{YQSy-c9tf9=W^FqBsF^tQzQK=y3N6MV^u4 zPd4b}F6HF$#fGzl2=GN%)q!S>!Bgx9z z{?AbUIpxxWYLbX4{5|O>AY>1A9eN&O8Q$+wR-4p;OJ=!Ptp57_^M&9?>K69|=hwW} z?Z3Z(+#l(^dRfJTNdk$!V?|8h$MQ8L+woPv`{~|`^-Ct&q1FIGx}YXUeT2lIhCl>S zLRx(Xs|txj?=JZY-GyEQegdmC?I{KRGQ3|Yn&_2(8vi><`HQAzGvh6e)Aat)M%T`N zDar2xbU_>E=7y+M0s-oFv4s^-x2g}C--hr@0QCis=uTcDKKmMkQKs`0puoCQa)rn{g>dUT`GH7{a~3iRu$H#s?b-iO7=srNJg9LYqE59;Rg>FYr!kwW zw2d6sx>F;Dx%@8je)G9uKr%#49JO20IkoX*HbmN87N@wlW z57vI(IIWVbnWVrqVh+6VJYGZ)5?CAZAg*5wKxr7#Zsr*U>`l;Bi@qmL`H|JMUBTF- zmg2p8VxQr%(`C|f?%aETm`>omfdva-s%8>Jp2i%29qdv<(d3O(AD$p7Cb^PE#)Yu{ zr`MwHV`E@|G6N2XV)5(68fvd0fyL;KRU^dJzl!G?5ws(~>N|O22qKUM98pI--UJ=2@19v&N`o zz~85U+yRw(3)ync=h?=Pw%sT}bal;Y+$tQ-a<}|vBj4*@oXrnm2kiBW_ttia!yWz! zKo;~dBvJBPv_9gc+0v~2r2cb;z>tBlTrL+A7oYmpmapg#&U9_J+N^9 zJIMuz5KnZNKcn3Aj*n7!V;y90HdOSHM!9dz)SvC>@o?yPqrX=-%?XP+<8;QUCa&;D zhv2&ozuTd8&#|e)9ggohd?M?dv311+DFy=lO=(9g2Ul*i)K$Vh=!(xO;quH0@BG>4 zrd9vzZ=@()TN4r8N5AAxa$vH>Fa9=$h92aNo_Wb>bWPoPM3TXf3V|3a2|bboG!WsE z!_$`pq6G?rY5cuR+`nFItnJwhP2aZ3W#$dW>1sW<SlIY5yJ!j-gesdbMWPz z!xYRQK)#41L(7rh>w(m~I67#B0DPA+C_1VsO!i-Z>?gDxtn`eRV3 zfRj05R4l4_ic&y_;z8QKU&!5@onP76h^yNltZxn8{B~N)6&5(suvfYYG-Pq`1~I4z zEt_i#-rI1|br|oh!&2FUO;Toaoxt1k8<9AXZGCd#sO#!KCux;f+ zwp<4&GgKR!gO{17XfgXV7AY0>n(7BhAIlXpT>5JuE&U&IDk{2Ix0>+YiY(f>DAHrI z&p;2j4GJgPpgaHvvq5H6nu7Fuu)1%~jDMTS-Bcup7WaA4(ms*1Fkm*9*gA@O<~4n9 zwX+p?s=2_QVNm{`cYx$uXZ#Thd6zq+QVtk=;`5J?t+GDr1VHlsr-*6FtcZ5JJv~3* zfQaHrdAXPAQK;yHwQRw7eslC_%svU7??_G{7LqJWswo zKLI_=dOYf13Mn;Y(d@zA)fSS3{&YBZ!vHK#0wAfH=gF2R`-6+Iu3rs`UXvU6`l`cS zs$tTC`EOd?yh}g>uArZy&86|#f`+ZM3r{S@T}P6?iK`X@%jL4{Pa_U;PNyf%BJ=e*IRvc^#=$BS|v6{(a zmZ&*kt;vSC>anEsV9m`w_nuGj3gEgZen1Tm3Qa*zd$FpDK|C)Bj7rft))#0rZ+x4%wZ7xI=5+Q`8c=R^mb_kU zXR>!2o|uJ0Vn{o;Re?y$ar5JOq_t%W_nyWZi|L&s?X<=~G#O;q&9)T-DSfiIRR-CL zDLNFykl1a{oIXTFDPj)}LKcQn=qD=T-$q$%2+(9X>MYX%@771SInWGKvu)KtT#ZLQ8ml`zzx2w_6=p?nXsB8e6G?;yUD~hDt7JSgrj_D9! z+{QtM3_cTdd!uo;=E5Q34~qx|QA!84QM@guk-@qL;2e*&f895NsskRsRO?RznvXxC zf%7}sf!Tjq4&pl#jXuqdGQ{+AWye@Zp&K0DYMdQzX!n_|G=nKYjQ(9}T*ea+&dN&~ zjZ3MozQJU-#@~wrZQG54ciKwo+R5|bRnJfojQS*PBsuMh>ZCYyKXRAS-^3<&q(F|h zJr$+)lM6hqw;cP|m)~u>vp7k;W)=y-epDxhgnJYYc;i<<_<-#X$U0>vxc^}3A}*Kjy$FMIn)j?f_vHRy@_+X< zqzn@amDQR1uj!LKkYqRX|GaWO+#bjxKhNvF^x-|CE$Zu%EJi-uG9*xb$z_|iXltC7 z=c&29AFuc3pW$|@+3&5QK@*ElJ^tCW+Qz}Qb&^mE$+tIrqkS+Pez$BIAZ0e6P}hmK zH6L^+A#IXqO>Ew!LOv2&F&%EYBAPwXu~5mf9}GU_O3?0Gp?GKx?^4DeM$<*>XmO%a zW7Pve3$K#BvB8+l0@0xU#oF@h?Qe;Vc|#eT*~7(R=*C~}r}3ATN48AWrnr}`V7E7Ycm2|Gas2Y`)2l;4 z_cii>J&}gi*uYx#L*@Gc?Ub2c0W@m+^RF&6E_ziynR=ejyMDAWRg2>+kLWFu0$bOL zeS9q`#LDP34!*I^viTpYD(Lg&cQ@3tA8|yw^+_*EbV)C!y1&nUlz%btmPE)yRgI%yXR>y5)d zx$ZO19ZJRDj{b&2)1Y)ru?AK-s3pBn!f3k6X9V-CV$LO~`*oggzZX(WxMN*LXqa?( zias`9J=M2W_RU?!4DKs2C2{Vt_tp*FdPtSuj?N10@WV4ygB+SVs`&^g;1n*CLvu$1 zeT40HeGNl%r=^!|Nh+C3sz4tz{q`gjCb|CQWLl`0V@`ov7)&R||$XvPJ3G(9~%tOL$OROeS6;$ROJq$mdoM%QBU`NV?>j~RJl!4B6u zSW#_wp}5ZemF>%XkH;sLMY6*hChqe#K0io_@hGb~^rNYBi*^m}v7A#>v$+m!7g1Rd z+g9Y>0rT&`%U%<-FA`Mob?&w<#I@}5;+U6ZVBf(Hx}TDQqT?ev;UTcAH2-Q%x4RTY zdQ}n-;ER=9R*vZW@f4eC37bv{ zMUKzt=twWW=$9Tn%#G?@ifcb(tf+iD2z)iNKRG^xp%!%bnt((Ska&OA4mCXfGK)-m z$0r35F>!dbCjLhX*7R9UUr2`}%ADPpA@<_>+`HJHG2OPOEsr&G%?U~yc8%ei+NJf6 zeH{xpNEY0cU>|6-41U)bH))+Sq&3Ha8ZtHEhz(hr3yvKLS>sO(nE9@jHpJ$2QX3;N zG`cv|D@dz;qXUY%YlfA522)1P@V_{s#`Q3w6olE1SU@AZj0S_;hnikJB{K`>L38zt~oL=n9855|lq zaMBZ)@N+9B%(2Sru(;Uc!5Y1Q=D>;1I{b}3N2RcD^9g59Xo|m<$5Hu25MEv4c?QOn zGW&!P0y8!5p{~^%Ux{s5gT0CUPK;Ly7^uyrPh*y2d*)Z6)nImFNL~EZuZ*66#v9Zy z&opAJq}ua#ztnjUF)T^4 z1C!O!owVBVf=N^Zlk@>s6!nF7`Kh!EN*KcxcYZ2-q?V9up43o83heT`KR16Ly(<*b z`N&JZF9Y8t;Pv-vVEy>x`?f;DM@GEMFrL1ERHJLm&$;!231YzpR-|^VoG!?r{$6fJ zPgxR&HQz{^5v6I)`gk$f;OEn4-rok}U%JFCsEHOu#u%h=#x^Mxxp~BlJb(7wq|Zrg=3P#&C50ONimuX6~gI zB?BKKEuIh1&Weoe=3WXAoIFgS9ECL0l@FG3z}tV(zL;ffe)BDkr=^AThp5xpAVlff z$HxTjrg4}{qZ`uEf+&$tkr!a~3TB6Pbck5#HGC=r>tQw58Nr5-?$fDRv$>1uEV(H+ z`APq$h;k5*dcI$$yg4v3c}VeSDbz1*6<@LKPOY39P+h9!mX5ekv^eEJ=$?MnQ?lMJr$_havW zHN-XvuJ5&K09>RuS20A0FP?KzxS8}5!3_5pZ|aZ2 z!7iBN8L1sT2Zlx#spyCe+3~uC6MUZW1-(T5&@DBpE~kj(6%<&Q3K_bXS2?+;HjeQ7 zZLrt#^80TdIUd)!VV^Q{AzanS94sR+5wR@=GBZoabaGX*N~$W$C~-{6k$j28-8s3c zRV6~5FUL!Umh037|2v^^(vY>WGFFVV$w!t zTVxxtTH7ZZloEelLC(w@;jq2OQ@*dZHEQ|TaU{#RZwgk3A+iSS8uSYlz_%0yW-+p; zlO_+UCNwSeE#Uz@!NyL-Oh&!DwE5zBmXTW^^8jO}hk*W1w6Ks$KC45|M z`9Cd@hekDP{ceFt4)2b0*Im=xet7R4$FQ239PL<}OLtT&gd^N;M>FH8eL?)K9iIns z8K9xpdFH|DEHws}u+9)PFRCI`p&WP`WAv*6jQwE+Q|0yq=MS;ux1(zY<{zNMsfS(@?mz)CxrmF zo0ZMN@@=iOD{C6-3iX|{Wa?oK&WqXefb}rjz=qxs)L96IcrE&ztI&|^JnP`jJu6Nv z5u+P6_vrY|W`0clv*TQi8|b)gq+SD&_6q10Tvuq0MDwA-g;Z+9^n{|F@h~E8*D|Lq zat~RpWEMR;w_GTya4lc`=@0(zYkheBul8_j|*=BFaiT&VkIxLxZxl9Ejm*Y9bfJE%_<<0|!D{?TR{RD!rr70VM&Nh+zJL?A_&dTwD8Ut9 z$JRlOdS{B$va&Je)(0nvgOb`hib6XWP?rh|6FMbVc<^aI9(ydYN9oCRc3Dp*uv7jL z=vG;Zzp7;gcW|JXX~d%lmQVr>{_SNO5R^b=(u~F{Bmfe9Zg0wfz5QNzYAhP-IHC&H zDH9-j?LF#M9KFx`FMs1W#=sQ3-79PE&<&+BX-5-p5U%1qs+h+~=o%SuW(rE67>F08x`h`I_NOEpv3a6TuKMP<3<1hk^N<&=#dccKlm9nZxVQ zo$EZWz;qI&ci~c>RuQQj$MOlwT+VRa7jq}mUYA$=UO6lQSH-8`{F~7Yd<0tjZY{G9 zspCV5ur3(FR3mF`gI*$Azv;j#_#?LaA^~=F$zto;u*LC`$m&OCgjt(F|J4 zpGEogG^S$uv9}o@@?}?5n8oy<9_l1Ckc(jWA6Lv`5PIBBnpaR)Xkx+%_E3TZo|WEg z2*GRxj|nDtaX$=7EW4Nx76^!Ob>3nfY?Mz!qrso5lCL%-@6aSsKP@GK- z&hEUhn=|5d7&esdNX4P0QCU-108T&AIm}SaX3}Rd@p+A$o2kSSVM#EK1ucxa&Y-d) z=Cu3OPGciZzcGgK(;vUXw>4KwByaOAwClP zXf-~TBvJqiHHZ1614I25HGr1ef&@lzzBm&=kHRD%`+ZFsV~$lD*}HLk+|~KR{q!`f z#oeS+sOL;3#=t-6pFji5@dig zgXNO&sDf3|RIO>#jc&SjpD%T-%pnv7}@ZK*IZzHgDo5@&v zM006hv$E2?K9j0GrCD#gSy^Z(OKRw9D1jv{G|`MdbxtT%R0}$8a!@Z8$!a&t_UvYU zUo!}ECGV8dA{t>e?G(|8T=~V)xRJY_*3+S=Jg8go3OHZ{ti&NxU0m8bU5bX$uqQ!!F;%)9UmGX?kX__B_?E=rkNYx?2481%kP=t>d#GfdgMi z2fG1-^Z0tAqq`6Oh{9%Cl(Gq2A;;e3{&y5``VU?r9tqsnSQ`yW-bnNSVA_|EKFu1EJ4FT13`RJ3ayff4DPOl%)P+ZAk(dnrtRM{AnDD z3`w?S?Cr_I^n6H?UR;=eij^G`o%7u*X54!E6=UzH?)-u%By}f>j}D{0A|T@CLylK| z`$VGjHBN4&6pOp*qAm1{{N7i{IxB;81)e-9SsIShVRro8z*G7jcbl1=*zr+#hNL2i z6V^JYE~DHL9dE4|Z}=4#E1F?=i(xgc_7m#_vNa_Zb(n5+4e>V%$El?iJ=Us{tj^*` z#7a3@%_MnC$GO?zw5~leO1`JgUoeI=Pt5ULiSIgWwyC(IIoig*MARd%`rpeoK=d%$^@bv@rB0NNz0t}G)b{)Ei(r)?3s|y z)R=r~@)x7Ka_OfEG3oS<52So@1NN1Mi2{$ab+Di( zv9i`M^Cj}1O9Y;0e8=YnEjf&Uak<+$cgqtL7E2^|C?^7qzfHZK=_99@oQQCCGdduk z{fY!4-Me0uY0Dh7C7`#GIq$8Wo8hRR*^H~ez)+dLDoQ{_>J)ef)%>&m)rsK5mAj|zdP^Hm~ z#QMz4mnFuqnYPuquI(U4zk>U_|2{j(a~?j2&O?Jw(r(OWzdk7y%dqF3;p^eM)M4kX zFL%A;mG?ur_lxc;Pe=DEM~_SI=R%i6VauO(qGUqOl)AYt@8D>+$3^0F|Jx30KENrQ zp*#3496k`7j}7)OE(y|meey;uaK&z?F~4DI5b^4;=a%lDtxsEDFG)i`_?qO&{-ZQ| z862Ic$z5z5AhThF)6d4Mdym9D}8BqO6R_1MA4SDE>MH< zyNJ_#NMbu;eS>+lq)(mf@SQc}qrMpLha^@le)}V8{Y%L#-{V3S48_K6>F@)%WNzhf#n_#S5$0>3XdYd5IE2luL%7!1WRIwvo+ zy)8CjdC>RvDCYE6s81)zYp~m*to(|6Zx<+}KH|RZvr}Cdzvr^N=&!*04N1sqle#W+Q+$tp*y z%PcsFf@fMV0{FX%IPQ^FPc*niC8q%1hIq7`kE7G#081cnI1%J=Qfg+=`%=H^T!5+pf2DP&~x^~;iz+Kv2*#K`dV{i@!D+DgkwsHc(fU z+7FU)ky~mj*&XqxH+;H9iYCoZ8)~}h&q^{fgW^pF+%R$Pq-6db{8DlGsf+PDkWxwd zF(arKxo#xro@Tfw&v|jd`}sxlAi{-Xp3J21Xl=%Z0Nt$=g=&*@;Cl|1c`X?al@nE_S6&Ymy zieTAQ6tKvn_YbzaJNWqT54*{qEl`Ez%gQdD?RR|82^Ud`@YpU0?9%{ zZ`+-LUHc?Aq5qq`PY^6a_Yl|te!YDG9oUmIf01xQ1>LZ!hij4RE>CsUqPG|FO9yhMH z&i%JXW{OzPZ>)6AX)C#TcO=JX{~e>#29B$;xv}<~zrg-OBte6P0LB~80nyVe9jN%r zY;H405x8F^^v^l%Q87oH{&|Iv6SAK3P%56ADLyHJy%26a4ZKkNwYQR4oz(G~7D%5G zzRc=v9pwsoV&ZqSgxH{8niuk(IXkbRKb=pAriJFu zt$g-`CzQ{`W_U4WM~Zn)q{E5#MR1xH>A@XeW#6d z8^^3w7<;FcbPp%4B}BN>u7b^j3y6WV9|#~^wX&|TEW$n`T-5=-8JQ=4@`ML|mb+AM zOcz@)vZ`@F&{b5NuHm?m=FKfwx)a2&{$1T#sL$}8Lc@P6ASRL5|1RHQQn*NUL^FVW zec^({?GF$C>%9!#f(^WReJ4{{kjj~QVBcqR8>i*+vwiOV-c9Df#e4oKHw&UPs(pU) zyZ5%2!5*}~SXuPRHKSit&AAB8PFDf8C*gpu;pubOp{>moyOwi0OG6O^=1fg4R2I!p z6hSYT!P4gOSc0ORieJBY(;P+yLT(?6lv{0av+hq?2M5jr|IrdQgXw=fp*#Bko4|Ex zo45G@st`C3v7N0jy`Qey9d+T*m$54<=q|-gHA^bR+AZUO{P$N5e|~SpO$Nl4=ju~I z19oFrmm0I_$?;yZ8GJwz%`w4gJ+sl{9tPu>;CDmLYj+{k5Diecqn}yroVNl(mNw=~ zWlQ)^{f+U{PO+WCUS*_=pop7LzWM3QZr(Jv_~Rg0zF6(Uh6=W81B z?iTccQlc&x2LjY9A@q1G8<~}!OWd)d)iT#BzCLTmKiw#L`pzOqHP}2qRc=6i$hhZ9 z0U)+Azt66{gFa5g`U@`oP%i?^9R zi7QWUT%EQoAnKFj@YP9OJsluUTA}x)`V*lU2;aP*67{C#1l`!15F} z{VXe3n`GJC9vZ+)`)f4Ps`_5TYNVKfe`@g{ zfB(Pb3cLLHHi_{?{rhQ8!9=8(8f(!)KP~@q!|#{(OmYJOvQ$0Q)E+k80~tc;1CAsp%)OoLIi3 zZ{BUdqVL`w8+!UK^?8)t_Tu}trN2S_Ys-HLzqfO)&RL z^wnTHDru(6@M!wr!17Dm+$CskE~-`PE8HcDADF0;oNwNF(h~)-6{$t2t9Q3Gj+<$-;TniRtp{CVAaoW7OT0+(rzR5g zg&HrsSu6Qdvr23t{4-*Z{1JCk>nq_?s|wxv)O{_XrxK5%a854!VQ$%Gb&KTF<1dIm zhyL@~I{EciPjc7!1qzl@Jb@jbd(2mfOFV;EoO?MuS&X3kAQ^%9`w@3t%XFad31U#A z{CUe;d7PJ)0b!|0S?J_v^9xo%BhxZf!{7S-jS2Cg-hsZB1;xq2Z4sk9 z$r6K+bU{YiyOun`n2`t^Bm{m0Yz{G+ERSV$UR?3y{#(qd|HM5oe9LLcM=?)j^h8v> zCJAkYs8BkN{_?n+k^7MGlCb>2;YqjfZ134O*+O&b=1Ok(R$%kXDkIYu505I~2knYn zN+$BfUl%z=wHd6l(ny!#EB1!jM0EBoYodMTtv|$jnYUPoV>>@cF!;W@)-&>(3hSFJ zl3zKIM0DP*^54Qsoo8vXUw})iqMr_Zx@{|gFf2AzeKlq-?c29)R^&6!4`)>AP^ceYZC9kO7S=|WE{(GZ`PiJ5n8v4;$II9ZANws~?!f#}@`Qqa=ZDr;a_mOBa7;~^J$xqL zC~&iP-Sk(w*sjR*@k;!XS`a7YO81XqEAL!kSQQfhpM(KhF6cOwbQ3xs*Kr#Ke}Lt9 zc7FT;PcYTx;R9EfGD)|h6J_YFw)yt-m~@Vy5!dix{}>nVx4z<^b&O%Xd35qZ9@zS6 zROk~E7mAJoZ&fl&x1(c1J7`cJWwEAKbCF`Dg1{za2rdDehWs+ zD)Dq_km;BoW0?DO-qw`w*N@Ax z{{`9wCHkr3=S9R9Bbfs6!$`O=vDX+m^d#bskqLJqZ!nT9bP2#nwy^nu7&*5R3BpK~ z&?Oin@d63K$eghGq0Y!}s@tMtSxgSMleZqPzG3pa3H;7|P+PrTdGq}DB`Ndh4+CsJ z9q|nV#lt=qfB6)Zo@FvQ+8%%BVLLQ|aKJ4xjvYbE8`<$iDEMyJ%4q?#q^pl`{ej#my+peK@skytB{bc_rO6UzXJ2Ww?= zshwnUlC*LCL08qi|EO2WLvOz!Ns-CL|Gt7(+HtpqNESvucoWISNR%+K9E`knBa(}e zJYie%FtQ_b$;U{DKngH&ERaHs90=P|gpn&x*2v@*J1OSm?L=6HAuH}6kI z*lDQOJg>9^Pb-lUjJ$CpQi_oPfs|n+P$1kzIEpH5e%uy3~T(B9J=kC6$1 zG+<=TN~F;lQIGH^JlG7Ad+nsj!*+z9wVv;F+?U*;S8~()TqNmep!(~1ymE+oJ&9&W zOc_YDI1w*5ih<2DdC)Fe`GzC}tUepZJFk-TO7-r#?P-6;cg}i=JZ2~D9*(aP@aYZn)xWA99Jn<~{wE`oMoC&)+5eXnE1!NKSxxHi6=~V_tD)Mu zm;ZE(xFL+#@kK`L6-Mm*A|qZ2BX)g}5wC?2yT8bYv0gM{&lef7UzlU>7a4I=7_sk* zj94dZb^l*;#Fw)QR<_UNMf->Y{E-(m?&nrE7gMNLT7pjvlC+hRp=;%pLyUNl7=%Q+ z2Z~@Mj z5zc3J4Y*uh7H#X4iGX+;No|YKPx4AT1tE}Wj6}KMxfgcwk~5;7DrgtJ=XjZ)D)8&M8+BQKteDm*lFu01sA7`zJj%4Z@IA*X zKz`7Z@~c38TbR!pkWUikvkv5Ah4D9l{Em>{1oCf%?cDVSP`=XMMyY}hN z9e$#zIXb4^*u9?AqF3&GY*F8TbnnAYm8$70`poOD^YosVmfmxoZlcej?mJKW)7S^j z)18z)be?XoYUv~A=^z^W*m=5<#y)YLKB9ec>O4J8Gx^{=T}ij)+2_*g;^RHdw0_XXk`6!!z-`xY(!8i@Cq**}%v ztKZzkJOYyMJ#$P^u?8g3$JgI(Nm(_M~^%T5%p{wfX=$Dn) z@G|}k+`qx%c%^;3l0-Nr+DSxUBFdtPNK9PHnux;02eT%kG4V*-5rc_IgC=5~3H8t^ z!orf7{8nAI;kRGj$jOQ^;ihAjuX24?k|xSt71@3|@{vM59>_o=B6@UTBA|&gFb`*zA)l?hKs1PThElQRpIZjhrf+!{uX_MeMikrc}Bu}POOHOj_IhRgn(rXVrq`mdjbC1oTz4nq*+FNr-7e6Ey0I4ccW}MiMLs`V` z+r``O{VaBuw##?7RU4zaVQPwvmY7Qn%QC+}h+&v<^78`u>Aa@NhYOtx@|$HC{tM?i zcjxUKRZf2#XRgYxjF1vG{|EmSph%Qh8Nw{IeUlxjXLVdoD-<0<5_@GD)hs-5X4DI-$DyM%v)0)%#l;TJflbU z?(6$>7v@v;$DU$+z3~+Ij@Ku+L^{oWyynJ2*w4b(lO4y)-rg2bxE9U3oyfYPVI+y~ zNT^UGKb7rmfzmWzkVGf)yLEvm8C2WEYNKhB1Q2TMW*;vg`_~O>xThYF@H&t!^$|uJ z#fmeau1a3zVgJhYVxcUNlv0q;-b2~8HFT$x-^rGC(c9uK%|M_+EG1CQ>ZF;b$rLRo zyXA9*a1SX|5;sXYDXfcS4>w6iHr8#6LY+#zIT=fo(=4OvHGE*b-y$=qth8;zy_Umf zk~86wspFqs_?tX;dA8?DkED=gdw$3wz^@TG!fBY*lH=Ca%lYp@rss!BlQM zmbkxsY6}zcQ7b$4n0Uf)Tw-N~{baTm@C|He*ftO4s?lm|y5*WbTsRuFby#Guu?4T^ zewm@$iG8?-`w0y4jYmkz7j~T}?1rrtEut(wn^(M z?Io2${%(a#^t~t!$ded%Fi~cH6|{E^WOx zyKIP?*~+#zb)~YgSfX<6L|pWJ{kWrxxZ|G;aS5JhKicn&EikUm#c*^?ohsVVP8bk* zT2y2wl!7vXS|hWDT34~@Re0Vj)UZ`G)jeA!Ps8qv-5EO13oXU69}sI8lLCpcGGiOdafqTZQY#Pxj9#Vmlt^5*ymFzfzIRtqBFRlQHh&W zQ3*RVFGPdEbi%=zInI(@yUBI!LM*YvpJ?HZ75qTxT#l>_pT4Q*_?@QC3U<_4TOAFv z?%aEv>N!Vf9&mj>R5lcKm5_utd-Qo28<0`yoG2J(L($boWIF12MmNjSRU~i>vqq+> zzSDON*HvA0y9l`>?RSJhJU&VO9G?6=Ge!QGYC2rt1q6Tq5C8%|00;m9AOHk_01yBI zPmRDY8SV|{%IK_CAsfwPX@_fw?Bg^{Wv{7LQMptu6boXzQ`h}K3%Tn>RVQa!h-`9V z6YEus$okR7+Wl`jFYa~b3OoC-cb9d=Bxi9dIbri}@=dq%?g!KpI^ibAFaEAR_8N0# z=!yFHm3z4|TD|V5SHrei74MXDm;RL#i@Ejk{iIi!6v);`WyiggGi$5YlS26pIpHhj zH+mvBmR43XODn?S+UnKC<(0+7rR5t-Lbf7p6-#+(_kPmpIVmJp7M2#0@4eTj=ElHmGK-6@4)5g2Ah*c^-)`4P;{rJx-6E=a#fIMS%f)P2>ZkE_Pvcfc@)|iVb(@eK zRI(-Dn%F|Bj%v87YD$azcC@tIW5OmzK6`7giA_VTIqQEm{uhS4;ROVM01yBIKmZ5; z0U!VbfB+Bx0zd!=d_M`i$&SyyIy?D>qur?cWg1h(mSU4Hf00e-*vqNBF zcy_G2F97TRXUDKG5+DEsfB+Bx0zd!=00AHX1b_e#00I<&!1~|&{{L4@{3}WVJ^}$C z00e*l5C8%|00;m9AOHk_01yBIKWqY%?C>nxJ^tt5{Qrk-PS73*00AHX1b_e#00KY& z2mk>f00e-*00NQofAXLH!3zig0U!VbfB+Bx0zd!=00AHX1b_e#c!mi$=l`(&e})YV zLjwXp00;m9AOHk_01yBIKmZ5;0U$sK!1^ED00;m9AOHk_01yBIKmZ5;0U!VbfWWg) z!1?$8;QasDw=;|o2mk>f00e*l5C8%|00;m9AOHlOZvt@s|9smYMh^sl01yBIKmZ5; z0U!VbfB+Bx0?!8lIRAe>>0zd!=00AHX1b_e#00KY&2mpcSn*f~uKi~F;(E|Y> z00e*l5C8%|00;m9AOHk_!1F<1a_ZkqeCSJN>c12Jochz$hZF3?r(@0W&&MBfzZ#nz z`SZyBh1H=i+0U4rQ}8&FE^{-L^yrg}ZK{n1HXC~n8?vpou&gPh=%Dj!U#;0qxxH6Y zY#f$ZFNs-6L{cfA%ZVk#hwJdBlF=-K^}QlXz$E(E=jlD>r&e4o5E<-r^|1gnLM#lDJ9IdSP8Gd$`H8U}N33DAcLc@lP*uiRESXBd^P_U1SSK z^0-qrexfxU33@a^i_&8jEYp(JTGTk2s>=rvxK8O|v{X6hCre7BvO$y$TeH41mLW5_ zeUsrilYGxW-gu+NvGx>ORa&x%>#}udp?P00l^c&G?k}G@P6_#FtUC6Xc*1a8Vr7N> zWVRQt4Qy!GHV@^h(Q0eD<(fZSI2yHeSY)uV1+V9Rf!*!IKHS6o1P1xWBP8VuyG{^w zqfUz!QI;mL=G@E~rA2!mt+qfOn;ZtNB7u-=lh#$*ODcu@-3obL>IGpyo`k3)uLliJ z37;x5!`@Vt&+kk5u~_20l@oobYZTujXBWuH_Z|XUIr|AWvR^xw|`fT!30&2mb)p^BUj=3Knzw#q4F+0m1jk)yP zI5t({)lNV=A1((tCA;NGG>?*XwU+EYbUA%b7Zx zs@~|9C}eTn#HM$hI@(QmYv$JntxxDj#j`H=`t%WA@R6+uTI#7S`2tF4ZOh$=XqY4q zlhowyMf6sEj?20G5rIZO@2>swFL`WdPQ~_&t3UeQ-?Q}}tr|Kf%iSxd@>6fUb3Q)} z^fAy+{gu_z+A{Ol`bvB|wtgeZPhUKhpZZ+R<)?u@x@@2FzS(!la(9}Wxkon7)&|AA zzZ>8!+t2E#{W|H=jBM`uTLe$bRRhZQ-3jWyz3Z#p+uz-K!DE{^9osWHYCzrZSw;=$ zb{>>)0s$uw0}pb%zog-IT~C{{}e-bq-PsouwC9(k1;vQhy$f>f00e*l5C8%|00;m9AOHk_01!Aofdn_g%uP>Q&ez1# znqu0j-o%IGJsBHeUXI5%O@%D!GmDo;$mQiL-esG7S+sp{`2s1Lp7x3i9ozfHrQs1K zF+H8PmDXY1Fdiu83qwTciZ8U^RP6oc;WRtK%v_nyVN+A|!v`&;d5A4%{m;g~V#pg_ nKmZ5;0U!VbfB+Bx0zd!=00AHX1c1P^LST{|o@Kk=u;Bg&T}0)f literal 0 HcmV?d00001 diff --git a/tests/join-sql/countries.pmtiles.json b/tests/join-sql/countries.pmtiles.json new file mode 100644 index 00000000..99884911 --- /dev/null +++ b/tests/join-sql/countries.pmtiles.json @@ -0,0 +1,29 @@ +{ "type": "FeatureCollection", "properties": { +"antimeridian_adjusted_bounds": "-63.146840,-21.370782,55.854503,55.065334", +"bounds": "-63.146840,-21.370782,55.854503,55.065334", +"center": "0.000000,0.000000,0", +"description": "/tmp/tmp588djzw6/bboxes.pmtiles", +"format": "pbf", +"generator_options": "tippecanoe '--tile-stats-values-limit=1' -o /tmp/tmp588djzw6/bboxes.pmtiles -l parsed-bboxes -U1 '--smallest-maximum-zoom-guess=7' -Bg --drop-fraction-as-needed -P --hilbert --generate-variable-depth-tile-pyramid -rp -b0 '--extend-zooms-if-still-dropping-maximum=3' '--extra-detail=30' -D10 '--tiny-polygon-size=1' --no-tiny-polygon-reduction-at-maximum-zoom '--simplification=1' '--simplification-at-maximum-zoom=0.25' --detect-longitude-wraparound --preserve-input-order '--maximum-tile-bytes=2621440' '--maximum-string-attribute-length=65536' --preserve-point-density-threshold 64 --set-attribute '{\"felt:cluster_size\": 1}' --accumulate-attribute '{\"felt:cluster_size\": \"sum\"}' --preserve-multiplier-density-threshold 512 '--accumulate-numeric-attributes=felt' /tmp/tmpg9jf8evz/parsed.geojsonseq.gz; ./tile-join -i -f -o tests/join-sql/countries.pmtiles --join-sqlite tests/join-sql/countries.gpkg --join-table countries --join-tile-attribute ne10-admin0:name_en --join-table-expression 'lower(country)' tests/join-sql/bboxes.pmtiles", +"json": "{\"vector_layers\":[{\"id\":\"parsed-bboxes\",\"description\":\"\",\"minzoom\":0,\"maxzoom\":0,\"fields\":{\"another\":\"String\",\"country\":\"String\",\"felt:cluster_size\":\"Number\",\"fid\":\"Number\",\"ne10-admin0:iso_a2_eh\":\"String\",\"ne10-admin0:iso_a3_eh\":\"String\",\"ne10-admin0:name_en\":\"String\",\"something\":\"String\"}}],\"tilestats\":{\"layerCount\":1,\"layers\":[{\"layer\":\"parsed-bboxes\",\"count\":5,\"geometry\":\"Polygon\",\"attributeCount\":8,\"attributes\":[{\"attribute\":\"another\",\"count\":4,\"type\":\"string\",\"values\":[\"bar\",\"hey\",\"why\",\"yes\"]},{\"attribute\":\"country\",\"count\":4,\"type\":\"string\",\"values\":[\"France\",\"Germany\",\"Italy\",\"saint-martin\"]},{\"attribute\":\"felt:cluster_size\",\"count\":1,\"type\":\"number\",\"values\":[1],\"min\":1,\"max\":1},{\"attribute\":\"fid\",\"count\":4,\"type\":\"number\",\"values\":[1,2,3,7],\"min\":1,\"max\":7},{\"attribute\":\"ne10-admin0:iso_a2_eh\",\"count\":5,\"type\":\"string\",\"values\":[\"de\",\"fr\",\"it\",\"mf\",\"sx\"]},{\"attribute\":\"ne10-admin0:iso_a3_eh\",\"count\":5,\"type\":\"string\",\"values\":[\"deu\",\"fra\",\"ita\",\"maf\",\"sxm\"]},{\"attribute\":\"ne10-admin0:name_en\",\"count\":4,\"type\":\"string\",\"values\":[\"france\",\"germany\",\"italy\",\"saint-martin\"]},{\"attribute\":\"something\",\"count\":4,\"type\":\"string\",\"values\":[\"blah\",\"foo\",\"hey\",\"what\"]}]}]}}", +"maxzoom": "0", +"minzoom": "0", +"name": "/tmp/tmp588djzw6/bboxes.pmtiles", +"strategies": "[{\"truncated_zooms\":1}]", +"type": "overlay", +"version": "2" +}, "features": [ +{ "type": "FeatureCollection", "properties": { "zoom": 0, "x": 0, "y": 0 }, "features": [ +{ "type": "FeatureCollection", "properties": { "layer": "parsed-bboxes", "version": 2, "extent": 1073741824 }, "features": [ +{ "type": "Feature", "id": 322, "properties": { "ne10-admin0:name_en": "germany", "ne10-admin0:iso_a2_eh": "de", "ne10-admin0:iso_a3_eh": "deu", "felt:cluster_size": 1, "another": "yes", "country": "Germany", "fid": 1, "something": "blah" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.022060, 47.271121 ], [ 5.852490, 47.271121 ], [ 5.852490, 55.065334 ], [ 15.022060, 55.065334 ], [ 15.022060, 47.271121 ] ] ] } } +, +{ "type": "Feature", "id": 435, "properties": { "ne10-admin0:name_en": "france", "ne10-admin0:iso_a2_eh": "fr", "ne10-admin0:iso_a3_eh": "fra", "felt:cluster_size": 1, "another": "why", "country": "France", "fid": 2, "something": "what" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -61.797841, -21.370782 ], [ -61.797841, 51.087541 ], [ 55.854503, 51.087541 ], [ 55.854503, -21.370782 ], [ -61.797841, -21.370782 ] ] ] } } +, +{ "type": "Feature", "id": 650, "properties": { "ne10-admin0:name_en": "italy", "ne10-admin0:iso_a2_eh": "it", "ne10-admin0:iso_a3_eh": "ita", "felt:cluster_size": 1, "another": "bar", "country": "Italy", "fid": 3, "something": "foo" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 18.517426, 35.489244 ], [ 6.602728, 35.489244 ], [ 6.602728, 47.085215 ], [ 18.517426, 47.085215 ], [ 18.517426, 35.489244 ] ] ] } } +, +{ "type": "Feature", "id": 1546, "properties": { "ne10-admin0:name_en": "saint-martin", "ne10-admin0:iso_a2_eh": "mf", "ne10-admin0:iso_a3_eh": "maf", "felt:cluster_size": 1, "another": "hey", "country": "saint-martin", "fid": 7, "something": "hey" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -63.010731, 18.033391 ], [ -63.146840, 18.033391 ], [ -63.146840, 18.122138 ], [ -63.010731, 18.122138 ], [ -63.010731, 18.033391 ] ] ] } } +, +{ "type": "Feature", "id": 1611, "properties": { "ne10-admin0:name_en": "saint-martin", "ne10-admin0:iso_a2_eh": "sx", "ne10-admin0:iso_a3_eh": "sxm", "felt:cluster_size": 1, "another": "hey", "country": "saint-martin", "fid": 7, "something": "hey" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -63.017568, 18.019110 ], [ -63.118886, 18.019110 ], [ -63.118886, 18.062120 ], [ -63.017568, 18.062120 ], [ -63.017568, 18.019110 ] ] ] } } +] } +] } +] } diff --git a/tests/ne_110m_ocean/join/joined.mbtiles.json b/tests/ne_110m_ocean/join/joined.mbtiles.json index 08148a95..4c2c3c80 100644 --- a/tests/ne_110m_ocean/join/joined.mbtiles.json +++ b/tests/ne_110m_ocean/join/joined.mbtiles.json @@ -1,5 +1,5 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-180.000000,-85.051129,180.000000,85.051129", +"antimeridian_adjusted_bounds": "0.000000,-85.051129,359.912109,85.051129", "bounds": "-180.000000,-85.051129,180.000000,85.051129", "center": "-45.000000,33.256630,4", "description": "tests/ne_110m_ocean/join/ocean.mbtiles", diff --git a/tests/raw-tiles/raw-tiles-z67-join.json b/tests/raw-tiles/raw-tiles-z67-join.json index d82e92ee..8582128c 100644 --- a/tests/raw-tiles/raw-tiles-z67-join.json +++ b/tests/raw-tiles/raw-tiles-z67-join.json @@ -1,6 +1,6 @@ { "type": "FeatureCollection", "properties": { -"antimeridian_adjusted_bounds": "-123.750000,45.089036,-120.937500,47.040182", -"bounds": "-123.750000,45.089036,-120.937500,47.040182", +"antimeridian_adjusted_bounds": "-122.682953,45.512121,-122.654800,45.569832", +"bounds": "-122.682953,45.512121,-122.654800,45.569832", "center": "-122.662354,45.514045,7", "description": "tests/raw-tiles/raw-tiles", "format": "pbf", diff --git a/tile-join.cpp b/tile-join.cpp index ee888959..d9484a28 100644 --- a/tile-join.cpp +++ b/tile-join.cpp @@ -55,7 +55,13 @@ int maxzoom = 32; int minzoom = 0; std::map renames; bool exclude_all = false; +bool exclude_all_tile_attributes = false; std::vector unidecode_data; +std::string join_tile_attribute; +std::string join_table_expression; +std::string join_table; +size_t join_count_limit = 1; +std::string attribute_for_id; bool want_overzoom = false; int buffer = 5; @@ -73,7 +79,115 @@ struct stats { std::vector strategies{}; }; -void append_tile(std::string message, int z, unsigned x, unsigned y, std::map &layermap, std::vector &header, std::map> &mapping, std::set &exclude, std::set &include, std::set &keep_layers, std::set &remove_layers, int ifmatched, mvt_tile &outtile, json_object *filter) { +// list, per feature in the tile, +// of lists of features in the sqlite response, +// each of which is a mapping from keys to values +std::vector>> get_joined_rows(sqlite3 *db, const std::vector &join_keys) { + std::vector>> ret; + ret.resize(join_keys.size()); + + // double quotes for table and column identifiers + const char *s = sqlite3_mprintf("select %s, * from \"%w\" where %s in (", + join_table_expression.c_str(), join_table.c_str(), join_table_expression.c_str()); + std::string query = s; + sqlite3_free((void *) s); + + std::multimap key_to_row; + for (size_t i = 0; i < join_keys.size(); i++) { + const mvt_value &v = join_keys[i]; + + // single quotes for literals + if (v.type == mvt_string) { + s = sqlite3_mprintf("'%q'", v.c_str()); + query += s; + sqlite3_free((void *) s); + key_to_row.emplace(v.get_string_value(), i); + } else { + std::string stringified = v.toString(); + key_to_row.emplace(stringified, i); + query += stringified; + } + + if (i + 1 < join_keys.size()) { + query += ", "; + } + } + + // this doesn't add a LIMIT to the query because our limit + // is per tiled feature, not a limit on the entire query response. + query += ");"; + + sqlite3_stmt *stmt; + if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, NULL) != SQLITE_OK) { + fprintf(stderr, "sqlite3 query %s failed: %s\n", query.c_str(), sqlite3_errmsg(db)); + exit(EXIT_SQLITE); + } + while (sqlite3_step(stmt) == SQLITE_ROW) { + int count = sqlite3_column_count(stmt); + std::map row; + + if (count > 0) { + // join key is 0th column of query + std::string key = (const char *) sqlite3_column_text(stmt, 0); + auto f = key_to_row.equal_range(key); + if (f.first == f.second) { + fprintf(stderr, "Unexpected join key: %s\n", key.c_str()); + continue; + } + + for (auto ff = f.first; ff != f.second; ++ff) { + if (ret[ff->second].size() < join_count_limit) { + for (int i = 1; i < count; i++) { + int type = sqlite3_column_type(stmt, i); + mvt_value v; + v.type = mvt_null; + + if (type == SQLITE_INTEGER || type == SQLITE_FLOAT) { + v = mvt_value(sqlite3_column_double(stmt, i)); + } else if (type == SQLITE_TEXT || type == SQLITE_BLOB) { + v.set_string_value((const char *) sqlite3_column_text(stmt, i)); + } + + const char *name = sqlite3_column_name(stmt, i); + row.emplace(name, v); + } + + ret[ff->second].push_back(row); + } + } + } + } + if (sqlite3_finalize(stmt) != SQLITE_OK) { + fprintf(stderr, "sqlite3 finalize failed: %s\n", sqlite3_errmsg(db)); + exit(EXIT_SQLITE); + } + + return ret; +} + +struct arg { + std::map> inputs{}; + std::map outputs{}; + + std::map *layermap = NULL; + + std::vector *header = NULL; + std::map> *mapping = NULL; + sqlite3 *db = NULL; + std::set *exclude = NULL; + std::set *include = NULL; + std::set *keep_layers = NULL; + std::set *remove_layers = NULL; + int ifmatched = 0; + json_object *filter = NULL; + struct tileset_reader *readers = NULL; + + double minlat, minlon; + double maxlat, maxlon; + double minlon2, maxlon2; +}; + +void append_tile(std::string message, int z, unsigned x, unsigned y, std::map &layermap, std::vector &header, std::map> &mapping, sqlite3 *db, std::set &exclude, std::set &include, std::set &keep_layers, std::set &remove_layers, int ifmatched, mvt_tile &outtile, json_object *filter, struct arg *a) { mvt_tile tile; int features_added = 0; bool was_compressed; @@ -139,96 +253,210 @@ void append_tile(std::string message, int z, unsigned x, unsigned y, std::map>> joined; + if (db != NULL) { + // collect join keys for sql query - for (size_t f = 0; f < layer.features.size(); f++) { - mvt_feature feat = layer.features[f]; - std::set exclude_attributes; + std::vector join_keys; + join_keys.resize(layer.features.size()); - if (filter != NULL && !evaluate(feat, layer, filter, exclude_attributes, z, unidecode_data)) { - continue; - } + for (size_t f = 0; f < layer.features.size(); f++) { + mvt_feature &feat = layer.features[f]; + join_keys[f].type = mvt_no_such_key; - mvt_feature outfeature; - int matched = 0; - - if (feat.has_id) { - outfeature.has_id = true; - outfeature.id = feat.id; - } - - std::map> attributes; - std::vector key_order; - - for (size_t t = 0; t + 1 < feat.tags.size(); t += 2) { - const char *key = layer.keys[feat.tags[t]].c_str(); - mvt_value &val = layer.values[feat.tags[t + 1]]; - serial_val sv = mvt_value_to_serial_val(val); - - if (sv.type == mvt_null) { - continue; - } - - if (include.count(std::string(key)) || (!exclude_all && exclude.count(std::string(key)) == 0 && exclude_attributes.count(std::string(key)) == 0)) { - attributes.insert(std::pair>(key, std::pair(val, sv))); - key_order.push_back(key); - } - - if (header.size() > 0 && strcmp(key, header[0].c_str()) == 0) { - std::map>::iterator ii = mapping.find(sv.s); - - if (ii != mapping.end()) { - std::vector fields = ii->second; - matched = 1; - - for (size_t i = 1; i < fields.size(); i++) { - std::string joinkey = header[i]; - std::string joinval = fields[i]; - int attr_type = mvt_string; - - if (joinval.size() > 0) { - if (joinval[0] == '"') { - joinval = csv_dequote(joinval); - } else if (is_number(joinval)) { - attr_type = mvt_double; - } - } else if (pe) { - attr_type = mvt_null; - } - - const char *sjoinkey = joinkey.c_str(); - - if (include.count(joinkey) || (!exclude_all && exclude.count(joinkey) == 0 && exclude_attributes.count(joinkey) == 0 && attr_type != mvt_null)) { - mvt_value outval; - if (attr_type == mvt_string) { - outval.type = mvt_string; - outval.set_string_value(joinval); - } else { - outval.type = mvt_double; - outval.numeric_value.double_value = atof(joinval.c_str()); - } - - auto fa = attributes.find(sjoinkey); - if (fa != attributes.end()) { - attributes.erase(fa); - } - - serial_val outsv; - outsv.type = outval.type; - outsv.s = joinval; - - // Convert from double to int if the joined attribute is an integer - outval = stringified_to_mvt_value(outval.type, joinval.c_str(), tile_stringpool); - - attributes.insert(std::pair>(joinkey, std::pair(outval, outsv))); - key_order.push_back(joinkey); - } - } + for (size_t t = 0; t + 1 < feat.tags.size(); t += 2) { + const std::string &key = layer.keys[feat.tags[t]]; + if (key == join_tile_attribute) { + const mvt_value &val = layer.values[feat.tags[t + 1]]; + join_keys[f] = val; + break; } } } - if (matched || !ifmatched) { + joined = get_joined_rows(db, join_keys); + } + + auto tilestats = layermap.find(layer.name); + + long long minx = LLONG_MAX; + long long miny = LLONG_MAX; + long long maxx = LLONG_MIN; + long long maxy = LLONG_MIN; + + long long minx2 = LLONG_MAX; + long long maxx2 = LLONG_MIN; + bool features_added_to_layer = false; + + for (size_t f = 0; f < layer.features.size(); f++) { + mvt_feature &feat = layer.features[f]; + + std::set exclude_attributes; + if (filter != NULL && !evaluate(feat, layer, filter, exclude_attributes, z, unidecode_data)) { + continue; + } + + struct match { + bool has_id = false; + unsigned long long id; + std::map> attributes; + std::vector key_order; + }; + + std::vector matches; + bool matched = false; + + // start filling out sql matches + + if (f < joined.size()) { + if (joined[f].size() > 0) { + matched = true; + } + + for (auto const &joined_feature : joined[f]) { + match m; + m.has_id = feat.has_id; + m.id = feat.id; + + if (!exclude_all_tile_attributes) { + for (size_t t = 0; t + 1 < feat.tags.size(); t += 2) { + const std::string &key = layer.keys[feat.tags[t]]; + mvt_value &val = layer.values[feat.tags[t + 1]]; + serial_val sv = mvt_value_to_serial_val(val); + + if (include.count(key) || (!exclude_all && exclude.count(key) == 0 && exclude_attributes.count(key) == 0)) { + m.attributes.insert(std::pair>(key, std::pair(val, sv))); + m.key_order.push_back(key); + } + } + } + + for (auto const &kv : joined_feature) { + if (kv.first == attribute_for_id) { + m.has_id = true; + m.id = mvt_value_to_long_long(kv.second); + } else if (include.count(kv.first) || (!exclude_all && exclude.count(kv.first) == 0 && exclude_attributes.count(kv.first) == 0)) { + if (kv.second.type != mvt_null) { + m.attributes.insert(std::pair>(kv.first, std::pair(kv.second, mvt_value_to_serial_val(kv.second)))); + m.key_order.push_back(kv.first); + } + } + } + + matches.push_back(m); + } + } + + // look for csv matches and start filling them out + + if (!matched) { + match m; + m.id = feat.id; + m.has_id = feat.has_id; + // populate attributes and key_order as we look for matches, + // because apparently at some point i thought it was important + // to insert the joined attributes at the point in the sequence + // where the join key had been + + for (size_t t = 0; t + 1 < feat.tags.size(); t += 2) { + const std::string &key = layer.keys[feat.tags[t]]; + mvt_value &val = layer.values[feat.tags[t + 1]]; + serial_val sv = mvt_value_to_serial_val(val); + + if (val.type == mvt_null) { + continue; + } + + if (!exclude_all_tile_attributes) { + if (include.count(std::string(key)) || (!exclude_all && exclude.count(std::string(key)) == 0 && exclude_attributes.count(std::string(key)) == 0)) { + m.attributes.insert(std::pair>(key, std::pair(val, sv))); + m.key_order.push_back(key); + } + } + + if (header.size() > 0 && key == header[0]) { + std::map>::iterator ii = mapping.find(sv.s); + + if (ii != mapping.end()) { + std::vector fields = ii->second; + matched = true; + + for (size_t i = 1; i < fields.size(); i++) { + std::string joinkey = header[i]; + std::string joinval = fields[i]; + int attr_type = mvt_string; + + if (joinval.size() > 0) { + if (joinval[0] == '"') { + joinval = csv_dequote(joinval); + } else if (is_number(joinval)) { + attr_type = mvt_double; + } + } else if (pe) { + attr_type = mvt_null; + } + + const char *sjoinkey = joinkey.c_str(); + + if (include.count(joinkey) || (!exclude_all && exclude.count(joinkey) == 0 && exclude_attributes.count(joinkey) == 0 && attr_type != mvt_null)) { + mvt_value outval; + if (attr_type == mvt_string) { + outval.type = mvt_string; + outval.set_string_value(joinval); + } else { + outval.type = mvt_double; + outval.numeric_value.double_value = atof(joinval.c_str()); + } + + auto fa = m.attributes.find(sjoinkey); + if (fa != m.attributes.end()) { + m.attributes.erase(fa); + } + + serial_val outsv; + outsv.type = outval.type; + outsv.s = joinval; + + outval = stringified_to_mvt_value(outval.type, joinval.c_str(), tile_stringpool); + + m.attributes.insert(std::pair>(joinkey, std::pair(outval, outsv))); + m.key_order.push_back(joinkey); + } + } + } + } + } + + if (matched) { + matches.push_back(m); + } + } + + if (!matched && !ifmatched) { + // no matches, but they said to keep even unmatched tile features, + // so make one that is just the original feature + + match m; + m.id = feat.id; + m.has_id = feat.has_id; + + if (!exclude_all_tile_attributes) { + for (size_t t = 0; t + 1 < feat.tags.size(); t += 2) { + const std::string &key = layer.keys[feat.tags[t]]; + mvt_value &val = layer.values[feat.tags[t + 1]]; + serial_val sv = mvt_value_to_serial_val(val); + + if (include.count(key) || (!exclude_all && exclude.count(key) == 0 && exclude_attributes.count(key) == 0)) { + m.attributes.insert(std::pair>(key, std::pair(val, sv))); + m.key_order.push_back(key); + } + } + } + + matches.push_back(m); + } + + for (auto &m : matches) { if (tilestats == layermap.end()) { layermap.insert(std::pair(layer.name, layermap_entry(layermap.size()))); tilestats = layermap.find(layer.name); @@ -236,14 +464,18 @@ void append_tile(std::string message, int z, unsigned x, unsigned y, std::mapsecond.maxzoom = z; } - // To keep attributes in their original order instead of alphabetical - for (auto k : key_order) { - auto fa = attributes.find(k); + mvt_feature outfeature; + outfeature.id = m.id; + outfeature.has_id = m.has_id; - if (fa != attributes.end()) { + // To keep attributes in their original order instead of alphabetical + for (auto k : m.key_order) { + auto fa = m.attributes.find(k); + + if (fa != m.attributes.end()) { outlayer.tag(outfeature, k, fa->second.first); add_to_tilestats(tilestats->second.tilestats, k, fa->second.second); - attributes.erase(fa); + m.attributes.erase(fa); } } @@ -257,7 +489,37 @@ void append_tile(std::string message, int z, unsigned x, unsigned y, std::mapsecond.minzoom) { @@ -276,6 +538,24 @@ void append_tile(std::string message, int z, unsigned x, unsigned y, std::mapminlat = std::min(a->minlat, std::min(lat1, lat2)); + a->minlon = std::min(a->minlon, std::min(lon1, lon2)); + a->maxlat = std::max(a->maxlat, std::max(lat1, lat2)); + a->maxlon = std::max(a->maxlon, std::max(lon1, lon2)); + + tile2lonlat(minx2, maxy, 32, &lon1, &lat1); + tile2lonlat(maxx2, miny, 32, &lon2, &lat2); + + a->minlon2 = std::min(a->minlon2, std::min(lon1, lon2)); + a->maxlon2 = std::max(a->maxlon2, std::max(lon1, lon2)); + } } if (features_added == 0) { @@ -283,22 +563,6 @@ void append_tile(std::string message, int z, unsigned x, unsigned y, std::map b) { - return a; - } else { - return b; - } -} - struct tilecmp { bool operator()(std::pair const &a, std::pair const &b) { // must match behavior of tileset_reader::operator<() @@ -736,23 +1000,6 @@ struct tileset_reader *begin_reading(char *fname) { return r; } -struct arg { - std::map> inputs{}; - std::map outputs{}; - - std::map *layermap = NULL; - - std::vector *header = NULL; - std::map> *mapping = NULL; - std::set *exclude = NULL; - std::set *include = NULL; - std::set *keep_layers = NULL; - std::set *remove_layers = NULL; - int ifmatched = 0; - json_object *filter = NULL; - struct tileset_reader *readers = NULL; -}; - void *join_worker(void *v) { arg *a = (arg *) v; @@ -760,7 +1007,7 @@ void *join_worker(void *v) { mvt_tile tile; for (size_t i = 0; i < ai->second.size(); i++) { - append_tile(ai->second[i], ai->first.z, ai->first.x, ai->first.y, *(a->layermap), *(a->header), *(a->mapping), *(a->exclude), *(a->include), *(a->keep_layers), *(a->remove_layers), a->ifmatched, tile, a->filter); + append_tile(ai->second[i], ai->first.z, ai->first.x, ai->first.y, *(a->layermap), *(a->header), *(a->mapping), a->db, *(a->exclude), *(a->include), *(a->keep_layers), *(a->remove_layers), a->ifmatched, tile, a->filter, a); } ai->second.clear(); @@ -795,7 +1042,7 @@ void *join_worker(void *v) { return NULL; } -void dispatch_tasks(std::map> &tasks, std::vector> &layermaps, sqlite3 *outdb, const char *outdir, std::vector &header, std::map> &mapping, std::set &exclude, std::set &include, int ifmatched, std::set &keep_layers, std::set &remove_layers, json_object *filter, struct tileset_reader *readers) { +void dispatch_tasks(std::map> &tasks, std::vector> &layermaps, sqlite3 *outdb, const char *outdir, std::vector &header, std::map> &mapping, sqlite3 *db, std::set &exclude, std::set &include, int ifmatched, std::set &keep_layers, std::set &remove_layers, json_object *filter, struct tileset_reader *readers, double *minlat, double *minlon, double *maxlat, double *maxlon, double *minlon2, double *maxlon2) { pthread_t pthreads[CPUS]; std::vector args; @@ -805,6 +1052,7 @@ void dispatch_tasks(std::map> &tasks, std::vector< args[i].layermap = &layermaps[i]; args[i].header = &header; args[i].mapping = &mapping; + args[i].db = db; args[i].exclude = &exclude; args[i].include = &include; args[i].keep_layers = &keep_layers; @@ -812,6 +1060,12 @@ void dispatch_tasks(std::map> &tasks, std::vector< args[i].ifmatched = ifmatched; args[i].filter = filter; args[i].readers = readers; + args[i].minlat = *minlat; + args[i].minlon = *minlon; + args[i].maxlat = *maxlat; + args[i].maxlon = *maxlon; + args[i].minlon2 = *minlon2; + args[i].maxlon2 = *maxlon2; } size_t count = 0; @@ -844,6 +1098,13 @@ void dispatch_tasks(std::map> &tasks, std::vector< perror("pthread_join"); } + *minlat = std::min(*minlat, args[i].minlat); + *minlon = std::min(*minlon, args[i].minlon); + *maxlat = std::max(*maxlat, args[i].maxlat); + *maxlon = std::max(*maxlon, args[i].maxlon); + *minlon2 = std::min(*minlon2, args[i].minlon2); + *maxlon2 = std::max(*maxlon2, args[i].maxlon2); + for (auto ai = args[i].outputs.begin(); ai != args[i].outputs.end(); ++ai) { if (outdb != NULL) { mbtiles_write_tile(outdb, ai->first.z, ai->first.x, ai->first.y, ai->second.data(), ai->second.size()); @@ -945,7 +1206,7 @@ void handle_vector_layers(json_object *vector_layers, std::map &layermap, sqlite3 *outdb, const char *outdir, struct stats *st, std::vector &header, std::map> &mapping, std::set &exclude, std::set &include, int ifmatched, std::string &attribution, std::string &description, std::set &keep_layers, std::set &remove_layers, std::string &name, json_object *filter, std::map &attribute_descriptions, std::string &generator_options, std::vector *strategies) { +void decode(struct tileset_reader *readers, std::map &layermap, sqlite3 *outdb, const char *outdir, struct stats *st, std::vector &header, std::map> &mapping, sqlite3 *db, std::set &exclude, std::set &include, int ifmatched, std::string &attribution, std::string &description, std::set &keep_layers, std::set &remove_layers, std::string &name, json_object *filter, std::map &attribute_descriptions, std::string &generator_options, std::vector *strategies) { std::vector> layermaps; for (size_t i = 0; i < CPUS; i++) { layermaps.push_back(std::map()); @@ -958,36 +1219,10 @@ void decode(struct tileset_reader *readers, std::mapall_done()) { std::pair current = readers->current(); - if (current.first.z != zoom_for_bbox) { - // Only use highest zoom for bbox calculation - // to avoid z0 always covering the world - - minlat = minlon = minlon2 = INT_MAX; - maxlat = maxlon = maxlon2 = INT_MIN; - zoom_for_bbox = current.first.z; - } - - double lat1, lon1, lat2, lon2; - tile2lonlat(current.first.x, current.first.y, current.first.z, &lon1, &lat1); - tile2lonlat(current.first.x + 1, current.first.y + 1, current.first.z, &lon2, &lat2); - minlat = min(lat2, minlat); - minlon = min(lon1, minlon); - maxlat = max(lat1, maxlat); - maxlon = max(lon2, maxlon); - - if (lon1 < 0) { - lon1 += 360; - lon2 += 360; - } - - minlon2 = min(lon1, minlon2); - maxlon2 = max(lon2, maxlon2); - if (current.first.z >= minzoom && current.first.z <= maxzoom) { zxy tile = current.first; if (tasks.count(tile) == 0) { @@ -1014,7 +1249,7 @@ void decode(struct tileset_reader *readers, std::mapzoom != current.first.z || readers->x != current.first.x || readers->y != current.first.y) { if (tasks.size() > 100 * CPUS) { - dispatch_tasks(tasks, layermaps, outdb, outdir, header, mapping, exclude, include, ifmatched, keep_layers, remove_layers, filter, readers); + dispatch_tasks(tasks, layermaps, outdb, outdir, header, mapping, db, exclude, include, ifmatched, keep_layers, remove_layers, filter, readers, &minlat, &minlon, &maxlat, &maxlon, &minlon2, &maxlon2); tasks.clear(); } } @@ -1033,19 +1268,19 @@ void decode(struct tileset_reader *readers, std::mapminlon = min(minlon, st->minlon); - st->maxlon = max(maxlon, st->maxlon); - st->minlat = min(minlat, st->minlat); - st->maxlat = max(maxlat, st->maxlat); - - st->minlon2 = min(minlon2, st->minlon2); - st->maxlon2 = max(maxlon2, st->maxlon2); - st->minlat2 = min(minlat, st->minlat2); - st->maxlat2 = max(maxlat, st->maxlat2); - - dispatch_tasks(tasks, layermaps, outdb, outdir, header, mapping, exclude, include, ifmatched, keep_layers, remove_layers, filter, readers); + dispatch_tasks(tasks, layermaps, outdb, outdir, header, mapping, db, exclude, include, ifmatched, keep_layers, remove_layers, filter, readers, &minlat, &minlon, &maxlat, &maxlon, &minlon2, &maxlon2); layermap = merge_layermaps(layermaps); + st->minlon = std::min(minlon, st->minlon); + st->maxlon = std::max(maxlon, st->maxlon); + st->minlat = std::min(minlat, st->minlat); + st->maxlat = std::max(maxlat, st->maxlat); + + st->minlon2 = std::min(minlon2, st->minlon2); + st->maxlon2 = std::max(maxlon2, st->maxlon2); + st->minlat2 = std::min(minlat, st->minlat2); + st->maxlat2 = std::max(maxlat, st->maxlat2); + struct tileset_reader *next; for (struct tileset_reader *r = readers; r != NULL; r = next) { next = r->next; @@ -1054,14 +1289,14 @@ void decode(struct tileset_reader *readers, std::mapdb, "SELECT value from metadata where name = 'minzoom'", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { - int minz = max(sqlite3_column_int(stmt, 0), minzoom); - st->minzoom = min(st->minzoom, minz); + int minz = std::max(sqlite3_column_int(stmt, 0), minzoom); + st->minzoom = std::min(st->minzoom, minz); } sqlite3_finalize(stmt); } if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'maxzoom'", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { - int maxz = min(sqlite3_column_int(stmt, 0), maxzoom); + int maxz = std::min(sqlite3_column_int(stmt, 0), maxzoom); if (!want_overzoom) { if (st->maxzoom >= 0 && maxz != st->maxzoom) { @@ -1069,7 +1304,7 @@ void decode(struct tileset_reader *readers, std::mapmaxzoom = max(st->maxzoom, maxz); + st->maxzoom = std::max(st->maxzoom, maxz); } sqlite3_finalize(stmt); } @@ -1116,20 +1351,6 @@ void decode(struct tileset_reader *readers, std::mapdb, "SELECT value from metadata where name = 'bounds'", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - const unsigned char *s = sqlite3_column_text(stmt, 0); - if (s != NULL) { - if (sscanf((char *) s, "%lf,%lf,%lf,%lf", &minlon, &minlat, &maxlon, &maxlat) == 4) { - st->minlon = min(minlon, st->minlon); - st->maxlon = max(maxlon, st->maxlon); - st->minlat = min(minlat, st->minlat); - st->maxlat = max(maxlat, st->maxlat); - } - } - } - sqlite3_finalize(stmt); - } if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'json'", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { const unsigned char *s = sqlite3_column_text(stmt, 0); @@ -1198,6 +1419,8 @@ int main(int argc, char **argv) { int filearg = 0; json_object *filter = NULL; + std::string join_sqlite_fname; + struct tileset_reader *readers = NULL; CPUS = get_num_avail_cpus(); @@ -1210,8 +1433,14 @@ int main(int argc, char **argv) { CPUS = 1; } + if (sqlite3_config(SQLITE_CONFIG_SERIALIZED) != SQLITE_OK) { + fprintf(stderr, "Could not enable sqlite3 serialized multithreading\n"); + exit(EXIT_SQLITE); + } + std::vector header; std::map> mapping; + sqlite3 *db = NULL; std::set exclude; std::set include; @@ -1235,6 +1464,7 @@ int main(int argc, char **argv) { {"exclude", required_argument, 0, 'x'}, {"exclude-all", no_argument, 0, 'X'}, {"include", required_argument, 0, 'y'}, + {"exclude-all-tile-attributes", no_argument, 0, '~'}, {"layer", required_argument, 0, 'l'}, {"exclude-layer", required_argument, 0, 'L'}, {"quiet", no_argument, 0, 'q'}, @@ -1245,6 +1475,13 @@ int main(int argc, char **argv) { {"rename-layer", required_argument, 0, 'R'}, {"read-from", required_argument, 0, 'r'}, + {"join-sqlite", required_argument, 0, '~'}, + {"join-tile-attribute", required_argument, 0, '~'}, + {"join-table-expression", required_argument, 0, '~'}, + {"join-table", required_argument, 0, '~'}, + {"join-count-limit", required_argument, 0, '~'}, + {"use-attribute-for-id", required_argument, 0, '~'}, + {"no-tile-size-limit", no_argument, &pk, 1}, {"no-tile-compression", no_argument, &pC, 1}, {"empty-csv-columns-are-null", no_argument, &pe, 1}, @@ -1429,6 +1666,24 @@ int main(int argc, char **argv) { max_tilestats_values = atoi(optarg); } else if (strcmp(opt, "unidecode-data") == 0) { unidecode_data = read_unidecode(optarg); + } else if (strcmp(opt, "join-sqlite") == 0) { + join_sqlite_fname = optarg; + if (sqlite3_open(optarg, &db) != SQLITE_OK) { + fprintf(stderr, "%s: %s\n", optarg, sqlite3_errmsg(db)); + exit(EXIT_SQLITE); + } + } else if (strcmp(opt, "join-table") == 0) { + join_table = optarg; + } else if (strcmp(opt, "join-table-expression") == 0) { + join_table_expression = optarg; + } else if (strcmp(opt, "join-tile-attribute") == 0) { + join_tile_attribute = optarg; + } else if (strcmp(opt, "use-attribute-for-id") == 0) { + attribute_for_id = optarg; + } else if (strcmp(opt, "exclude-all-tile-attributes") == 0) { + exclude_all_tile_attributes = true; + } else if (strcmp(opt, "join-count-limit") == 0) { + join_count_limit = atoi(optarg); } else { fprintf(stderr, "%s: Unrecognized option --%s\n", argv[0], opt); exit(EXIT_ARGS); @@ -1510,7 +1765,7 @@ int main(int argc, char **argv) { std::string generator_options; std::vector strategies; - decode(readers, layermap, outdb, out_dir, &st, header, mapping, exclude, include, ifmatched, attribution, description, keep_layers, remove_layers, name, filter, attribute_descriptions, generator_options, &strategies); + decode(readers, layermap, outdb, out_dir, &st, header, mapping, db, exclude, include, ifmatched, attribution, description, keep_layers, remove_layers, name, filter, attribute_descriptions, generator_options, &strategies); if (set_attribution.size() != 0) { attribution = set_attribution; @@ -1541,6 +1796,10 @@ int main(int argc, char **argv) { } } + if (st.maxlon < st.minlon) { + st.maxlon = st.minlon = st.maxlat = st.minlat = st.minlon2 = st.maxlon2 = st.minlat2 = st.maxlat2 = 0; + } + if (st.maxlon - st.minlon <= st.maxlon2 - st.minlon2) { st.minlon2 = st.minlon; st.maxlon2 = st.maxlon; diff --git a/version.hpp b/version.hpp index fac06ade..71bf5f69 100644 --- a/version.hpp +++ b/version.hpp @@ -1,6 +1,6 @@ #ifndef VERSION_HPP #define VERSION_HPP -#define VERSION "v2.73.0" +#define VERSION "v2.74.0" #endif