Files
tippecanoe/tile-join.cpp
Erica FischerandClaude Opus 5 734bba7c78 Fix three latent defects exposed by compiler warnings, and clear the rest (#406)
* Fix variable-length-array and uninitialized-union compiler warnings

Clang warns about every variable-length array in C++ (-Wvla-cxx-extension,
on by default), since VLAs are a compiler extension rather than standard
C++. Replace all 57 of them with std::vector, or with std::string for the
mkstemp() template buffers built from tmpdir. Add -Wvla to WARNING_FLAGS so
new ones don't creep back in.

Separately, mvt_value's numeric_value union is 16 bytes wide (the size of
string_value), but both constructors only wrote the 8 bytes of the member
they were setting, leaving the rest indeterminate. The implicit copy
constructor copies the union as a whole, so copying any non-string value
read uninitialized bytes, which GCC reports as

  mvt.hpp:83:8: warning: 'v.mvt_value::numeric_value. ... .len' may be
  used uninitialized [-Wmaybe-uninitialized]

Give string_value, the widest member, a default member initializer so the
union's full width is initialized however it is later used.

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

* Fix remaining float-conversion and format-truncation warnings

Clang's -Wimplicit-const-int-float-conversion flagged two comparisons
against LLONG_MAX, which is not representable as a double and rounds up
to 2^63.

In serial.cpp this was a real latent overflow, not just noise: the guard
`extent <= LLONG_MAX` was really `extent <= 2^63`, so an extent of exactly
2^63 passed it and then hit `(long long) extent`, which is undefined for
that value and yields LLONG_MIN in practice -- the opposite of the clamp
the else branch intends. Make the bound exclusive so the conversion is
always in range. Requires a polygon area at the very top of the double
range to reach, but the clamp now behaves as written.

In mbtiles.cpp the value is only a stand-in for infinity on its way into
JSON, so cast explicitly; the emitted number is unchanged.

Separately, g++ at -O0 warned that `char abbrev[20]` can be truncated by
"%lld", which is correct: the most negative long long needs 21 bytes with
the NUL. That branch is only reached when point_count < 1000, so it cannot
happen today, but size the buffer to fit rather than rely on that, and
replace the garbled comment about how the size was derived.

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

* Clamp the low end of extent before converting to long long too

The upper bound was fixed in the previous commit; the same overflow exists
on the negative side. get_area() returns a signed shoelace area, so inner
rings contribute negatively, and a polygon whose holes outweigh its rings
drives extent below zero. Far enough below and `(long long) extent` is
undefined again.

The bounds are asymmetric, so this is not simply the mirror of the upper
one: LLONG_MIN is exactly -2^63 and converts exactly, so unlike LLONG_MAX
it can be an inclusive bound.

Verified with -fsanitize=float-cast-overflow that the previous form traps
on 2^63 and on doubles just below -2^63, and that this one is clean across
both boundaries, the infinities, and NaN (which falls to LLONG_MAX, as it
did before).

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

* Add CHANGELOG entries for 2.81.0 and bump the version

