Files
tippecanoe/mlt_decode.cpp
Claude c70d98afe8 Add a MLT=0 build for a tippecanoe without MapLibre Tile support
MapLibre Tile support pulls in the maplibre-tile-spec submodule and needs
cmake to build it, which is a lot to ask of anyone who only wants to work
with Mapbox Vector Tiles.

`make MLT=0` compiles with -DNO_MLT, skips the submodule and its cmake
build entirely, and drops the MLT tests from `make test`. The result needs
no dependencies beyond the ones MVT already needed, and can be built from
a checkout with no submodules at all.

Everything that touches the MLT library is behind the #ifdef, which is
just the two files that were written for it. The option parsing and the
tile format helpers stay compiled either way, so nothing else needs to
know: --output-format=mlt reports that the build has no MLT support rather
than being an unrecognized value, and a tile that is recognized as MLT
reports the same instead of being misparsed as a protobuf, since the
format sniffing itself doesn't need the library.

CI builds and tests this configuration from a checkout without submodules,
so it can't quietly stop working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxhELiLtpUFwrPSWwTYdHG
2026-08-06 18:05:42 +00:00

312 lines
7.5 KiB
C++

#include "mlt.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#ifndef NO_MLT
#include <mlt/decoder.hpp>
#include <mlt/geometry.hpp>
#include <mlt/layer.hpp>
#include <mlt/properties.hpp>
#include <mlt/tile.hpp>
#endif
namespace {
bool read_varint(const char *&p, const char *end, unsigned long long &out) {
out = 0;
int shift = 0;
while (p < end) {
unsigned char c = (unsigned char) *p++;
if (shift > 63) {
return false;
}
out |= ((unsigned long long) (c & 0x7F)) << shift;
if ((c & 0x80) == 0) {
return true;
}
shift += 7;
}
return false;
}
#ifndef NO_MLT
long long round_coord(float v) {
return (long long) std::llround(v);
}
// Append a sequence of coordinates as a moveto followed by linetos. Rings
// come out of the MLT decoder explicitly closed, but MVT rings are closed
// implicitly by the closepath operation, so the repeated final point is
// dropped.
void add_line(mvt_feature &feature, const mlt::CoordVec &coords, bool ring) {
size_t n = coords.size();
if (ring && n > 1 && coords.front() == coords.back()) {
n--;
}
for (size_t i = 0; i < n; i++) {
feature.geometry.emplace_back(i == 0 ? mvt_moveto : mvt_lineto,
round_coord(coords[i].x), round_coord(coords[i].y));
}
if (ring && n > 0) {
feature.geometry.emplace_back(mvt_closepath, 0, 0);
}
}
void convert_geometry(const mlt::geometry::Geometry &geom, mvt_feature &feature) {
using GeometryType = mlt::metadata::tileset::GeometryType;
namespace geometry = mlt::geometry;
switch (geom.type) {
case GeometryType::POINT: {
const auto &point = static_cast<const geometry::Point &>(geom);
feature.type = mvt_point;
feature.geometry.emplace_back(mvt_moveto,
round_coord(point.getCoordinate().x),
round_coord(point.getCoordinate().y));
break;
}
case GeometryType::MULTIPOINT: {
const auto &multipoint = static_cast<const geometry::MultiPoint &>(geom);
feature.type = mvt_point;
for (const auto &coord : multipoint.getCoordinates()) {
feature.geometry.emplace_back(mvt_moveto, round_coord(coord.x), round_coord(coord.y));
}
break;
}
case GeometryType::LINESTRING: {
const auto &linestring = static_cast<const geometry::LineString &>(geom);
feature.type = mvt_linestring;
add_line(feature, linestring.getCoordinates(), false);
break;
}
case GeometryType::MULTILINESTRING: {
const auto &multilinestring = static_cast<const geometry::MultiLineString &>(geom);
feature.type = mvt_linestring;
for (const auto &linestring : multilinestring.getLineStrings()) {
add_line(feature, linestring, false);
}
break;
}
case GeometryType::POLYGON: {
const auto &polygon = static_cast<const geometry::Polygon &>(geom);
feature.type = mvt_polygon;
for (const auto &ring : polygon.getRings()) {
add_line(feature, ring, true);
}
break;
}
case GeometryType::MULTIPOLYGON: {
const auto &multipolygon = static_cast<const geometry::MultiPolygon &>(geom);
feature.type = mvt_polygon;
for (const auto &polygon : multipolygon.getPolygons()) {
for (const auto &ring : polygon) {
add_line(feature, ring, true);
}
}
break;
}
}
}
// Converts one MLT property into the equivalent MVT value, returning false
// for properties that MVT has no way to represent, which are left off the
// feature instead.
struct property_converter {
mvt_value &out;
const std::shared_ptr<std::string> &pool;
bool operator()(std::nullptr_t) const {
return false;
}
bool operator()(bool v) const {
out.type = mvt_bool;
out.numeric_value.bool_value = v;
return true;
}
bool operator()(std::int32_t v) const {
out.type = mvt_int;
out.numeric_value.int_value = v;
return true;
}
bool operator()(std::int64_t v) const {
out.type = mvt_int;
out.numeric_value.int_value = v;
return true;
}
bool operator()(std::uint32_t v) const {
out.type = mvt_uint;
out.numeric_value.uint_value = v;
return true;
}
bool operator()(std::uint64_t v) const {
out.type = mvt_uint;
out.numeric_value.uint_value = v;
return true;
}
bool operator()(float v) const {
out.type = mvt_float;
out.numeric_value.float_value = v;
return true;
}
bool operator()(double v) const {
out.type = mvt_double;
out.numeric_value.double_value = v;
return true;
}
bool operator()(std::string_view v) const {
out.s = pool;
out.set_string_value(v);
return true;
}
template <typename T>
bool operator()(const std::optional<T> &v) const {
if (v.has_value()) {
return (*this)(*v);
}
return false;
}
};
void convert_layer(const mlt::Layer &in, mvt_layer &out, const std::shared_ptr<std::string> &pool) {
out.version = 2;
out.name = in.getName();
out.extent = in.getExtent();
// The property columns are held in an unordered map, so sort the names
// to keep the attributes of the decoded tile in a stable order.
std::vector<std::pair<const std::string *, const mlt::PresentProperties *>> columns;
columns.reserve(in.getProperties().size());
for (const auto &property : in.getProperties()) {
columns.emplace_back(&property.first, &property.second);
}
std::sort(columns.begin(), columns.end(), [](auto const &a, auto const &b) {
return *a.first < *b.first;
});
out.features.reserve(in.getFeatures().size());
for (const auto &in_feature : in.getFeatures()) {
mvt_feature feature;
if (in_feature.getID()) {
feature.id = *in_feature.getID();
feature.has_id = true;
}
convert_geometry(in_feature.getGeometry(), feature);
for (const auto &column : columns) {
auto property = column.second->getProperty(in_feature.getIndex());
if (!property) {
continue;
}
mvt_value value;
if (std::visit(property_converter{value, pool}, *property)) {
out.tag(feature, *column.first, value);
}
}
out.features.push_back(std::move(feature));
}
}
#endif
} // namespace
bool is_mlt(const std::string &message) {
// An MLT tile is a sequence of layers, each of which is a varint byte
// count followed by a varint tag whose only defined value is 1.
//
// An MVT tile is a protobuf whose only field is the repeated layer
// field 3, so it begins with the byte 0x1a followed by the byte count
// of a layer, which can never be as short as the single byte that
// would be needed to masquerade as an MLT layer tag.
const char *p = message.data();
const char *end = p + message.size();
unsigned long long layer_length;
if (!read_varint(p, end, layer_length)) {
return false;
}
if (layer_length < 2 || layer_length > (unsigned long long) (end - p)) {
return false;
}
unsigned long long layer_tag;
if (!read_varint(p, end, layer_tag)) {
return false;
}
return layer_tag == 1;
}
#ifdef NO_MLT
bool decode_mlt(const std::string &message, mvt_tile &out) {
(void) message;
(void) out;
fprintf(stderr, "This build was compiled without MapLibre Tile support\n");
return false;
}
#else
bool decode_mlt(const std::string &message, mvt_tile &out) {
try {
mlt::Decoder decoder(true);
mlt::MapLibreTile tile = decoder.decode(mlt::DataView(message.data(), message.size()));
std::shared_ptr<std::string> pool = std::make_shared<std::string>();
out.layers.clear();
out.layers.reserve(tile.getLayers().size());
for (const auto &layer : tile.getLayers()) {
mvt_layer out_layer;
convert_layer(layer, out_layer, pool);
out.layers.push_back(std::move(out_layer));
}
return true;
} catch (std::exception const &e) {
fprintf(stderr, "MLT decoding error: %s\n", e.what());
return false;
}
}
#endif