Files
tippecanoe/decode.cpp
Erica FischerandClaude Opus 5 e6e1ec3263 Generate the usage message of each tool from its long_options (#409)
* Generate the usage message of each tool from its long_options

The usage messages of tile-join, tippecanoe-overzoom,
tippecanoe-json-tool, tippecanoe-decode, and tippecanoe-enumerate were
hand-written lists of options that had drifted years out of date, since
nothing tied them to the options that are really accepted. Move the
option-list printing that tippecanoe already does into a shared
print_usage(), and use it in all the tools, so that the message is
derived from the same long_options table that getopt_long() gets and
can't fall behind it again.

The tables now carry section headings, as tippecanoe's does, and the
options that were only reachable by their short names (tile-join's -O,
-b, -R, and -r among them) are listed for the first time.

Also state the non-option arguments the way each tool really treats
them: tile-join takes source tilesets unless --read-from names a file to
read them from, tippecanoe-decode takes a tileset either alone or with a
zoom/x/y, tippecanoe-json-tool reads standard input when no files are
named, and tippecanoe-overzoom's two forms are the ones its argument
parsing recognizes. tippecanoe-overzoom now reports the missing -o
instead of passing NULL to fopen(), and tippecanoe-enumerate goes
through getopt_long() so that it will pick up any options added later.

The shared getopt_string() replaces the identical loop that four of the
tools each had for building the short option string, and strip_usage_headings()
the one for dropping the headings before getopt_long() sees them.

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

* Print the usage message when tippecanoe is run with no arguments

Running `tippecanoe` with nothing at all reported the missing output
file, which is true but is not what someone who typed the bare command
needs to know. Check for the empty command line before parsing and print
the general usage message instead, and leave the specific complaint for
the case where an input file was named but an output file wasn't.

To make the message reachable from there, the options table and the
usage printing move out of main() into a usage() function, as in the
other tools.

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

* Address review: alternation, the dead tile-join option, and --version

Four fixes from review of the generated usage messages:

* `--output` and `--output-to-directory` are one-of, not one required and
  one optional, in both tippecanoe and tile-join. A `usage_required_option`
  can now name an alternation that it belongs to, and the options in one
  are listed together as `(--output=... | --output-to-directory=...)`,
  which is what the runtime check enforces.

* tile-join's `--use-attribute-for-id` has had no implementation since
  533e000 removed it; only the table entry was left behind, so the option
  parsed and then exited with "Unrecognized option". Generating the usage
  message from the table turned that into a documented option that doesn't
  work, so remove the leftover entry too.

* `--version` was grouped under "Progress indicator", in the options table
  and in the README both. Give it a heading of its own now that the
  headings are something users see.

* print_usage() left `width` holding the length of the last synopsis line,
  and only got away with it because every table so far begins with a
  heading, which resets it. Start the option list on a line of its own
  instead of depending on that.

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

---------

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

669 lines
16 KiB
C++

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sqlite3.h>
#include <getopt.h>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <zlib.h>
#include <math.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <protozero/pbf_reader.hpp>
#include "mvt.hpp"
#include "projection.hpp"
#include "geometry.hpp"
#include "write_json.hpp"
#include "jsonpull/jsonpull.h"
#include "mbtiles.hpp"
#include "dirtiles.hpp"
#include "pmtiles_file.hpp"
#include "errors.hpp"
#include "usage.hpp"
int minzoom = 0;
int maxzoom = 32;
bool force = false;
std::set<std::string> include_attr;
bool progress_time() {
return false;
}
void do_stats(mvt_tile &tile, size_t size, bool compressed, int z, unsigned x, unsigned y, json_writer &state) {
state.json_write_hash();
state.json_write_string("zoom");
state.json_write_signed(z);
state.json_write_string("x");
state.json_write_unsigned(x);
state.json_write_string("y");
state.json_write_unsigned(y);
state.json_write_string("bytes");
state.json_write_unsigned(size);
state.json_write_string("compressed");
state.json_write_bool(compressed);
state.json_write_string("layers");
state.json_write_hash();
for (size_t i = 0; i < tile.layers.size(); i++) {
state.json_write_string(tile.layers[i].name);
size_t points = 0, lines = 0, polygons = 0;
for (size_t j = 0; j < tile.layers[i].features.size(); j++) {
if (tile.layers[i].features[j].type == mvt_point) {
points++;
} else if (tile.layers[i].features[j].type == mvt_linestring) {
lines++;
} else if (tile.layers[i].features[j].type == mvt_polygon) {
polygons++;
}
}
state.json_write_hash();
state.json_write_string("points");
state.json_write_unsigned(points);
state.json_write_string("lines");
state.json_write_unsigned(lines);
state.json_write_string("polygons");
state.json_write_unsigned(polygons);
state.json_write_string("extent");
state.json_write_signed(tile.layers[i].extent);
state.json_end_hash();
}
state.json_end_hash();
state.json_end_hash();
state.json_write_newline();
}
void handle(std::string message, int z, unsigned x, unsigned y, std::set<std::string> const &to_decode, bool pipeline, bool stats, json_writer &state, int coordinate_mode) {
mvt_tile tile;
bool was_compressed;
try {
if (!tile.decode(message, was_compressed)) {
fprintf(stderr, "Couldn't parse 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_PROTOBUF);
}
if (stats) {
do_stats(tile, message.size(), was_compressed, z, x, y, state);
return;
}
if (!pipeline) {
state.json_write_hash();
state.json_write_string("type");
state.json_write_string("FeatureCollection");
if (true) {
state.json_write_string("properties");
state.json_write_hash();
state.json_write_string("zoom");
state.json_write_signed(z);
state.json_write_string("x");
state.json_write_signed(x);
state.json_write_string("y");
state.json_write_signed(y);
if (!was_compressed) {
state.json_write_string("compressed");
state.json_write_bool(false);
}
state.json_end_hash();
if (projection != projections) {
state.json_write_string("crs");
state.json_write_hash();
state.json_write_string("type");
state.json_write_string("name");
state.json_write_string("properties");
state.json_write_hash();
state.json_write_string("name");
state.json_write_string(projection->alias);
state.json_end_hash();
state.json_end_hash();
}
}
state.json_write_string("features");
state.json_write_array();
state.json_write_newline();
}
bool first_layer = true;
for (size_t l = 0; l < tile.layers.size(); l++) {
mvt_layer &layer = tile.layers[l];
if (layer.extent <= 0) {
fprintf(stderr, "Impossible layer extent %lld in mbtiles\n", layer.extent);
exit(EXIT_IMPOSSIBLE);
}
if (to_decode.size() != 0 && !to_decode.count(layer.name)) {
continue;
}
if (!pipeline) {
if (true) {
if (!first_layer) {
state.json_comma_newline();
}
state.json_write_hash();
state.json_write_string("type");
state.json_write_string("FeatureCollection");
state.json_write_string("properties");
state.json_write_hash();
state.json_write_string("layer");
state.json_write_string(layer.name);
state.json_write_string("version");
state.json_write_signed(layer.version);
state.json_write_string("extent");
state.json_write_signed(layer.extent);
state.json_end_hash();
state.json_write_string("features");
state.json_write_array();
state.json_write_newline();
first_layer = false;
}
}
// X and Y are unsigned, so no need to check <0
if (x > (1ULL << z) || y > (1ULL << z)) {
fprintf(stderr, "Impossible tile %d/%u/%u\n", z, x, y);
exit(EXIT_IMPOSSIBLE);
}
double scale = 0;
if (coordinate_mode == 1) { // fraction
scale = layer.extent;
} else if (coordinate_mode == 2) { // integer
scale = 1;
}
layer_to_geojson(layer, z, x, y, !pipeline, pipeline, pipeline, false, 0, 0, 0, !force, state, scale, include_attr);
if (!pipeline) {
if (true) {
state.json_end_array();
state.json_end_hash();
state.json_write_newline();
}
}
}
if (!pipeline) {
state.json_end_array();
state.json_end_hash();
state.json_write_newline();
}
}
void decode(char *fname, int z, unsigned x, unsigned y, std::set<std::string> const &to_decode, bool pipeline, bool stats, std::set<std::string> const &exclude_meta, int coordinate_mode) {
sqlite3 *db = NULL;
bool isdir = false;
int oz = z;
unsigned ox = x, oy = y;
json_writer state(stdout);
int fd = open(fname, O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
struct stat st;
if (fstat(fd, &st) == 0) {
char *map = (char *) mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map != NULL && map != MAP_FAILED) {
if (strcmp(map, "SQLite format 3") != 0 && strncmp(map, "PMTiles", 7) != 0) {
if (z >= 0) {
if (st.st_size > 250 * 1024 * 1024) {
fprintf(stderr, "%s: unrealistically large single-tile size %zu\n", fname, (size_t) st.st_size);
exit(EXIT_MEMORY);
} else {
std::string s = std::string(map, st.st_size);
handle(s, z, x, y, to_decode, pipeline, stats, state, coordinate_mode);
munmap(map, st.st_size);
return;
}
} else {
fprintf(stderr, "Must specify zoom/x/y to decode a single pbf file\n");
exit(EXIT_ARGS);
}
}
}
munmap(map, st.st_size);
} else {
perror("fstat");
}
if (close(fd) != 0) {
perror("close");
exit(EXIT_CLOSE);
}
} else {
perror(fname);
}
struct stat st;
std::vector<zxy> tiles;
char *pmtiles_map = NULL;
std::vector<pmtiles::entry_zxy> entries;
bool is_pmtiles = false;
if (stat(fname, &st) == 0 && (st.st_mode & S_IFDIR) != 0) {
isdir = true;
db = dirmeta2tmp(fname);
tiles = enumerate_dirtiles(fname, minzoom, maxzoom);
} 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);
}
db = pmtilesmeta2tmp(fname, pmtiles_map);
entries = pmtiles_entries_tms(pmtiles_map, minzoom, maxzoom);
is_pmtiles = true;
} else {
if (sqlite3_open(fname, &db) != SQLITE_OK) {
fprintf(stderr, "%s: %s\n", fname, sqlite3_errmsg(db));
exit(EXIT_OPEN);
}
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);
}
}
if (z < 0) {
int within = 0;
if (!pipeline && !stats) {
state.json_write_hash();
state.json_write_string("type");
state.json_write_string("FeatureCollection");
state.json_write_string("properties");
state.json_write_hash();
state.json_write_newline();
const char *sql2 = "SELECT name, value from metadata order by name;";
sqlite3_stmt *stmt2;
if (sqlite3_prepare_v2(db, sql2, -1, &stmt2, NULL) != SQLITE_OK) {
fprintf(stderr, "%s: select failed: %s\n", fname, sqlite3_errmsg(db));
exit(EXIT_SQLITE);
}
while (sqlite3_step(stmt2) == SQLITE_ROW) {
const unsigned char *name = sqlite3_column_text(stmt2, 0);
const unsigned char *value = sqlite3_column_text(stmt2, 1);
if (name == NULL || value == NULL) {
fprintf(stderr, "Corrupt mbtiles file: null metadata\n");
exit(EXIT_SQLITE);
}
if (exclude_meta.count((char *) name) == 0) {
if (within) {
state.json_comma_newline();
}
within = 1;
state.json_write_string((char *) name);
state.json_write_string((char *) value);
}
}
state.json_write_newline();
state.wantnl = false; // XXX
sqlite3_finalize(stmt2);
}
if (stats) {
state.json_write_array();
state.json_write_newline();
}
if (!pipeline && !stats) {
state.json_end_hash();
state.json_write_string("features");
state.json_write_array();
state.json_write_newline();
}
if (isdir) {
within = 0;
for (size_t i = 0; i < tiles.size(); i++) {
if (!pipeline && !stats) {
if (within) {
state.json_comma_newline();
}
within = 1;
}
if (stats) {
if (within) {
state.json_comma_newline();
}
within = 1;
}
std::string fn = std::string(fname) + "/" + tiles[i].path();
FILE *f = fopen(fn.c_str(), "rb");
if (f == NULL) {
perror(fn.c_str());
exit(EXIT_OPEN);
}
std::string s;
char buf[2000];
ssize_t n;
while ((n = fread(buf, 1, 2000, f)) > 0) {
s.append(std::string(buf, n));
}
fclose(f);
handle(s, tiles[i].z, tiles[i].x, tiles[i].y, to_decode, pipeline, stats, state, coordinate_mode);
}
} else if (is_pmtiles) {
within = 0;
for (auto const &entry : entries) {
if (!pipeline && !stats) {
if (within) {
state.json_comma_newline();
}
within = 1;
}
if (stats) {
if (within) {
state.json_comma_newline();
}
within = 1;
}
std::string s{pmtiles_map + entry.offset, entry.length};
handle(s, entry.z, entry.x, entry.y, to_decode, pipeline, stats, state, coordinate_mode);
}
} else {
const char *sql = "SELECT tile_data, zoom_level, tile_column, tile_row from tiles where zoom_level between ? and ? order by zoom_level, tile_column, tile_row;";
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);
}
sqlite3_bind_int(stmt, 1, minzoom);
sqlite3_bind_int(stmt, 2, maxzoom);
within = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
if (!pipeline && !stats) {
if (within) {
state.json_comma_newline();
}
within = 1;
}
if (stats) {
if (within) {
state.json_comma_newline();
}
within = 1;
}
int len = sqlite3_column_bytes(stmt, 0);
int tz = sqlite3_column_int(stmt, 1);
int tx = sqlite3_column_int(stmt, 2);
int ty = sqlite3_column_int(stmt, 3);
if (tz < 0 || tz >= 32) {
fprintf(stderr, "Impossible zoom level %d in mbtiles\n", tz);
exit(EXIT_IMPOSSIBLE);
}
ty = (1LL << tz) - 1 - ty;
const char *s = (const char *) sqlite3_column_blob(stmt, 0);
if (s == NULL) {
fprintf(stderr, "Corrupt mbtiles file: null entry in tiles table\n");
exit(EXIT_SQLITE);
}
handle(std::string(s, len), tz, tx, ty, to_decode, pipeline, stats, state, coordinate_mode);
}
sqlite3_finalize(stmt);
}
if (!pipeline && !stats) {
state.json_end_array();
state.json_end_hash();
state.json_write_newline();
}
if (stats) {
state.json_end_array();
state.json_write_newline();
}
if (pipeline) {
state.json_write_newline();
}
} else {
int handled = 0;
while (z >= 0 && !handled) {
if (is_pmtiles) {
uint64_t tile_offset;
uint32_t tile_length;
std::tie(tile_offset, tile_length) = pmtiles_get_tile(pmtiles_map, z, x, y);
if (tile_length > 0) {
if (z != oz) {
fprintf(stderr, "%s: Warning: using tile %d/%u/%u instead of %d/%u/%u\n", fname, z, x, y, oz, ox, oy);
}
std::string s{pmtiles_map + tile_offset, tile_length};
handle(s, z, x, y, to_decode, pipeline, stats, state, coordinate_mode);
handled = 1;
}
} else {
const char *sql = "SELECT tile_data from tiles where zoom_level = ? and tile_column = ? and tile_row = ?;";
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);
}
sqlite3_bind_int(stmt, 1, z);
sqlite3_bind_int(stmt, 2, x);
sqlite3_bind_int(stmt, 3, (1LL << z) - 1 - y);
while (sqlite3_step(stmt) == SQLITE_ROW) {
int len = sqlite3_column_bytes(stmt, 0);
const char *s = (const char *) sqlite3_column_blob(stmt, 0);
if (s == NULL) {
fprintf(stderr, "Corrupt mbtiles file: null entry in tiles table\n");
exit(EXIT_SQLITE);
}
if (z != oz) {
fprintf(stderr, "%s: Warning: using tile %d/%u/%u instead of %d/%u/%u\n", fname, z, x, y, oz, ox, oy);
}
handle(std::string(s, len), z, x, y, to_decode, pipeline, stats, state, coordinate_mode);
handled = 1;
}
sqlite3_finalize(stmt);
}
z--;
x /= 2;
y /= 2;
}
}
if (sqlite3_close(db) != SQLITE_OK) {
fprintf(stderr, "%s: could not close database: %s\n", fname, sqlite3_errmsg(db));
exit(EXIT_CLOSE);
}
}
static const struct option long_options[] = {
{"Tiles to decode", 0, 0, 0},
{"minimum-zoom", required_argument, 0, 'Z'},
{"maximum-zoom", required_argument, 0, 'z'},
{"layer", required_argument, 0, 'l'},
{"Output format", 0, 0, 0},
{"projection", required_argument, 0, 's'},
{"fractional-coordinates", no_argument, 0, 'F'},
{"integer-coordinates", no_argument, 0, 'I'},
{"tag-layer-and-zoom", no_argument, 0, 'c'},
{"stats", no_argument, 0, 'S'},
{"Filtering the output", 0, 0, 0},
{"include", required_argument, 0, 'y'},
{"exclude-metadata-row", required_argument, 0, 'x'},
{"Ignoring errors in the input", 0, 0, 0},
{"force", no_argument, 0, 'f'},
{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] tileset",
"[options] tileset zoom x y",
NULL,
};
print_usage(stderr, argv[0], forms, long_options, NULL);
fprintf(stderr, "\nThe tileset can be an .mbtiles or .pmtiles file or a directory of tiles,\n");
fprintf(stderr, "or, if zoom/x/y is specified, a single .pbf tile.\n");
exit(EXIT_ARGS);
}
int main(int argc, char **argv) {
extern int optind;
extern char *optarg;
int i;
std::set<std::string> to_decode;
bool pipeline = false;
bool stats = false;
std::set<std::string> exclude_meta;
int coordinate_mode = 0;
strip_usage_headings(long_options, real_long_options);
std::string getopt_str = getopt_string(real_long_options);
while ((i = getopt_long(argc, argv, getopt_str.c_str(), real_long_options, NULL)) != -1) {
switch (i) {
case 0:
break;
case 's':
set_projection_or_exit(optarg);
break;
case 'F':
coordinate_mode = 1;
break;
case 'I':
coordinate_mode = 2;
break;
case 'z':
maxzoom = atoi(optarg);
break;
case 'Z':
minzoom = atoi(optarg);
break;
case 'l':
to_decode.insert(optarg);
break;
case 'c':
pipeline = true;
break;
case 'S':
stats = true;
break;
case 'f':
force = true;
break;
case 'x':
exclude_meta.insert(optarg);
break;
case 'y':
include_attr.insert(optarg);
break;
default:
usage(argv);
}
}
if (argc == optind + 4) {
decode(argv[optind], atoi(argv[optind + 1]), atoi(argv[optind + 2]), atoi(argv[optind + 3]), to_decode, pipeline, stats, exclude_meta, coordinate_mode);
} else if (argc == optind + 1) {
decode(argv[optind], -1, -1, -1, to_decode, pipeline, stats, exclude_meta, coordinate_mode);
} else {
usage(argv);
}
return 0;
}