mirror of
https://github.com/felt/tippecanoe.git
synced 2026-10-02 08:25:40 +02:00
Tilesets written with --output-format=mlt could not be read back by any of tippecanoe's own tools, which made MLT a dead end rather than a tile format. Build the mlt-cpp decoder from the vendored maplibre-tile-spec submodule alongside the encoder, and convert a decoded MapLibre Tile back into the equivalent mvt_tile: geometry, feature ids, and property columns. MLT rings come back explicitly closed, so the repeated final point becomes an MVT closepath, and the property columns, which are held in an unordered map, are sorted by name so that the attributes of a decoded tile come out in a stable order. Rather than adding an option to each tool, mvt_tile::decode() detects the encoding and dispatches, so everywhere tippecanoe already reads a vector tile can read MLT. An MLT tile begins with a varint layer length followed by a varint layer tag whose only defined value is 1, while an MVT tile is a protobuf whose only field is the repeated layer field 3, so it begins with 0x1a followed by a layer length that can never be as short as the one byte that would be needed to look like an MLT layer tag. Tile directories also needed a fix: enumerate_dirtiles() recognized .mlt file names but still recorded .pbf as the extension to read them back with, so decoding an MLT directory failed to find any of its tiles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LxhELiLtpUFwrPSWwTYdHG
359 lines
9.2 KiB
C++
359 lines
9.2 KiB
C++
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <algorithm>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <limits.h>
|
|
#include <sys/stat.h>
|
|
#include <sqlite3.h>
|
|
#include "jsonpull/jsonpull.h"
|
|
#include "mbtiles.hpp"
|
|
#include "dirtiles.hpp"
|
|
#include "errors.hpp"
|
|
#include "write_json.hpp"
|
|
|
|
std::string dir_read_tile(std::string base, struct zxy tile) {
|
|
std::ifstream pbfFile(base + "/" + tile.path(), std::ios::in | std::ios::binary);
|
|
std::ostringstream contents;
|
|
contents << pbfFile.rdbuf();
|
|
pbfFile.close();
|
|
|
|
return (contents.str());
|
|
}
|
|
|
|
void dir_write_tile(const char *outdir, int z, int tx, int ty, std::string const &pbf, const char *ext) {
|
|
// Don't check mkdir error returns, since most of these calls to
|
|
// mkdir will be creating directories that already exist.
|
|
mkdir(outdir, S_IRWXU | S_IRWXG | S_IRWXO);
|
|
|
|
std::string curdir(outdir);
|
|
std::string slash("/");
|
|
|
|
std::string newdir = curdir + slash + std::to_string(z);
|
|
mkdir(newdir.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
|
|
|
|
newdir = newdir + "/" + std::to_string(tx);
|
|
mkdir(newdir.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
|
|
|
|
newdir = newdir + "/" + std::to_string(ty) + ext;
|
|
|
|
struct stat st;
|
|
if (stat(newdir.c_str(), &st) == 0) {
|
|
fprintf(stderr, "Can't write tile to already existing %s\n", newdir.c_str());
|
|
exit(EXIT_EXISTS);
|
|
}
|
|
|
|
FILE *fp = fopen(newdir.c_str(), "wb");
|
|
if (fp == NULL) {
|
|
fprintf(stderr, "%s: %s\n", newdir.c_str(), strerror(errno));
|
|
exit(EXIT_WRITE);
|
|
}
|
|
|
|
if (fwrite(pbf.c_str(), sizeof(char), pbf.size(), fp) != pbf.size()) {
|
|
fprintf(stderr, "%s: %s\n", newdir.c_str(), strerror(errno));
|
|
exit(EXIT_WRITE);
|
|
}
|
|
|
|
if (fclose(fp) != 0) {
|
|
fprintf(stderr, "%s: %s\n", newdir.c_str(), strerror(errno));
|
|
exit(EXIT_CLOSE);
|
|
}
|
|
}
|
|
|
|
static bool numeric(const char *s) {
|
|
if (*s == '\0') {
|
|
return false;
|
|
}
|
|
for (; *s != 0; s++) {
|
|
if (*s < '0' || *s > '9') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// If this is the name of a tile within a tile directory, returns the
|
|
// extension that it uses, or NULL if it isn't a tile at all.
|
|
static const char *tile_extension(const char *s) {
|
|
while (*s >= '0' && *s <= '9') {
|
|
s++;
|
|
}
|
|
|
|
if (strcmp(s, ".pbf") == 0 || strcmp(s, ".mvt") == 0 || strcmp(s, ".mlt") == 0) {
|
|
return s;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void check_dir(const char *dir, char **argv, bool force, bool forcetable) {
|
|
struct stat st;
|
|
|
|
mkdir(dir, S_IRWXU | S_IRWXG | S_IRWXO);
|
|
std::string meta = std::string(dir) + "/" + "metadata.json";
|
|
if (force) {
|
|
unlink(meta.c_str()); // error OK since it may not exist;
|
|
} else {
|
|
if (stat(meta.c_str(), &st) == 0) {
|
|
fprintf(stderr, "%s: Tileset \"%s\" already exists. You can use --force if you want to delete the old tileset.\n", argv[0], dir);
|
|
fprintf(stderr, "%s: %s: file exists\n", argv[0], meta.c_str());
|
|
if (!forcetable) {
|
|
exit(EXIT_EXISTS);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (forcetable) {
|
|
// Don't clear existing tiles
|
|
return;
|
|
}
|
|
|
|
std::vector<zxy> tiles = enumerate_dirtiles(dir, INT_MIN, INT_MAX);
|
|
|
|
for (size_t i = 0; i < tiles.size(); i++) {
|
|
std::string fn = std::string(dir) + "/" + tiles[i].path();
|
|
|
|
if (force) {
|
|
if (unlink(fn.c_str()) != 0) {
|
|
perror(fn.c_str());
|
|
exit(EXIT_UNLINK);
|
|
}
|
|
} else {
|
|
fprintf(stderr, "%s: file exists\n", fn.c_str());
|
|
exit(EXIT_EXISTS);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<zxy> enumerate_dirtiles(const char *fname, int minzoom, int maxzoom) {
|
|
std::vector<zxy> tiles;
|
|
|
|
DIR *d1 = opendir(fname);
|
|
if (d1 != NULL) {
|
|
struct dirent *dp;
|
|
while ((dp = readdir(d1)) != NULL) {
|
|
if (numeric(dp->d_name) && atoi(dp->d_name) >= minzoom && atoi(dp->d_name) <= maxzoom) {
|
|
std::string z = std::string(fname) + "/" + dp->d_name;
|
|
int tz = atoi(dp->d_name);
|
|
|
|
DIR *d2 = opendir(z.c_str());
|
|
if (d2 == NULL) {
|
|
perror(z.c_str());
|
|
exit(EXIT_OPEN);
|
|
}
|
|
|
|
struct dirent *dp2;
|
|
while ((dp2 = readdir(d2)) != NULL) {
|
|
if (numeric(dp2->d_name)) {
|
|
std::string x = z + "/" + dp2->d_name;
|
|
int tx = atoi(dp2->d_name);
|
|
|
|
DIR *d3 = opendir(x.c_str());
|
|
if (d3 == NULL) {
|
|
perror(x.c_str());
|
|
exit(EXIT_OPEN);
|
|
}
|
|
|
|
struct dirent *dp3;
|
|
while ((dp3 = readdir(d3)) != NULL) {
|
|
const char *ext = tile_extension(dp3->d_name);
|
|
if (ext != NULL) {
|
|
int ty = atoi(dp3->d_name);
|
|
zxy tile(tz, tx, ty);
|
|
tile.extension = ext;
|
|
|
|
tiles.push_back(tile);
|
|
}
|
|
}
|
|
|
|
closedir(d3);
|
|
}
|
|
}
|
|
|
|
closedir(d2);
|
|
}
|
|
}
|
|
|
|
closedir(d1);
|
|
}
|
|
|
|
std::stable_sort(tiles.begin(), tiles.end());
|
|
return tiles;
|
|
}
|
|
|
|
void dir_erase_zoom(const char *fname, int zoom) {
|
|
DIR *d1 = opendir(fname);
|
|
if (d1 != NULL) {
|
|
struct dirent *dp;
|
|
while ((dp = readdir(d1)) != NULL) {
|
|
if (numeric(dp->d_name) && atoi(dp->d_name) == zoom) {
|
|
std::string z = std::string(fname) + "/" + dp->d_name;
|
|
|
|
DIR *d2 = opendir(z.c_str());
|
|
if (d2 == NULL) {
|
|
perror(z.c_str());
|
|
exit(EXIT_OPEN);
|
|
}
|
|
|
|
struct dirent *dp2;
|
|
while ((dp2 = readdir(d2)) != NULL) {
|
|
if (numeric(dp2->d_name)) {
|
|
std::string x = z + "/" + dp2->d_name;
|
|
|
|
DIR *d3 = opendir(x.c_str());
|
|
if (d3 == NULL) {
|
|
perror(x.c_str());
|
|
exit(EXIT_OPEN);
|
|
}
|
|
|
|
struct dirent *dp3;
|
|
while ((dp3 = readdir(d3)) != NULL) {
|
|
if (tile_extension(dp3->d_name) != NULL) {
|
|
std::string y = x + "/" + dp3->d_name;
|
|
if (unlink(y.c_str()) != 0) {
|
|
perror(y.c_str());
|
|
exit(EXIT_UNLINK);
|
|
}
|
|
}
|
|
}
|
|
|
|
closedir(d3);
|
|
}
|
|
}
|
|
|
|
closedir(d2);
|
|
}
|
|
}
|
|
|
|
closedir(d1);
|
|
}
|
|
}
|
|
|
|
sqlite3 *dirmeta2tmp(const char *fname) {
|
|
sqlite3 *db;
|
|
char *err = NULL;
|
|
|
|
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 name = fname;
|
|
name += "/metadata.json";
|
|
|
|
FILE *f = fopen(name.c_str(), "r");
|
|
if (f == NULL) {
|
|
perror(name.c_str());
|
|
} else {
|
|
json_pull *jp = json_begin_file(f);
|
|
json_object *o = json_read_tree(jp);
|
|
if (o == NULL) {
|
|
fprintf(stderr, "%s: metadata parsing error: %s\n", name.c_str(), jp->error);
|
|
exit(EXIT_JSON);
|
|
}
|
|
|
|
if (o->type != JSON_HASH) {
|
|
fprintf(stderr, "%s: bad metadata format\n", name.c_str());
|
|
exit(EXIT_JSON);
|
|
}
|
|
|
|
for (size_t i = 0; i < o->value.object.length; i++) {
|
|
if (o->value.object.keys[i]->type != JSON_STRING || o->value.object.values[i]->type != JSON_STRING) {
|
|
fprintf(stderr, "%s: non-string in metadata\n", name.c_str());
|
|
}
|
|
|
|
char *sql = sqlite3_mprintf("INSERT INTO metadata (name, value) VALUES (%Q, %Q);", o->value.object.keys[i]->value.string.string, 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", o->value.object.keys[i]->value.string.string, err);
|
|
}
|
|
sqlite3_free(sql);
|
|
}
|
|
|
|
json_end(jp);
|
|
fclose(f);
|
|
}
|
|
|
|
return db;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void dir_write_metadata(const char *outdir, const metadata &m) {
|
|
std::string metadata = std::string(outdir) + "/metadata.json";
|
|
|
|
struct stat st;
|
|
if (stat(metadata.c_str(), &st) == 0) {
|
|
// Leave existing metadata in place with --allow-existing
|
|
} else {
|
|
FILE *fp = fopen(metadata.c_str(), "w");
|
|
if (fp == NULL) {
|
|
perror(metadata.c_str());
|
|
exit(EXIT_OPEN);
|
|
}
|
|
|
|
json_writer state(fp);
|
|
|
|
state.json_write_hash();
|
|
state.json_write_newline();
|
|
|
|
out(state, "name", m.name);
|
|
out(state, "description", m.description);
|
|
out(state, "version", std::to_string(m.version));
|
|
out(state, "minzoom", std::to_string(m.minzoom));
|
|
out(state, "maxzoom", std::to_string(m.maxzoom));
|
|
out(state, "center", std::to_string(m.center_lon) + "," + std::to_string(m.center_lat) + "," + std::to_string(m.center_z));
|
|
out(state, "bounds", std::to_string(m.minlon) + "," + std::to_string(m.minlat) + "," + std::to_string(m.maxlon) + "," + std::to_string(m.maxlat));
|
|
out(state, "antimeridian_adjusted_bounds", std::to_string(m.minlon2) + "," + std::to_string(m.minlat2) + "," + std::to_string(m.maxlon2) + "," + std::to_string(m.maxlat2));
|
|
out(state, "type", m.type);
|
|
if (m.attribution.size() > 0) {
|
|
out(state, "attribution", m.attribution);
|
|
}
|
|
if (m.strategies_json.size() > 0) {
|
|
out(state, "strategies", m.strategies_json);
|
|
}
|
|
if (m.decisions_json.size() > 0) {
|
|
out(state, "tippecanoe_decisions", m.decisions_json);
|
|
}
|
|
out(state, "format", m.format);
|
|
out(state, "generator", m.generator);
|
|
out(state, "generator_options", m.generator_options);
|
|
|
|
if (m.vector_layers_json.size() > 0 || m.tilestats_json.size() > 0) {
|
|
std::string json = "{";
|
|
|
|
if (m.vector_layers_json.size() > 0) {
|
|
json += "\"vector_layers\":" + m.vector_layers_json;
|
|
|
|
if (m.tilestats_json.size() > 0) {
|
|
json += ",\"tilestats\":" + m.tilestats_json;
|
|
}
|
|
} else {
|
|
if (m.tilestats_json.size() > 0) {
|
|
json += "\"tilestats\":" + m.tilestats_json;
|
|
}
|
|
}
|
|
|
|
json += "}";
|
|
|
|
out(state, "json", json);
|
|
}
|
|
|
|
state.json_write_newline();
|
|
state.json_end_hash();
|
|
state.json_write_newline();
|
|
fclose(fp);
|
|
}
|
|
}
|