Files
tippecanoe/pmtiles_file.cpp
T
Erica Fischer f2127fec97 Reduce thrashing during feature ingestion and tiling (#56)
* Remove the concept of "separate metadata"

This was an extra level of attribute indirection (features point
to metadata records which point to key and value strings) which was
intended to reduce the size of temporary storage for features with
large numbers of attributes that were also spread across large numbers
of tiles at maxzoom.

For other kinds of features, the extra indirection slowed things down
instead, and, especially when maxzoom guessing was being used, many more
features were having their metadata externalized than could actually
benefit from it.

* Shave a few bytes off temporary files by using more unsigned integers

* Flush stderr after logging progress

* Revert "Shave a few bytes off temporary files by using more unsigned integers"

This reverts commit eef29084ec.

* Limit the size of the string pools and trees to fit in memory

* Add missing #include

* Move the string pool and search tree from mmap to allocated memory

* Sort in allocated rather than mapped memory too

* Also use pread instead of mapping to read in the data to sort

* When the pool gets too big, switch to just the file, not memory

* Switch string pool from memory to disk when memory is 10% full

* Add to-memory versions of the serialization functions

* Crashy work in progress toward compression

* Fix the pointer bug that was causing the crash

* Serialize features into memory rather than straight to disk

* Compress individual features in the temporary files

* Don't need to store the length of the geometry

* Remove per-feature compression; move minzoom back into the object

* Start adding a stream compressor object

* Track file position within fwrite_check()

* Add compressed stream writer functions

* Pull the writing of the serialized feature out to the callers

* Starting toward compression again from a different point

* Hook up more compression functions

* Remove unused code from the other day

* Make enough deflate calls to flush out all the buffered data

* Start on decompression

* Tile number is uncompressed, tile content is compressed

* Work on alternating compressed and uncompressed in decompression

* Closer, but still doesn't work

* Sort of works

* Works until we get to concatenated tiles

* More attempts that don't work

* One bug down

* It made a tileset!

* Handle nonzero initial zooms

* Fix seeking within compressed feature streams

* Tests pass!

* Remove debug spew

* Oops: remember to delete the temporary files so they don't hang around

* Test that fails with the current compression code

* Properly account for bytes read while closing the compressed stream

* Limit the number of warnings about bad label points

* A little more armor when closing decompression

* This time for sure

* A different, less fragile, test that failed previously with compression

* Move feature stream compression to its own file

* Remove now-unused code to deserialize from a file

* Forgot to add the new files

* Remove a little debugging logging

* Add a couple of comments on what it means to be within decompression

* Fix indentation

* Update changelog. Remove stray debugging comment.
2023-02-14 12:47:40 -08:00

451 lines
14 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"
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);
}
out(state, "generator", m.generator);
out(state, "generator_options", m.generator_options);
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 quiet, bool quiet_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;
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 (!quiet && !quiet_progress) {
fprintf(stderr, " %3.1f%% %d/%u/%u \r", progress, zxy.z, zxy.x, zxy.y);
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 (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;
}