CHANGELOG.md was last updated for 2.80.0 (#361), and version.hpp has not
moved since. Twelve PRs have landed in the meantime with no entry: #365,
#368, #375, #382, #384, #385, #391, #395, #397, #399, #400, and #401.

Document all of them, plus this PR, under a single 2.81.0 heading. They are
not given separate version numbers because none of them was ever released
under one -- version.hpp read v2.80.0 throughout -- so assigning a version
per PR would invent release history. 2.81.0 is the version that will
actually carry them.

Where an unreleased PR was corrected by a later one (#384 by #385, #397 by
#399), the pair is described as the single behavior that ships, since the
intermediate behavior was never in a release.

Minor rather than patch bump: the batch adds command-line options.

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

* Review feedback: enforce the union-width assumption, describe both clamp ends

The comment on mvt_value's union claimed string_value is the widest member.
That is true on LP64 (16 bytes against 8) but not on ILP32, where size_t is
4 and it ties with double and long long. The default member initializer still
covers the full union either way, so the fix held, but the justification did
not travel. Replace the claim with a static_assert that checks it on whatever
target is being built, so a platform where it stops holding is a compile
error rather than silently indeterminate bytes. Verified the assert is not
vacuous by widening the union in a scratch copy and watching it fail.

The changelog described only the upper end of the extent clamp. Describe both:
the old guard admitted everything below LLONG_MIN too.

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

* Add 2.81.0 changelog entries for the four PRs merged from main

#404, #408, #409, and #410 landed while this branch was open. None of them
bumped version.hpp, so they belong under the same 2.81.0 heading as the rest
of the unreleased work rather than getting versions of their own.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-06 17:06:07 -07:00

1686 lines
48 KiB
C++

// for vasprintf() on Linux
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#define _DEFAULT_SOURCE
#include <dirent.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sqlite3.h>
#include <limits.h>
#include <getopt.h>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <zlib.h>
#include <math.h>
#include <pthread.h>
#include "mvt.hpp"
#include "projection.hpp"
#include "mbtiles.hpp"
#include "geometry.hpp"
#include "dirtiles.hpp"
#include "pmtiles_file.hpp"
#include "evaluator.hpp"
#include "csv.hpp"
#include "text.hpp"
#include "tile.hpp"
#include "tile-cache.hpp"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <functional>
#include "jsonpull/jsonpull.h"
#include "milo/dtoa_milo.h"
#include "errors.hpp"
#include "geometry.hpp"
#include "thread.hpp"
#include "platform.hpp"
#include "usage.hpp"
int pk = false;
int pC = false;
int pg = false;
int pe = false;
size_t CPUS;
int quiet = false;
int maxzoom = 32;
int minzoom = 0;
std::map<std::string, std::string> renames;
bool exclude_all = false;
bool exclude_all_tile_attributes = false;
bool exclude_all_tile_geometries = false;
std::vector<std::string> unidecode_data;
bool want_overzoom = false;
int buffer = 5;
bool progress_time() {
return false;
}
struct stats {
int minzoom = 0;
int maxzoom = 0;
double midlat = 0, midlon = 0;
double minlat = 0, minlon = 0, maxlat = 0, maxlon = 0;
double minlat2 = 0, minlon2 = 0, maxlat2 = 0, maxlon2 = 0;
std::vector<struct strategy> strategies{};
};
struct arg {
std::map<zxy, std::vector<std::string>> inputs{};
std::map<zxy, std::string> outputs{};
std::map<std::string, layermap_entry> *layermap = NULL;
std::vector<std::string> *header = NULL;
std::map<std::string, std::vector<std::string>> *mapping = NULL;
sqlite3 *db = NULL;
std::set<std::string> *exclude = NULL;
std::set<std::string> *include = NULL;
std::set<std::string> *keep_layers = NULL;
std::set<std::string> *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<std::string, layermap_entry> &layermap, std::vector<std::string> &header, std::map<std::string, std::vector<std::string>> &mapping, sqlite3 * /* db */, std::set<std::string> &exclude, std::set<std::string> &include, std::set<std::string> &keep_layers, std::set<std::string> &remove_layers, int ifmatched, mvt_tile &outtile, json_object *filter, struct arg *a) {
mvt_tile tile;
int features_added = 0;
bool was_compressed;
try {
if (!tile.decode(message, was_compressed)) {
fprintf(stderr, "Couldn't decompress tile %d/%u/%u\n", z, x, y);
exit(EXIT_MVT);
}
} catch (std::exception const &e) {
fprintf(stderr, "PBF decoding error in tile %d/%u/%u\n", z, x, y);
exit(EXIT_MVT);
}
std::shared_ptr<std::string> tile_stringpool = std::make_shared<std::string>();
for (size_t l = 0; l < tile.layers.size(); l++) {
mvt_layer &layer = tile.layers[l];
auto found = renames.find(layer.name);
if (found != renames.end()) {
layer.name = found->second;
}
if (keep_layers.size() > 0 && keep_layers.count(layer.name) == 0) {
continue;
}
if (remove_layers.count(layer.name) != 0) {
continue;
}
size_t ol;
for (ol = 0; ol < outtile.layers.size(); ol++) {
if (tile.layers[l].name == outtile.layers[ol].name) {
break;
}
}
if (ol == outtile.layers.size()) {
outtile.layers.push_back(mvt_layer());
outtile.layers[ol].name = layer.name;
outtile.layers[ol].version = layer.version;
outtile.layers[ol].extent = layer.extent;
}
mvt_layer &outlayer = outtile.layers[ol];
if (layer.extent != outlayer.extent) {
if (layer.extent > outlayer.extent) {
// this always scales up the existing layer instead of scaling down
// the layer that is being added, because the assumption is that
// scaling up is safe while scaling down requires geometry cleaning.
for (size_t i = 0; i < outlayer.features.size(); i++) {
for (size_t j = 0; j < outlayer.features[i].geometry.size(); j++) {
outlayer.features[i].geometry[j].x = outlayer.features[i].geometry[j].x * layer.extent / outlayer.extent;
outlayer.features[i].geometry[j].y = outlayer.features[i].geometry[j].y * layer.extent / outlayer.extent;
}
}
outlayer.extent = layer.extent;
}
}
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<std::string> 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<std::string, std::pair<mvt_value, serial_val>> attributes;
std::vector<std::string> key_order;
};
std::vector<match> matches;
bool matched = false;
// 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<std::string, std::pair<mvt_value, serial_val>>(key, std::pair<mvt_value, serial_val>(val, sv)));
m.key_order.push_back(key);
}
}
if (header.size() > 0 && key == header[0]) {
std::map<std::string, std::vector<std::string>>::iterator ii = mapping.find(sv.s);
if (ii != mapping.end()) {
std::vector<std::string> 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<std::string, std::pair<mvt_value, serial_val>>(joinkey, std::pair<mvt_value, serial_val>(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<std::string, std::pair<mvt_value, serial_val>>(key, std::pair<mvt_value, serial_val>(val, sv)));
m.key_order.push_back(key);
}
}
}
matches.push_back(m);
}
for (auto &m : matches) {
if (tilestats == layermap.end()) {
layermap.insert(std::pair<std::string, layermap_entry>(layer.name, layermap_entry(layermap.size())));
tilestats = layermap.find(layer.name);
tilestats->second.minzoom = z;
tilestats->second.maxzoom = z;
}
mvt_feature outfeature;
outfeature.id = m.id;
outfeature.has_id = m.has_id;
// 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);
m.attributes.erase(fa);
}
}
if (exclude_all_tile_geometries) {
outfeature.type = -1;
} else {
outfeature.type = feat.type;
outfeature.geometry = feat.geometry;
if (layer.extent != outlayer.extent) {
for (size_t i = 0; i < outfeature.geometry.size(); i++) {
outfeature.geometry[i].x = outfeature.geometry[i].x * outlayer.extent / layer.extent;
outfeature.geometry[i].y = outfeature.geometry[i].y * outlayer.extent / layer.extent;
}
}
for (auto const &g : outfeature.geometry) {
if (g.op == mvt_moveto || g.op == mvt_lineto) {
// pin to the tile extent, since we don't want bounds bigger than the earth
long long gx = std::min((long long) outlayer.extent, std::max(0LL, g.x));
long long gy = std::min((long long) outlayer.extent, std::max(0LL, g.y));
// to world scale
gx = gx * (1LL << (32 - z)) / outlayer.extent;
gy = gy * (1LL << (32 - z)) / outlayer.extent;
// to world offset
gx += (1LL << (32 - z)) * x;
gy += (1LL << (32 - z)) * y;
minx = std::min(minx, gx);
miny = std::min(miny, gy);
maxx = std::max(maxx, gx);
maxy = std::max(maxy, gy);
// if in the western hemisphere, try shifting to east
if (gx < (1LL << 31)) {
gx += 1LL << 32;
}
minx2 = std::min(minx2, gx);
maxx2 = std::max(maxx2, gx);
}
}
}
features_added++;
features_added_to_layer = true;
outlayer.features.push_back(outfeature);
if (z < tilestats->second.minzoom) {
tilestats->second.minzoom = z;
}
if (z > tilestats->second.maxzoom) {
tilestats->second.maxzoom = z;
}
if (feat.type == mvt_point) {
tilestats->second.points++;
} else if (feat.type == mvt_linestring) {
tilestats->second.lines++;
} else if (feat.type == mvt_polygon) {
tilestats->second.polygons++;
}
}
}
if (features_added_to_layer) {
double lat1, lon1;
double lat2, lon2;
tile2lonlat(minx, maxy, 32, &lon1, &lat1);
tile2lonlat(maxx, miny, 32, &lon2, &lat2);
a->minlat = 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) {
return;
}
}
struct tilecmp {
bool operator()(std::pair<unsigned, unsigned> const &a, std::pair<unsigned, unsigned> const &b) {
// must match behavior of tileset_reader::operator<()
// except backwards, since we are pulling from the end of the list
if (b.first < a.first) {
return true;
}
if (b.first == a.first) {
// Y sorts backwards, in TMS order
if (b.second > a.second) {
return true;
}
}
return false;
}
} tilecmp;
// The `tileset_reader` is an iterator through the tiles of a tileset,
// in z/x/tms_y order.
//
// The basic idea is that it is used like this:
//
// void blah(const char *fname) {
// tileset_reader r(fname);
//
// for (; !r.all_done(); r.advance()) {
// std::pair<zxy, std::string> tile = r.current();
// whatever(tile);
// }
//
// r.close();
// }
//
// The complication is that you can actually keep calling current()
// and advance() after the tileset_reader claims to be done, in which case
// it will produce overzoomed tiles generated from the tiles in
// the maxzoom tileset. The parent tiles for those overzoomed tiles
// are retrieved internally using get_tile() rather than through the
// main iteration query.
struct tileset_reader {
// z/x/y and data of the current tile
long long zoom = 0;
long long x = 0;
long long y = 0;
std::string data = "";
bool current_tile_is_overzoomed = false;
// "done" means we have read all of the real tiles from the source.
// The iterator will continue to produce overzoomed tiles after it is "done."
bool done = false;
// for overzooming
int maxzoom_so_far = -1;
std::vector<std::pair<unsigned, unsigned>> tiles_at_maxzoom_so_far;
std::vector<std::pair<unsigned, unsigned>> overzoomed_tiles; // tiles at `zoom`
std::vector<std::pair<unsigned, unsigned>> next_overzoomed_tiles; // tiles at `zoom + 1`
bool overzoom_consumed_at_this_zoom = false;
// parent tile cache
tile_cache cache;
// for iterating mbtiles
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
struct tileset_reader *next = NULL;
// for iterating dirtiles
std::vector<zxy> dirtiles;
std::string dirbase;
std::string name;
// for iterating pmtiles
char *pmtiles_map = NULL;
std::vector<pmtiles::entry_zxy> pmtiles_entries;
tileset_reader(const char *fname) {
name = fname;
struct stat st;
if (stat(fname, &st) == 0 && (st.st_mode & S_IFDIR) != 0) {
db = NULL;
stmt = NULL;
next = NULL;
dirtiles = enumerate_dirtiles(fname, minzoom, maxzoom);
dirbase = fname;
} else if (pmtiles_has_suffix(fname)) {
int pmtiles_fd = open(fname, O_RDONLY | O_CLOEXEC);
pmtiles_map = (char *) mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, pmtiles_fd, 0);
if (pmtiles_map == MAP_FAILED) {
perror("mmap in decode");
exit(EXIT_MEMORY);
}
if (::close(pmtiles_fd) != 0) {
perror("close");
exit(EXIT_CLOSE);
}
pmtiles_entries = pmtiles_entries_tms(pmtiles_map, minzoom, maxzoom);
std::reverse(pmtiles_entries.begin(), pmtiles_entries.end());
} else {
if (sqlite3_open(fname, &db) != SQLITE_OK) {
fprintf(stderr, "%s: %s\n", fname, sqlite3_errmsg(db));
exit(EXIT_SQLITE);
}
char *err = NULL;
if (sqlite3_exec(db, "PRAGMA integrity_check;", NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "%s: integrity_check: %s\n", fname, err);
exit(EXIT_SQLITE);
}
const char *sql = "SELECT zoom_level, tile_column, tile_row, tile_data from tiles order by zoom_level, tile_column, tile_row;";
sqlite3_stmt *query;
if (sqlite3_prepare_v2(db, sql, -1, &query, NULL) != SQLITE_OK) {
fprintf(stderr, "%s: select failed: %s\n", fname, sqlite3_errmsg(db));
exit(EXIT_SQLITE);
}
stmt = query;
next = NULL;
}
}
// Checks the done status not only of this tileset_reader but also
// the others chained to it in the queue.
//
// Also claims not to be done if at least one overzoomed tile
// has been consumed at this zoom level, in which case they should
// all allowed to be consumed before stopping.
bool all_done() {
if (!done) {
return false;
}
if (overzoom_consumed_at_this_zoom) {
return false;
}
for (struct tileset_reader *r = next; r != NULL; r = r->next) {
if (!r->done) {
return false;
}
if (r->overzoom_consumed_at_this_zoom) {
return false;
}
}
return true;
}
std::pair<zxy, std::string> current() {
if (current_tile_is_overzoomed) {
overzoom_consumed_at_this_zoom = true;
}
return std::pair<zxy, std::string>(zxy(zoom, x, y), data);
}
void advance() {
if (done) {
if (!want_overzoom) {
fprintf(stderr, "overzoom advance called without -O\n");
exit(EXIT_IMPOSSIBLE);
}
if (overzoomed_tiles.size() == 0) {
next_overzoom();
overzoom_consumed_at_this_zoom = false;
}
if (overzoomed_tiles.size() == 0) {
// we have nothing to overzoom; give up
current_tile_is_overzoomed = false;
zoom = 32;
return;
}
auto xy = overzoomed_tiles.back();
overzoomed_tiles.erase(overzoomed_tiles.begin() + overzoomed_tiles.size() - 1);
x = xy.first;
y = xy.second;
data = retrieve_overzoom(zxy(zoom, x, y));
current_tile_is_overzoomed = true;
return;
}
current_tile_is_overzoomed = false;
if (db != NULL) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
zoom = sqlite3_column_int(stmt, 0);
x = sqlite3_column_int(stmt, 1);
int tms_y = sqlite3_column_int(stmt, 2);
y = (1LL << zoom) - 1 - tms_y;
const char *s = (const char *) sqlite3_column_blob(stmt, 3);
size_t len = sqlite3_column_bytes(stmt, 3);
data = std::string(s, len);
} else {
done = true;
}
} else if (pmtiles_map != NULL) {
if (pmtiles_entries.size() == 0) {
done = true;
} else {
zoom = pmtiles_entries.back().z;
x = pmtiles_entries.back().x;
y = pmtiles_entries.back().y;
data = std::string(pmtiles_map + pmtiles_entries.back().offset, pmtiles_entries.back().length);
pmtiles_entries.pop_back();
}
} else {
if (dirtiles.size() == 0) {
done = true;
} else {
zoom = dirtiles[0].z;
x = dirtiles[0].x;
y = dirtiles[0].y;
data = dir_read_tile(dirbase, dirtiles[0]);
dirtiles.erase(dirtiles.begin());
}
}
if (done) {
if (want_overzoom) {
next_overzoom();
advance();
} else {
zoom = 32;
}
} else {
if (zoom > maxzoom_so_far) {
maxzoom_so_far = zoom;
tiles_at_maxzoom_so_far.clear();
}
if (want_overzoom) {
tiles_at_maxzoom_so_far.push_back(std::pair<unsigned, unsigned>(x, y));
}
}
}
void close() {
if (pmtiles_map) {
db = pmtilesmeta2tmp(name.c_str(), pmtiles_map);
// json, strategies
} else if (db == NULL) {
db = dirmeta2tmp(dirbase.c_str());
} else {
sqlite3_finalize(stmt);
}
}
void next_overzoom() {
zoom++;
overzoomed_tiles.clear();
// +1 because maxzoom_so_far is initially -1, an invalid shift
long long scale = (1LL << (zoom + 1)) / (1LL << (maxzoom_so_far + 1));
// If this is the first overzoomed level, we don't know yet
// which tiles will be useful, so spell out all 4 child tiles
// from each parent tile.
//
// If it is further overzoomed than that, we have a list of
// which child tiles will have features in them, so use that.
if (zoom == maxzoom_so_far + 1) {
for (auto const &xy : tiles_at_maxzoom_so_far) {
for (long long xx = 0; xx < scale; xx++) {
for (long long yy = 0; yy < scale; yy++) {
overzoomed_tiles.push_back(std::pair<unsigned, unsigned>(xy.first * scale + xx, xy.second * scale + yy));
}
}
}
} else {
overzoomed_tiles = std::move(next_overzoomed_tiles);
next_overzoomed_tiles.clear();
}
std::stable_sort(overzoomed_tiles.begin(), overzoomed_tiles.end(), tilecmp);
overzoom_consumed_at_this_zoom = false;
}
mvt_tile get_tile(zxy tile) {
std::string source;
if (db != NULL) {
const char *sql = "SELECT tile_data from tiles where zoom_level = ? and tile_column = ? and tile_row = ?;";
sqlite3_stmt *query;
if (sqlite3_prepare_v2(db, sql, -1, &query, NULL) != SQLITE_OK) {
fprintf(stderr, "%s: select failed: %s\n", name.c_str(), sqlite3_errmsg(db));
exit(EXIT_SQLITE);
}
sqlite3_bind_int(query, 1, tile.z);
sqlite3_bind_int(query, 2, tile.x);
sqlite3_bind_int(query, 3, (1LL << tile.z) - 1 - tile.y);
if (sqlite3_step(query) == SQLITE_ROW) {
const char *s = (const char *) sqlite3_column_blob(query, 0);
size_t len = sqlite3_column_bytes(query, 0);
source = std::string(s, len);
}
sqlite3_finalize(query);
} else if (pmtiles_map != NULL) {
uint64_t tile_offset;
uint32_t tile_length;
std::tie(tile_offset, tile_length) = pmtiles_get_tile(pmtiles_map, tile.z, tile.x, tile.y);
if (tile_length > 0) {
source = std::string(pmtiles_map + tile_offset, tile_length);
}
} else {
source = dir_read_tile(dirbase, tile);
}
mvt_tile content;
if (source.size() == 0) {
return content;
}
try {
bool was_compressed;
if (!content.decode(source, was_compressed)) {
fprintf(stderr, "Couldn't parse tile %lld/%lld/%lld\n", tile.z, tile.x, tile.y);
exit(EXIT_MVT);
}
} catch (std::exception const &e) {
fprintf(stderr, "PBF decoding error in tile %lld/%lld/%lld\n", tile.z, tile.x, tile.y);
exit(EXIT_PROTOBUF);
}
return content;
}
// Sort in z/x/tms_y order, because that is the order of the
// straightforward query of the mbtiles tiles table.
bool operator<(const struct tileset_reader &r) const {
// must match behavior of tilecmp
if (zoom < r.zoom) {
return true;
}
if (zoom > r.zoom) {
return false;
}
if (x < r.x) {
return true;
}
if (x > r.x) {
return false;
}
int sorty = (1LL << zoom) - 1 - y;
int r_sorty = (1LL << r.zoom) - 1 - r.y;
if (sorty < r_sorty) {
return true;
}
if (sorty > r_sorty) {
return false;
}
if (data < r.data) {
return true;
}
return false;
}
std::string retrieve_overzoom(zxy tile) {
// lock around sqlite3 access
static pthread_mutex_t retrieve_lock = PTHREAD_MUTEX_INITIALIZER;
zxy parent_tile = tile;
while (parent_tile.z > maxzoom_so_far) {
parent_tile.z--;
parent_tile.x /= 2;
parent_tile.y /= 2;
}
if (pthread_mutex_lock(&retrieve_lock) != 0) {
perror("pthread_mutex_lock");
}
std::function<mvt_tile(zxy)> getter = [&](zxy tileno) {
return get_tile(tileno);
};
mvt_tile source = cache.get(parent_tile, getter);
if (pthread_mutex_unlock(&retrieve_lock) != 0) {
perror("pthread_mutex_unlock");
}
if (source.layers.size() != 0) {
std::vector<source_tile> tv;
source_tile t;
t.tile = std::move(source);
t.z = parent_tile.z;
t.x = parent_tile.x;
t.y = parent_tile.y;
tv.push_back(std::move(t));
std::string ret = overzoom(tv, tile.z, tile.x, tile.y, -1, buffer,
std::set<std::string>(), std::set<std::string>(), std::vector<std::string>(),
false, &next_overzoomed_tiles, false, NULL, false,
std::unordered_map<std::string, attribute_op>(), unidecode_data, 0, 0,
std::vector<mvt_layer>(), "", "", SIZE_MAX,
std::vector<clipbbox>(), false);
return ret;
}
return "";
}
};
struct tileset_reader *begin_reading(char *fname) {
struct tileset_reader *r = new tileset_reader(fname);
// The reason this prefetches is so the tileset_reader queue can be
// priority-ordered, so the one with the next relevant tile
// is first in line.
r->advance();
return r;
}
void *join_worker(void *v) {
arg *a = (arg *) v;
for (auto ai = a->inputs.begin(); ai != a->inputs.end(); ++ai) {
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->db, *(a->exclude), *(a->include), *(a->keep_layers), *(a->remove_layers), a->ifmatched, tile, a->filter, a);
}
ai->second.clear();
bool anything = false;
mvt_tile outtile;
for (size_t i = 0; i < tile.layers.size(); i++) {
if (tile.layers[i].features.size() > 0) {
outtile.layers.push_back(tile.layers[i]);
anything = true;
}
}
if (anything) {
std::string pbf = outtile.encode();
std::string compressed;
if (!pC) {
compress(pbf, compressed, true);
} else {
compressed = pbf;
}
if (!pk && compressed.size() > 500000) {
fprintf(stderr, "Tile %lld/%lld/%lld size is %lld, >500000. Skipping this tile.\n", ai->first.z, ai->first.x, ai->first.y, (long long) compressed.size());
} else {
a->outputs.insert(std::pair<zxy, std::string>(ai->first, compressed));
}
}
}
return NULL;
}
void dispatch_tasks(std::map<zxy, std::vector<std::string>> &tasks, std::vector<std::map<std::string, layermap_entry>> &layermaps, sqlite3 *outdb, const char *outdir, std::vector<std::string> &header, std::map<std::string, std::vector<std::string>> &mapping, sqlite3 *db, std::set<std::string> &exclude, std::set<std::string> &include, int ifmatched, std::set<std::string> &keep_layers, std::set<std::string> &remove_layers, json_object *filter, struct tileset_reader *readers, double *minlat, double *minlon, double *maxlat, double *maxlon, double *minlon2, double *maxlon2) {
std::vector<pthread_t> pthreads(CPUS);
std::vector<arg> args;
for (size_t i = 0; i < CPUS; i++) {
args.push_back(arg());
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;
args[i].remove_layers = &remove_layers;
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;
// This isn't careful about distributing tasks evenly across CPUs,
// but, from testing, it actually takes a little longer to do
// the proper allocation than is saved by perfectly balanced threads.
for (auto ai = tasks.begin(); ai != tasks.end(); ++ai) {
args[count].inputs.insert(*ai);
count = (count + 1) % CPUS;
if (ai == tasks.begin()) {
if (!quiet) {
fprintf(stderr, "%lld/%lld/%lld \r", ai->first.z, ai->first.x, ai->first.y);
fflush(stderr);
}
}
}
for (size_t i = 0; i < CPUS; i++) {
if (thread_create(&pthreads[i], NULL, join_worker, &args[i]) != 0) {
perror("pthread_create");
exit(EXIT_PTHREAD);
}
}
for (size_t i = 0; i < CPUS; i++) {
void *retval;
if (pthread_join(pthreads[i], &retval) != 0) {
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());
} else if (outdir != NULL) {
dir_write_tile(outdir, ai->first.z, ai->first.x, ai->first.y, ai->second);
}
}
}
}
void handle_strategies(const unsigned char *s, std::vector<strategy> *st) {
json_pull *jp = json_begin_string((const char *) s);
json_object *o = json_read_tree(jp);
if (o != NULL && o->type == JSON_ARRAY) {
for (size_t i = 0; i < o->value.array.length; i++) {
json_object *h = o->value.array.array[i];
if (h->type == JSON_HASH) {
for (size_t j = 0; j < h->value.object.length; j++) {
json_object *k = h->value.object.keys[j];
json_object *v = h->value.object.values[j];
if (k->type != JSON_STRING) {
fprintf(stderr, "Key %zu of %zu is not a string: %s\n", j, i, s);
} else if (v->type != JSON_NUMBER) {
fprintf(stderr, "Value %zu of %zu is not a number: %s\n", j, i, s);
} else {
if (i >= st->size()) {
st->resize(i + 1);
}
if (strcmp(k->value.string.string, "dropped_by_rate") == 0) {
(*st)[i].dropped_by_rate += v->value.number.number;
} else if (strcmp(k->value.string.string, "dropped_by_gamma") == 0) {
(*st)[i].dropped_by_gamma += v->value.number.number;
} else if (strcmp(k->value.string.string, "dropped_as_needed") == 0) {
(*st)[i].dropped_as_needed += v->value.number.number;
} else if (strcmp(k->value.string.string, "coalesced_as_needed") == 0) {
(*st)[i].coalesced_as_needed += v->value.number.number;
} else if (strcmp(k->value.string.string, "truncated_zooms") == 0) {
(*st)[i].truncated_zooms += v->value.number.number;
} else if (strcmp(k->value.string.string, "detail_reduced") == 0) {
(*st)[i].detail_reduced += v->value.number.number;
} else if (strcmp(k->value.string.string, "tiny_polygons") == 0) {
(*st)[i].tiny_polygons += v->value.number.number;
} else if (strcmp(k->value.string.string, "tile_size_desired") == 0) {
(*st)[i].tile_size += v->value.number.number;
} else if (strcmp(k->value.string.string, "feature_count_desired") == 0) {
(*st)[i].feature_count += v->value.number.number;
}
}
}
} else {
fprintf(stderr, "Element %zu is not a hash: %s\n", i, s);
}
}
json_free(o);
}
json_end(jp);
}
void handle_vector_layers(json_object *vector_layers, std::map<std::string, layermap_entry> &layermap, std::map<std::string, std::string> &attribute_descriptions) {
if (vector_layers != NULL && vector_layers->type == JSON_ARRAY) {
for (size_t i = 0; i < vector_layers->value.array.length; i++) {
if (vector_layers->value.array.array[i]->type == JSON_HASH) {
json_object *id = json_hash_get(vector_layers->value.array.array[i], "id");
json_object *desc = json_hash_get(vector_layers->value.array.array[i], "description");
if (id != NULL && desc != NULL && id->type == JSON_STRING && desc->type == JSON_STRING) {
std::string sid = id->value.string.string;
std::string sdesc = desc->value.string.string;
if (sdesc.size() != 0) {
auto f = layermap.find(sid);
if (f != layermap.end()) {
f->second.description = sdesc;
}
}
}
json_object *fields = json_hash_get(vector_layers->value.array.array[i], "fields");
if (fields != NULL && fields->type == JSON_HASH) {
for (size_t j = 0; j < fields->value.object.length; j++) {
if (fields->value.object.keys[j]->type == JSON_STRING && fields->value.object.values[j]->type) {
const char *desc2 = fields->value.object.values[j]->value.string.string;
if (strcmp(desc2, "Number") != 0 &&
strcmp(desc2, "String") != 0 &&
strcmp(desc2, "Boolean") != 0 &&
strcmp(desc2, "Mixed") != 0) {
attribute_descriptions.insert(std::pair<std::string, std::string>(fields->value.object.keys[j]->value.string.string, desc2));
}
}
}
}
}
}
}
}
void decode(struct tileset_reader *readers, std::map<std::string, layermap_entry> &layermap, sqlite3 *outdb, const char *outdir, struct stats *st, std::vector<std::string> &header, std::map<std::string, std::vector<std::string>> &mapping, sqlite3 *db, std::set<std::string> &exclude, std::set<std::string> &include, int ifmatched, std::string &attribution, std::string &description, std::set<std::string> &keep_layers, std::set<std::string> &remove_layers, std::string &name, json_object *filter, std::map<std::string, std::string> &attribute_descriptions, std::string &generator_options, std::vector<strategy> *strategies) {
std::vector<std::map<std::string, layermap_entry>> layermaps;
for (size_t i = 0; i < CPUS; i++) {
layermaps.push_back(std::map<std::string, layermap_entry>());
}
std::map<zxy, std::vector<std::string>> tasks;
double minlat = INT_MAX;
double minlon = INT_MAX;
double maxlat = INT_MIN;
double maxlon = INT_MIN;
double minlon2 = INT_MAX;
double maxlon2 = INT_MIN;
while (readers != NULL && !readers->all_done()) {
std::pair<zxy, std::string> current = readers->current();
if (current.first.z >= minzoom && current.first.z <= maxzoom) {
zxy tile = current.first;
if (tasks.count(tile) == 0) {
tasks.insert(std::pair<zxy, std::vector<std::string>>(tile, std::vector<std::string>()));
}
auto f = tasks.find(tile);
f->second.push_back(current.second);
}
// Advance the tileset_reader that we just added as a task.
// The reason this prefetches is so the tileset_reader queue can be
// priority-ordered, so the one with the next relevant tile
// is first in line.
readers->advance();
// pull the tileset_reader off the front of the queue for reordering
tileset_reader *r = readers;
readers = readers->next;
r->next = NULL;
// Is the next tileset_reader on the tileset_reader queue looking at a different tile?
// Then this tile is done and we can safely run the output queue.
if (readers == NULL || readers->zoom != 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, db, exclude, include, ifmatched, keep_layers, remove_layers, filter, readers, &minlat, &minlon, &maxlat, &maxlon, &minlon2, &maxlon2);
tasks.clear();
}
}
// put the tileset_reader back onto the queue,
// in whatever sequence its next tile calls for
struct tileset_reader **rr;
for (rr = &readers; *rr != NULL; rr = &((*rr)->next)) {
if (*r < **rr) {
break;
}
}
r->next = *rr;
*rr = r;
}
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;
r->close();
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'minzoom'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
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 = std::min(sqlite3_column_int(stmt, 0), maxzoom);
if (!want_overzoom) {
if (st->maxzoom >= 0 && maxz != st->maxzoom) {
fprintf(stderr, "Warning: mismatched maxzooms: %d in %s vs previous %d\n", maxz, r->name.c_str(), st->maxzoom);
}
}
st->maxzoom = std::max(st->maxzoom, maxz);
}
sqlite3_finalize(stmt);
}
if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'center'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char *s = sqlite3_column_text(stmt, 0);
if (s != NULL) {
sscanf((char *) s, "%lf,%lf", &st->midlon, &st->midlat);
}
}
sqlite3_finalize(stmt);
}
if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'attribution'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char *s = sqlite3_column_text(stmt, 0);
if (s != NULL) {
attribution = std::string((char *) s);
}
}
sqlite3_finalize(stmt);
}
if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'description'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char *s = sqlite3_column_text(stmt, 0);
if (s != NULL) {
description = std::string((char *) s);
}
}
sqlite3_finalize(stmt);
}
if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'name'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char *s = sqlite3_column_text(stmt, 0);
if (s != NULL) {
if (name.size() == 0) {
name = std::string((char *) s);
} else {
std::string proposed = name + " + " + std::string((char *) s);
if (proposed.size() < 255) {
name = proposed;
}
}
}
}
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);
if (s != NULL) {
json_pull *jp = json_begin_string((const char *) s);
json_object *o = json_read_tree(jp);
if (o != NULL && o->type == JSON_HASH) {
json_object *vector_layers = json_hash_get(o, "vector_layers");
handle_vector_layers(vector_layers, layermap, attribute_descriptions);
json_free(o);
}
json_end(jp);
}
}
sqlite3_finalize(stmt);
}
if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'generator_options'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char *s = sqlite3_column_text(stmt, 0);
if (s != NULL) {
if (generator_options.size() != 0) {
generator_options.append("; ");
generator_options.append((const char *) s);
} else {
generator_options = (const char *) s;
}
}
}
sqlite3_finalize(stmt);
}
if (sqlite3_prepare_v2(r->db, "SELECT value from metadata where name = 'strategies'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char *s = sqlite3_column_text(stmt, 0);
handle_strategies(s, strategies);
}
sqlite3_finalize(stmt);
}
// Closes either real r->db or temp mirror of metadata.json
if (sqlite3_close(r->db) != SQLITE_OK) {
fprintf(stderr, "Could not close database: %s\n", sqlite3_errmsg(r->db));
exit(EXIT_CLOSE);
}
delete r;
}
}
static const struct option long_options[] = {
{"Output tileset", 0, 0, 0},
{"output", required_argument, 0, 'o'},
{"output-to-directory", required_argument, 0, 'e'},
{"force", no_argument, 0, 'f'},
{"Tileset description and attribution", 0, 0, 0},
{"name", required_argument, 0, 'n'},
{"attribution", required_argument, 0, 'A'},
{"description", required_argument, 0, 'N'},
{"Input tilesets", 0, 0, 0},
{"read-from", required_argument, 0, 'r'},
{"Zoom levels", 0, 0, 0},
{"maximum-zoom", required_argument, 0, 'z'},
{"minimum-zoom", required_argument, 0, 'Z'},
{"overzoom", no_argument, 0, 'O'},
{"buffer", required_argument, 0, 'b'},
{"Layer names", 0, 0, 0},
{"layer", required_argument, 0, 'l'},
{"exclude-layer", required_argument, 0, 'L'},
{"rename-layer", required_argument, 0, 'R'},
{"Joining with a CSV file", 0, 0, 0},
{"csv", required_argument, 0, 'c'},
{"if-matched", no_argument, 0, 'i'},
{"empty-csv-columns-are-null", no_argument, &pe, 1},
{"Filtering feature attributes", 0, 0, 0},
{"exclude", required_argument, 0, 'x'},
{"include", required_argument, 0, 'y'},
{"exclude-all", no_argument, 0, 'X'},
{"exclude-all-tile-attributes", no_argument, 0, '~'},
{"exclude-all-tile-geometries", no_argument, 0, '~'},
{"Filtering features by attributes", 0, 0, 0},
{"feature-filter-file", required_argument, 0, 'J'},
{"feature-filter", required_argument, 0, 'j'},
{"Setting or disabling tile size limits", 0, 0, 0},
{"no-tile-size-limit", no_argument, &pk, 1},
{"no-tile-compression", no_argument, &pC, 1},
{"no-tile-stats", no_argument, &pg, 1},
{"tile-stats-attributes-limit", required_argument, 0, '~'},
{"tile-stats-sample-values-limit", required_argument, 0, '~'},
{"tile-stats-values-limit", required_argument, 0, '~'},
{"Progress indicator", 0, 0, 0},
{"quiet", no_argument, 0, 'q'},
{"", 0, 0, 0},
{"prevent", required_argument, 0, 'p'},
{"unidecode-data", required_argument, 0, '~'},
{0, 0, 0, 0},
};
// the options above, with the usage message headings removed
static struct option real_long_options[sizeof(long_options) / sizeof(long_options[0])];
void usage(char **argv) {
static const char *const forms[] = {
"[options] source.mbtiles ...",
"[options] --read-from=inputfile.txt",
NULL,
};
static const struct usage_required_option required[] = {
{"output", "new.mbtiles", 1},
{"output-to-directory", "directory", 1},
{NULL, NULL, 0},
};
print_usage(stderr, argv[0], forms, long_options, required);
exit(EXIT_ARGS);
}
int main(int argc, char **argv) {
char *out_mbtiles = NULL;
char *out_dir = NULL;
sqlite3 *outdb = NULL;
char *csv = NULL;
int force = 0;
int ifmatched = 0;
int filearg = 0;
json_object *filter = NULL;
std::string join_sqlite_fname;
struct tileset_reader *readers = NULL;
CPUS = get_num_avail_cpus();
const char *TIPPECANOE_MAX_THREADS = getenv("TIPPECANOE_MAX_THREADS");
if (TIPPECANOE_MAX_THREADS != NULL) {
CPUS = atoi(TIPPECANOE_MAX_THREADS);
}
if (CPUS < 1) {
CPUS = 1;
}
if (sqlite3_config(SQLITE_CONFIG_SERIALIZED) != SQLITE_OK) {
fprintf(stderr, "Could not enable sqlite3 serialized multithreading\n");
exit(EXIT_SQLITE);
}
std::vector<std::string> header;
std::map<std::string, std::vector<std::string>> mapping;
sqlite3 *db = NULL;
std::set<std::string> exclude;
std::set<std::string> include;
std::set<std::string> keep_layers;
std::set<std::string> remove_layers;
std::string set_name, set_description, set_attribution;
strip_usage_headings(long_options, real_long_options);
std::string getopt_str = getopt_string(real_long_options);
extern int optind;
extern char *optarg;
int i;
std::string commandline = format_commandline(argc, argv);
int option_index = 0;
while ((i = getopt_long(argc, argv, getopt_str.c_str(), real_long_options, &option_index)) != -1) {
switch (i) {
case 0:
break;
case 'o':
out_mbtiles = optarg;
break;
case 'e':
out_dir = optarg;
break;
case 'f':
force = 1;
break;
case 'O':
want_overzoom = true;
break;
case 'b':
buffer = atoi(optarg);
break;
case 'i':
ifmatched = 1;
break;
case 'A':
set_attribution = optarg;
break;
case 'n':
set_name = optarg;
break;
case 'N':
set_description = optarg;
break;
case 'z':
maxzoom = atoi(optarg);
break;
case 'Z':
minzoom = atoi(optarg);
break;
case 'J':
filter = read_filter(optarg);
break;
case 'j':
filter = parse_filter(optarg);
break;
case 'p':
if (strcmp(optarg, "k") == 0) {
pk = true;
} else if (strcmp(optarg, "C") == 0) {
pC = true;
} else if (strcmp(optarg, "g") == 0) {
pg = true;
} else if (strcmp(optarg, "e") == 0) {
pe = true;
} else {
fprintf(stderr, "%s: Unknown option for -p%s\n", argv[0], optarg);
exit(EXIT_ARGS);
}
break;
case 'c':
if (csv != NULL) {
fprintf(stderr, "Only one -c for now\n");
exit(EXIT_ARGS);
}
csv = optarg;
readcsv(csv, header, mapping);
break;
case 'x':
exclude.insert(std::string(optarg));
break;
case 'X':
exclude_all = true;
break;
case 'y':
exclude_all = true;
include.insert(std::string(optarg));
break;
case 'l':
keep_layers.insert(std::string(optarg));
break;
case 'L':
remove_layers.insert(std::string(optarg));
break;
case 'R': {
char *cp = strchr(optarg, ':');
if (cp == NULL || cp == optarg) {
fprintf(stderr, "%s: -R requires old:new\n", argv[0]);
exit(EXIT_ARGS);
}
std::string before = std::string(optarg).substr(0, cp - optarg);
std::string after = std::string(cp + 1);
renames.insert(std::pair<std::string, std::string>(before, after));
break;
}
case 'r': {
std::fstream read_file;
read_file.open(std::string(optarg), std::ios::in);
if (read_file.is_open()) {
std::string sa;
filearg = 1;
while (getline(read_file, sa)) {
char *c = const_cast<char *>(sa.c_str());
tileset_reader *r = begin_reading(c);
// put the new tileset_reader in priority order
struct tileset_reader **rr;
for (rr = &readers; *rr != NULL; rr = &((*rr)->next)) {
if (*r < **rr) {
break;
}
}
r->next = *rr;
*rr = r;
}
read_file.close();
}
break;
}
case 'q':
quiet = true;
break;
case '~': {
const char *opt = real_long_options[option_index].name;
if (strcmp(opt, "tile-stats-attributes-limit") == 0) {
max_tilestats_attributes = atoi(optarg);
} else if (strcmp(opt, "tile-stats-sample-values-limit") == 0) {
max_tilestats_sample_values = atoi(optarg);
} else if (strcmp(opt, "tile-stats-values-limit") == 0) {
max_tilestats_values = atoi(optarg);
} else if (strcmp(opt, "unidecode-data") == 0) {
unidecode_data = read_unidecode(optarg);
} else if (strcmp(opt, "exclude-all-tile-attributes") == 0) {
exclude_all_tile_attributes = true;
} else if (strcmp(opt, "exclude-all-tile-geometries") == 0) {
exclude_all_tile_geometries = true;
} else {
fprintf(stderr, "%s: Unrecognized option --%s\n", argv[0], opt);
exit(EXIT_ARGS);
}
break;
}
default:
usage(argv);
}
}
if ((argc - optind < 1) && (filearg == 0)) {
usage(argv);
}
if (out_mbtiles == NULL && out_dir == NULL) {
fprintf(stderr, "%s: must specify -o out.mbtiles or -e directory\n", argv[0]);
usage(argv);
}
if (out_mbtiles != NULL && out_dir != NULL) {
fprintf(stderr, "%s: Options -o and -e cannot be used together\n", argv[0]);
usage(argv);
}
if (minzoom > maxzoom) {
fprintf(stderr, "%s: Minimum zoom -Z%d cannot be greater than maxzoom -z%d\n", argv[0], minzoom, maxzoom);
exit(EXIT_ARGS);
}
if (buffer < 0) {
fprintf(stderr, "%s: buffer cannot be less than 0\n", argv[0]);
exit(EXIT_ARGS);
}
if (out_mbtiles != NULL) {
if (force) {
unlink(out_mbtiles);
} else {
if (pmtiles_has_suffix(out_mbtiles)) {
check_pmtiles(out_mbtiles, argv, false);
}
}
outdb = mbtiles_open(out_mbtiles, argv, 0);
}
if (out_dir != NULL) {
check_dir(out_dir, argv, force, false);
}
struct stats st;
st.minzoom = st.minlat = st.minlon = st.minlat2 = st.minlon2 = INT_MAX;
st.maxzoom = st.maxlat = st.maxlon = st.maxlat2 = st.maxlon2 = INT_MIN;
std::map<std::string, layermap_entry> layermap;
std::string attribution;
std::string description;
std::string name;
if (filearg == 0) {
for (i = optind; i < argc; i++) {
tileset_reader *r = begin_reading(argv[i]);
// put the new tileset_reader in priority order
struct tileset_reader **rr;
for (rr = &readers; *rr != NULL; rr = &((*rr)->next)) {
if (*r < **rr) {
break;
}
}
r->next = *rr;
*rr = r;
}
}
std::map<std::string, std::string> attribute_descriptions;
std::string generator_options;
std::vector<strategy> 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;
}
if (set_description.size() != 0) {
description = set_description;
}
if (set_name.size() != 0) {
name = set_name;
}
if (generator_options.size() != 0) {
generator_options.append("; ");
}
generator_options.append(commandline);
// don't trust the source metadata maxzooms;
// claim the zooms that were actually written
st.maxzoom = INT_MIN;
st.minzoom = INT_MAX;
for (auto &l : layermap) {
if (l.second.minzoom < st.minzoom) {
st.minzoom = l.second.minzoom;
}
if (l.second.maxzoom > st.maxzoom) {
st.maxzoom = l.second.maxzoom;
}
}
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;
}
metadata m = make_metadata(name.c_str(), st.minzoom, st.maxzoom, st.minlat, st.minlon, st.maxlat, st.maxlon, st.minlat2, st.minlon2, st.maxlat2, st.maxlon2, st.midlat, st.midlon, attribution.size() != 0 ? attribution.c_str() : NULL, layermap, true, description.c_str(), !pg, attribute_descriptions, "tile-join", generator_options, strategies, st.maxzoom, 2.5, 1);
if (outdb != NULL) {
mbtiles_write_metadata(outdb, m, true);
} else {
dir_write_metadata(out_dir, m);
}
if (outdb != NULL) {
mbtiles_close(outdb, argv[0]);
}
if (filter != NULL) {
json_free(filter);
}
if (pmtiles_has_suffix(out_mbtiles)) {
mbtiles_map_image_to_pmtiles(out_mbtiles, m, !pC, quiet, false);
}
return 0;
}