Files
tippecanoe/pmtiles_file.cpp
Erica Fischer 7d4d264d50 Speeding up tippecanoe-overzoom (#191)
* Speed up mvt_value comparison

* Converting repetitive ifs to cases

* More conversions from ifs to cases

* Optimize the always-true filter case

* Don't convert types of attributes without accumulators

* Unordered map seems to be faster than map

* Add missing header

* Fix some warnings

* Fix the warnings better

* Avoid an int->string->int conversion

* Lazily initialize layer key and values maps when actually needed

* More switches from maps to unordered_maps

* Sure, I'll take the microoptimization

* More emplacement

* Save some copies

* Emplaces and moves

* Lazy linear scan of attributes instead of building a map

* Extra printfs, missing header

* Avoid clipping if the input and output tiles are the same

* But do clip if the tile extent is being reduced

* Make sure I'm not constructing std::strings here at runtime

* More worrying about runtime string construction

* A couple more std::moves

* Const references!

* More const references

* Another std::move

* Make the string_value of mvt_value std::optional

* Reserve storage when decoding

* Provision for different mvt_values to share a string pool

* Use the string pool when decoding

* Avoid another string construction

* Try limiting the depth of the search for duplicate attributes

* Revert "Try limiting the depth of the search for duplicate attributes"

This reverts commit 9ec94a15ff.

* Update changelog

* Fix typo noticed during code review
2024-01-29 11:33:56 -08:00

470 lines
15 KiB
C++

#include <unordered_map>
#include <vector>
#include <fstream>
#include <string.h>
#include <algorithm>
#include <unistd.h>
#include <sys/stat.h>
#include <sqlite3.h>
#include "errors.hpp"
#include "pmtiles_file.hpp"
#include "mvt.hpp"
#include "write_json.hpp"
#include "main.hpp"
bool pmtiles_has_suffix(const char *filename) {
if (filename == nullptr) {
return false;
}
size_t lenstr = strlen(filename);
if (lenstr < 8) {
return false;
}
if (strncmp(filename + (lenstr - 8), ".pmtiles", 8) == 0) {
return true;
}
return false;
}
void check_pmtiles(const char *filename, char **argv, bool forcetable) {
struct stat st;
if (stat(filename, &st) == 0) {
fprintf(stderr, "%s: Tileset \"%s\" already exists. You can use --force if you want to delete the old tileset.\n", argv[0], filename);
fprintf(stderr, "%s: %s: file exists\n", argv[0], filename);
if (forcetable) {
fprintf(stderr, "%s: --allow-existing is not supported for pmtiles\n", argv[0]);
}
exit(EXIT_EXISTS);
}
}
std::string decompress_fn(const std::string &input, uint8_t compression) {
std::string output;
if (compression == pmtiles::COMPRESSION_NONE) {
output = input;
} else if (compression == pmtiles::COMPRESSION_GZIP) {
decompress(input, output);
} else {
throw std::runtime_error("Unknown or unsupported compression.");
}
return output;
}
std::string compress_fn(const std::string &input, uint8_t compression) {
std::string output;
if (compression == pmtiles::COMPRESSION_NONE) {
output = input;
} else if (compression == pmtiles::COMPRESSION_GZIP) {
compress(input, output, true);
} else {
throw std::runtime_error("Unknown or unsupported compression.");
}
return output;
}
std::vector<pmtiles::entry_zxy> pmtiles_entries_tms(const char *pmtiles_map, int minzoom, int maxzoom) {
std::vector<pmtiles::entry_zxy> filtered;
auto all_entries = pmtiles::entries_tms(&decompress_fn, pmtiles_map);
std::copy_if(all_entries.begin(), all_entries.end(), std::back_inserter(filtered), [minzoom, maxzoom](pmtiles::entry_zxy e) { return e.z >= minzoom && e.z <= maxzoom; });
return filtered;
}
std::pair<uint64_t, uint32_t> pmtiles_get_tile(const char *pmtiles_map, int z, int x, int y) {
return pmtiles::get_tile(&decompress_fn, pmtiles_map, z, x, y);
}
static void out(json_writer &state, std::string k, std::string v) {
state.json_comma_newline();
state.json_write_string(k);
state.json_write_string(v);
}
std::string metadata_to_pmtiles_json(metadata m) {
std::string buf;
json_writer state(&buf);
state.json_write_hash();
state.json_write_newline();
out(state, "name", m.name);
out(state, "format", m.format);
out(state, "type", m.type);
out(state, "description", m.description);
out(state, "version", std::to_string(m.version));
if (m.attribution.size() > 0) {
out(state, "attribution", m.attribution);
}
if (m.strategies_json.size() > 0) {
state.json_comma_newline();
state.json_write_string("strategies");
state.json_write_json(m.strategies_json);
}
if (m.decisions_json.size() > 0) {
state.json_comma_newline();
state.json_write_string("tippecanoe_decisions");
state.json_write_json(m.decisions_json);
}
out(state, "generator", m.generator);
out(state, "generator_options", m.generator_options);
std::string bounds2 = std::to_string(m.minlon2) + "," + std::to_string(m.minlat2) + "," + std::to_string(m.maxlon2) + "," + std::to_string(m.maxlat2);
out(state, "antimeridian_adjusted_bounds", bounds2);
if (m.vector_layers_json.size() > 0) {
state.json_comma_newline();
state.json_write_string("vector_layers");
state.json_write_json(m.vector_layers_json);
}
if (m.tilestats_json.size() > 0) {
state.json_comma_newline();
state.json_write_string("tilestats");
state.json_write_json(m.tilestats_json);
}
state.json_write_newline();
state.json_end_hash();
state.json_write_newline();
std::string compressed;
compress(buf, compressed, true);
return compressed;
}
void mbtiles_map_image_to_pmtiles(char *fname, metadata m, bool tile_compression, bool fquiet, bool fquiet_progress) {
sqlite3 *db;
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);
}
// materialize list of all tile IDs
std::vector<uint64_t> tile_ids;
{
const char *sql = "SELECT zoom_level, tile_column, tile_row FROM map";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
fprintf(stderr, "%s: select failed: %s\n", fname, sqlite3_errmsg(db));
exit(EXIT_SQLITE);
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
int zoom = sqlite3_column_int(stmt, 0);
int x = sqlite3_column_int(stmt, 1);
int sorty = sqlite3_column_int(stmt, 2);
int y = (1LL << zoom) - 1 - sorty;
uint64_t res = pmtiles::zxy_to_tileid(zoom, x, y);
tile_ids.push_back(res);
}
sqlite3_finalize(stmt);
}
std::sort(tile_ids.begin(), tile_ids.end());
std::unordered_map<std::string, std::pair<unsigned long long, unsigned long>> hash_to_offset_len;
std::vector<pmtiles::entryv3> entries;
unsigned long long offset = 0;
std::string tmpname = (std::string(fname) + ".tmp");
// write tile data to tempfile in clustered order
{
const char *map_sql = "SELECT tile_id FROM map WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?";
sqlite3_stmt *map_stmt;
if (sqlite3_prepare_v2(db, map_sql, -1, &map_stmt, NULL) != SQLITE_OK) {
fprintf(stderr, "%s: select failed: %s\n", fname, sqlite3_errmsg(db));
exit(EXIT_SQLITE);
}
const char *image_sql = "SELECT tile_data FROM images WHERE tile_id = ?";
sqlite3_stmt *image_stmt;
if (sqlite3_prepare_v2(db, image_sql, -1, &image_stmt, NULL) != SQLITE_OK) {
fprintf(stderr, "%s: select failed: %s\n", fname, sqlite3_errmsg(db));
exit(EXIT_SQLITE);
}
std::ofstream tmp_ostream;
tmp_ostream.open(tmpname.c_str(), std::ios::out | std::ios::binary);
int idx = 0;
int progress_reported = -1;
for (auto const &tile_id : tile_ids) {
idx = idx + 1;
double progress = ((double) idx / tile_ids.size()) * 100;
pmtiles::zxy zxy = pmtiles::tileid_to_zxy(tile_id);
if (!fquiet && !fquiet_progress && progress_time() && (int) progress != progress_reported) {
fprintf(stderr, " %3.1f%% %d/%u/%u \r", progress, zxy.z, zxy.x, zxy.y);
progress_reported = (int) progress;
fflush(stderr);
}
sqlite3_bind_int(map_stmt, 1, zxy.z);
sqlite3_bind_int(map_stmt, 2, zxy.x);
sqlite3_bind_int(map_stmt, 3, (1LL << zxy.z) - 1 - zxy.y);
if (sqlite3_step(map_stmt) != SQLITE_ROW) {
fprintf(stderr, "Corrupt mbtiles file: null entry in map table\n");
exit(EXIT_SQLITE);
}
std::string hsh{reinterpret_cast<const char *>(sqlite3_column_text(map_stmt, 0))};
if (hash_to_offset_len.count(hsh) > 0) {
auto offset_len = hash_to_offset_len.at(hsh);
if (entries.size() > 0 && tile_id == entries[entries.size() - 1].tile_id + 1 && entries[entries.size() - 1].offset == std::get<0>(offset_len)) {
entries[entries.size() - 1].run_length++;
} else {
entries.emplace_back(tile_id, std::get<0>(offset_len), std::get<1>(offset_len), 1);
}
} else {
sqlite3_bind_text(image_stmt, 1, hsh.data(), hsh.size(), SQLITE_STATIC);
if (sqlite3_step(image_stmt) != SQLITE_ROW) {
fprintf(stderr, "Corrupt mbtiles file: null entry in image table\n");
exit(EXIT_SQLITE);
}
int len = sqlite3_column_bytes(image_stmt, 0);
const char *blob = (const char *) sqlite3_column_blob(image_stmt, 0);
tmp_ostream.write(blob, len);
entries.emplace_back(tile_id, offset, len, 1);
hash_to_offset_len.emplace(hsh, std::make_pair(offset, len));
offset += len;
sqlite3_reset(image_stmt);
sqlite3_clear_bindings(image_stmt);
}
sqlite3_reset(map_stmt);
sqlite3_clear_bindings(map_stmt);
}
tmp_ostream.close();
sqlite3_finalize(map_stmt);
sqlite3_finalize(image_stmt);
}
// finalize PMTiles archive.
{
std::sort(entries.begin(), entries.end(), pmtiles::entryv3_cmp());
std::string root_bytes;
std::string leaves_bytes;
int num_leaves;
std::tie(root_bytes, leaves_bytes, num_leaves) = make_root_leaves(&compress_fn, pmtiles::COMPRESSION_GZIP, entries);
pmtiles::headerv3 header;
header.min_zoom = m.minzoom;
header.max_zoom = m.maxzoom;
header.min_lon_e7 = m.minlon * 10000000;
header.min_lat_e7 = m.minlat * 10000000;
header.max_lon_e7 = m.maxlon * 10000000;
header.max_lat_e7 = m.maxlat * 10000000;
header.center_zoom = m.center_z;
header.center_lon_e7 = m.center_lon * 10000000;
header.center_lat_e7 = m.center_lat * 10000000;
std::string json_metadata = metadata_to_pmtiles_json(m);
sqlite3_close(db);
header.clustered = 0x1;
header.internal_compression = pmtiles::COMPRESSION_GZIP;
if (tile_compression) {
header.tile_compression = pmtiles::COMPRESSION_GZIP;
} else {
header.tile_compression = pmtiles::COMPRESSION_NONE;
}
if (m.format == "pbf") {
header.tile_type = pmtiles::TILETYPE_MVT;
} else if (m.format == "png") {
header.tile_type = pmtiles::TILETYPE_PNG;
} else {
header.tile_type = pmtiles::TILETYPE_UNKNOWN;
}
header.root_dir_offset = 127;
header.root_dir_bytes = root_bytes.size();
header.json_metadata_offset = header.root_dir_offset + header.root_dir_bytes;
header.json_metadata_bytes = json_metadata.size();
header.leaf_dirs_offset = header.json_metadata_offset + header.json_metadata_bytes;
header.leaf_dirs_bytes = leaves_bytes.size();
header.tile_data_offset = header.leaf_dirs_offset + header.leaf_dirs_bytes;
header.tile_data_bytes = offset;
header.addressed_tiles_count = tile_ids.size();
header.tile_entries_count = entries.size();
header.tile_contents_count = hash_to_offset_len.size();
std::ifstream tmp_istream(tmpname.c_str(), std::ios::in | std::ios_base::binary);
std::ofstream ostream;
ostream.open(fname, std::ios::out | std::ios::binary);
auto header_str = header.serialize();
ostream.write(header_str.data(), header_str.length());
ostream.write(root_bytes.data(), root_bytes.length());
ostream.write(json_metadata.data(), json_metadata.size());
ostream.write(leaves_bytes.data(), leaves_bytes.length());
ostream << tmp_istream.rdbuf();
tmp_istream.close();
unlink(tmpname.c_str());
ostream.close();
}
}
// this should go away if we get rid of temporary metadata DBs.
// it transforms the PMTiles header + json into string keys/string values
// consistent with mbtiles and dirtiles, for the test suite
sqlite3 *pmtilesmeta2tmp(const char *fname, const char *pmtiles_map) {
sqlite3 *db;
char *sql;
char *err;
if (sqlite3_open("", &db) != SQLITE_OK) {
fprintf(stderr, "Temporary db: %s\n", sqlite3_errmsg(db));
exit(EXIT_SQLITE);
}
if (sqlite3_exec(db, "CREATE TABLE metadata (name text, value text);", NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "Create metadata table: %s\n", err);
exit(EXIT_SQLITE);
}
std::string header_s{pmtiles_map, 127};
auto header = pmtiles::deserialize_header(header_s);
sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES ('minzoom', %d);", header.min_zoom);
if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "set minzoom: %s\n", err);
}
sqlite3_free(sql);
sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES ('maxzoom', %d);", header.max_zoom);
if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "set maxzoom: %s\n", err);
}
sqlite3_free(sql);
double center_lon = double(header.center_lon_e7) / 10000000.0;
double center_lat = double(header.center_lat_e7) / 10000000.0;
sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES ('center', '%f,%f,%d');", center_lon, center_lat, header.center_zoom);
if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "set center: %s\n", err);
}
sqlite3_free(sql);
double minlon = double(header.min_lon_e7) / 10000000.0;
double minlat = double(header.min_lat_e7) / 10000000.0;
double maxlon = double(header.max_lon_e7) / 10000000.0;
double maxlat = double(header.max_lat_e7) / 10000000.0;
sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES ('bounds', '%f,%f,%f,%f');", minlon, minlat, maxlon, maxlat);
if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "set bounds: %s\n", err);
}
sqlite3_free(sql);
std::string json_s{pmtiles_map + header.json_metadata_offset, header.json_metadata_bytes};
std::string decompressed_json;
if (header.internal_compression == pmtiles::COMPRESSION_NONE) {
decompressed_json = json_s;
} else if (header.internal_compression == pmtiles::COMPRESSION_GZIP) {
decompress(json_s, decompressed_json);
} else {
fprintf(stderr, "Unknown or unsupported pmtiles compression: %d\n", header.internal_compression);
exit(EXIT_OPEN);
}
json_pull *jp = json_begin_string(decompressed_json.c_str());
json_object *o = json_read_tree(jp);
if (o == NULL) {
fprintf(stderr, "%s: metadata parsing error: %s\n", fname, jp->error);
exit(EXIT_JSON);
}
if (o->type != JSON_HASH) {
fprintf(stderr, "%s: bad metadata format\n", fname);
exit(EXIT_JSON);
}
bool has_json = false;
std::string buf;
json_writer state(&buf);
state.nospace = true;
state.json_write_hash();
for (size_t i = 0; i < o->value.object.length; i++) {
const char *key = o->value.object.keys[i]->value.string.string;
if (strcmp(key, "vector_layers") == 0 && o->value.object.values[i]->type == JSON_ARRAY) {
has_json = true;
state.nospace = true;
state.json_write_string("vector_layers");
state.nospace = true;
state.json_write_json(json_stringify(o->value.object.values[i]));
} else if (strcmp(key, "tilestats") == 0 && o->value.object.values[i]->type == JSON_HASH) {
has_json = true;
state.nospace = true;
state.json_write_string("tilestats");
state.nospace = true;
state.json_write_json(json_stringify(o->value.object.values[i]));
} else if (strcmp(key, "strategies") == 0 && o->value.object.values[i]->type == JSON_ARRAY) {
sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES ('strategies', %Q);", json_stringify(o->value.object.values[i]));
if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "set %s in metadata: %s\n", key, err);
}
sqlite3_free(sql);
} else if (strcmp(key, "tippecanoe_decisions") == 0 && o->value.object.values[i]->type == JSON_HASH) {
sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES ('tippecanoe_decisions', %Q);", json_stringify(o->value.object.values[i]));
if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "set %s in metadata: %s\n", key, err);
}
sqlite3_free(sql);
} else if (o->value.object.keys[i]->type != JSON_STRING || o->value.object.values[i]->type != JSON_STRING) {
fprintf(stderr, "%s\n", key);
fprintf(stderr, "%s: non-string in metadata\n", fname);
} else {
sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES (%Q, %Q);", key, o->value.object.values[i]->value.string.string);
if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "set %s in metadata: %s\n", key, err);
}
sqlite3_free(sql);
}
}
json_end(jp);
state.nospace = true;
state.json_end_hash();
if (has_json) {
sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES ('json', %Q);", buf.c_str());
if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "set json in metadata: %s\n", err);
}
sqlite3_free(sql);
}
return db;
}