From 53b98d76c48c05b10e5c9fd3bf829c6028e1ef86 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 4 Feb 2026 16:18:54 +0300 Subject: [PATCH 1/4] Add MLT (MapLibre Tile) output format Integrate the C++ MLT encoder from maplibre-tile-spec as a submodule. Tiles are encoded by converting the existing mvt_tile to mlt::Encoder::Layer and calling the encoder library directly. New CLI flags: --output-format=mlt Encode tiles as MLT instead of MVT --pretessellate Pre-triangulate polygons (MLT only) --no-mlt-feature-sort Disable within-tile Hilbert sorting (MLT only) MLT tiles use .mlt extension in directory output and format=mlt in mbtiles metadata. Compression and all existing flags work unchanged. --- .gitignore | 4 + .gitmodules | 4 + Makefile | 47 +++++++++- README.md | 7 +- dirtiles.cpp | 6 +- dirtiles.hpp | 2 +- main.cpp | 22 ++++- main.hpp | 7 ++ maplibre-tile-spec | 1 + mbtiles.cpp | 7 +- mbtiles.hpp | 2 +- mlt.cpp | 191 +++++++++++++++++++++++++++++++++++++++ mlt.hpp | 9 ++ tests/mlt/points.geojson | 5 + tile.cpp | 11 ++- 15 files changed, 309 insertions(+), 16 deletions(-) create mode 100644 .gitmodules create mode 160000 maplibre-tile-spec create mode 100644 mlt.cpp create mode 100644 mlt.hpp create mode 100644 tests/mlt/points.geojson diff --git a/.gitignore b/.gitignore index 9f2b22afb..dff9b97a0 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,9 @@ tests/**/*.mbtiles tests/**/*.check tests/**/*.geobuf +# MLT build directory +mlt-build/ + # Vim *.swp @@ -51,3 +54,4 @@ tests/**/*.geobuf # Nodejs node_modules +tmp/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..7c4755cdc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "maplibre-tile-spec"] + path = maplibre-tile-spec + url = https://github.com/dannote/maplibre-tile-spec.git + branch = feature/cpp-encoder diff --git a/Makefile b/Makefile index 0b9883993..d44357761 100644 --- a/Makefile +++ b/Makefile @@ -57,9 +57,22 @@ H = $(wildcard *.h) $(wildcard *.hpp) C = $(wildcard *.c) $(wildcard *.cpp) INCLUDES = -I/usr/local/include -I. -Iclipper2/include +MLT_INCLUDES = -Imaplibre-tile-spec/cpp/include -isystem maplibre-tile-spec/cpp/vendor/fsst +MLT_LIBS = mlt-build/libmlt-cpp-encoder.a mlt-build/libfsst-lib.a LIBS = -L/usr/local/lib -tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o serial.o main.o platform.o text.o dirtiles.o pmtiles_file.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o visvalingam.o compression.o clip.o sort.o attribute.o thread.o shared_borders.o clipper2/src/clipper.engine.o +mlt-build/libmlt-cpp-encoder.a: maplibre-tile-spec/cpp/CMakeLists.txt + cmake -S maplibre-tile-spec/cpp -B mlt-build \ + -DCMAKE_BUILD_TYPE=Release \ + -DMLT_WITH_FASTPFOR=OFF \ + -DMLT_WITH_JSON=OFF \ + -DMLT_WITH_TESTS=OFF \ + -DMLT_WITH_TOOLS=OFF \ + -DCMAKE_CXX_STANDARD=20 \ + $(if $(VERBOSE),,--log-level=WARNING) > /dev/null + cmake --build mlt-build --target mlt-cpp-encoder $(if $(VERBOSE),,-- -s) > /dev/null + +tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o mlt.o serial.o main.o platform.o text.o dirtiles.o pmtiles_file.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o visvalingam.o compression.o clip.o sort.o attribute.o thread.o shared_borders.o clipper2/src/clipper.engine.o $(MLT_LIBS) $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread tippecanoe-enumerate: enumerate.o @@ -85,11 +98,14 @@ tippecanoe-overzoom: overzoom.o mvt.o clip.o evaluator.o jsonpull/jsonpull.o tex %.o: %.c $(CC) -MMD $(PG) $(INCLUDES) $(FINAL_FLAGS) $(CFLAGS) -c -o $@ $< +mlt.o: mlt.cpp mlt-build/libmlt-cpp-encoder.a + $(CXX) -MMD $(PG) $(INCLUDES) $(MLT_INCLUDES) $(FINAL_FLAGS) $(CXXFLAGS) -std=c++20 -c -o $@ $< + %.o: %.cpp $(CXX) -MMD $(PG) $(INCLUDES) $(FINAL_FLAGS) $(CXXFLAGS) -c -o $@ $< clean: - rm -f ./tippecanoe ./tippecanoe-* ./tile-join ./unit *.o *.d */*.o */*.d tests/**/*.mbtiles tests/**/*.check + rm -rf ./tippecanoe ./tippecanoe-* ./tile-join ./unit *.o *.d */*.o */*.d tests/**/*.mbtiles tests/**/*.check mlt-build indent: clang-format -i -style="{BasedOnStyle: Google, IndentWidth: 8, UseTab: Always, AllowShortIfStatementsOnASingleLine: false, ColumnLimit: 0, ContinuationIndentWidth: 8, SpaceAfterCStyleCast: true, IndentCaseLabels: false, AllowShortBlocksOnASingleLine: false, AllowShortFunctionsOnASingleLine: false, SortIncludes: false}" $(filter-out flatgeobuf.cpp,$(C)) $(H) jsonpull/*.[ch] @@ -97,7 +113,7 @@ indent: TESTS = $(wildcard tests/*/out/*.json) SPACE = $(NULL) $(NULL) -test: tippecanoe tippecanoe-decode $(addsuffix .check,$(TESTS)) raw-tiles-test parallel-test pbf-test join-test enumerate-test decode-test join-filter-test unit json-tool-test allow-existing-test csv-test layer-json-test pmtiles-test decode-pmtiles-test overzoom-test +test: tippecanoe tippecanoe-decode $(addsuffix .check,$(TESTS)) raw-tiles-test parallel-test pbf-test join-test enumerate-test decode-test join-filter-test unit json-tool-test allow-existing-test csv-test layer-json-test pmtiles-test decode-pmtiles-test overzoom-test mlt-test ./unit suffixes = json json.gz @@ -584,6 +600,31 @@ layer-json-test: tippecanoe tippecanoe-decode cmp tests/layer-json/out.mbtiles.json.check tests/layer-json/out.mbtiles.json rm -f tests/layer-json/out.mbtiles.json.check tests/layer-json/out.mbtiles +mlt-test: tippecanoe + # Points: MLT output, verify tile count matches MVT + ./tippecanoe -q --output-format=mlt -z5 -f -o tests/mlt/points.mbtiles tests/mlt/points.geojson + ./tippecanoe -q -z5 -f -o tests/mlt/points-mvt.mbtiles tests/mlt/points.geojson + @test $$(sqlite3 tests/mlt/points.mbtiles "SELECT COUNT(*) FROM tiles") -eq $$(sqlite3 tests/mlt/points-mvt.mbtiles "SELECT COUNT(*) FROM tiles") || (echo "FAIL: MLT and MVT tile counts differ" && exit 1) + @echo "PASS: MLT tile count matches MVT" + # Verify format metadata + @test "$$(sqlite3 tests/mlt/points.mbtiles "SELECT value FROM metadata WHERE name='format'")" = "mlt" || (echo "FAIL: format metadata is not 'mlt'" && exit 1) + @echo "PASS: MLT format metadata correct" + # Verify tiles are gzip compressed + @sqlite3 tests/mlt/points.mbtiles "SELECT hex(substr(tile_data, 1, 2)) FROM tiles LIMIT 1" | grep -q "1F8B" || (echo "FAIL: MLT tiles not gzip compressed" && exit 1) + @echo "PASS: MLT tiles are gzip compressed" + # Directory output with .mlt extension + rm -rf tests/mlt/dir-out + ./tippecanoe -q --output-format=mlt -z2 -f -e tests/mlt/dir-out tests/mlt/points.geojson + @test $$(find tests/mlt/dir-out -name '*.mlt' | wc -l) -gt 0 || (echo "FAIL: No .mlt files in directory output" && exit 1) + @test $$(find tests/mlt/dir-out -name '*.pbf' | wc -l) -eq 0 || (echo "FAIL: .pbf files in MLT directory output" && exit 1) + @echo "PASS: Directory output uses .mlt extension" + # Pretessellate flag + ./tippecanoe -q --output-format=mlt --pretessellate -z5 -f -o tests/mlt/points-tess.mbtiles tests/mlt/points.geojson + @test $$(sqlite3 tests/mlt/points-tess.mbtiles "SELECT COUNT(*) FROM tiles") -gt 0 || (echo "FAIL: No tiles with pretessellate" && exit 1) + @echo "PASS: --pretessellate produces tiles" + rm -f tests/mlt/points.mbtiles tests/mlt/points-mvt.mbtiles tests/mlt/points-tess.mbtiles + rm -rf tests/mlt/dir-out + # Use this target to regenerate the standards that the tests are compared against # after making a change that legitimately changes their output diff --git a/README.md b/README.md index ae6ceac9e..32ea8fce3 100644 --- a/README.md +++ b/README.md @@ -552,7 +552,10 @@ the same layer, enclose them in an `all` expression so they will all be evaluate * `--limit-tile-feature-count-at-maximum-zoom=`_features_: Abruptly limit each tile at the maximum zoom level to the specified number of _features_, after ordering them if specified. * `-pf` or `--no-feature-limit`: Don't limit tiles to 200,000 features * `-pk` or `--no-tile-size-limit`: Don't limit tiles to 500K bytes - * `-pC` or `--no-tile-compression`: Don't compress the PBF vector tile data. If you are getting "Unimplemented type 3" error messages from a renderer, it is probably because it expects uncompressed tiles using this option rather than the normal gzip-compressed tiles. + * `-pC` or `--no-tile-compression`: Don't compress the vector tile data. If you are getting "Unimplemented type 3" error messages from a renderer, it is probably because it expects uncompressed tiles using this option rather than the normal gzip-compressed tiles. + * `--output-format=`*format*: Set the tile encoding format. Supported values: `mvt` (default, Mapbox Vector Tiles) or `mlt` ([MapLibre Tiles](https://github.com/maplibre/maplibre-tile-spec)). MLT is a columnar format that typically produces smaller tiles than MVT. + * `--pretessellate`: When using `--output-format=mlt`, pre-triangulate polygon geometries for faster rendering. Only applies to layers where all features are polygons. + * `--no-mlt-feature-sort`: When using `--output-format=mlt`, disable within-tile spatial sorting of features by Hilbert curve index. Sorting is on by default and improves compression. * `-pg` or `--no-tile-stats`: Don't generate the `tilestats` row in the tileset metadata. Uploads without [tilestats](https://github.com/mapbox/mapbox-geostats) will take longer to process. * `--tile-stats-attributes-limit=`*count*: Include `tilestats` information about at most *count* attributes instead of the default 1000. * `--tile-stats-sample-values-limit=`*count*: Calculate `tilestats` attribute statistics based on *count* values instead of the default 1000. @@ -821,7 +824,7 @@ The options are: ### Setting or disabling tile size limits * `-pk` or `--no-tile-size-limit`: Don't skip tiles larger than 500K. - * `-pC` or `--no-tile-compression`: Don't compress the PBF vector tile data. + * `-pC` or `--no-tile-compression`: Don't compress the vector tile data. * `-pg` or `--no-tile-stats`: Don't generate the `tilestats` row in the tileset metadata. Uploads without [tilestats](https://github.com/mapbox/mapbox-geostats) will take longer to process. * `--tile-stats-attributes-limit=`*count*: Include `tilestats` information about at most *count* attributes instead of the default 1000. * `--tile-stats-sample-values-limit=`*count*: Calculate `tilestats` attribute statistics based on *count* values instead of the default 1000. diff --git a/dirtiles.cpp b/dirtiles.cpp index 98138bd5a..381849906 100644 --- a/dirtiles.cpp +++ b/dirtiles.cpp @@ -25,7 +25,7 @@ std::string dir_read_tile(std::string base, struct zxy tile) { return (contents.str()); } -void dir_write_tile(const char *outdir, int z, int tx, int ty, std::string const &pbf) { +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); @@ -39,7 +39,7 @@ void dir_write_tile(const char *outdir, int z, int tx, int ty, std::string const newdir = newdir + "/" + std::to_string(tx); mkdir(newdir.c_str(), S_IRWXU | S_IRWXG | S_IRWXO); - newdir = newdir + "/" + std::to_string(ty) + ".pbf"; + newdir = newdir + "/" + std::to_string(ty) + ext; struct stat st; if (stat(newdir.c_str(), &st) == 0) { @@ -81,7 +81,7 @@ static bool pbfname(const char *s) { s++; } - return strcmp(s, ".pbf") == 0 || strcmp(s, ".mvt") == 0; + return strcmp(s, ".pbf") == 0 || strcmp(s, ".mvt") == 0 || strcmp(s, ".mlt") == 0; } void check_dir(const char *dir, char **argv, bool force, bool forcetable) { diff --git a/dirtiles.hpp b/dirtiles.hpp index 226d44309..9dbe13a6a 100644 --- a/dirtiles.hpp +++ b/dirtiles.hpp @@ -6,7 +6,7 @@ #ifndef DIRTILES_HPP #define DIRTILES_HPP -void dir_write_tile(const char *outdir, int z, int tx, int ty, std::string const &pbf); +void dir_write_tile(const char *outdir, int z, int tx, int ty, std::string const &pbf, const char *ext = ".pbf"); void dir_erase_zoom(const char *outdir, int z); void dir_write_metadata(const char *outdir, const metadata &m); diff --git a/main.cpp b/main.cpp index 1f8b8f832..675045dd8 100644 --- a/main.cpp +++ b/main.cpp @@ -104,6 +104,9 @@ bool drop_by_attribute_descending = false; std::vector order_by; bool order_reverse; bool order_by_size = false; +int output_format = OUTPUT_MVT; +bool mlt_sort_features = true; +bool mlt_pretessellate = false; int prevent[256]; int additional[256]; @@ -2824,7 +2827,8 @@ std::pair read_input(std::vector &sources, char *fname, i ai->second.maxzoom = maxzoom; } - metadata m = make_metadata(fname, minzoom, maxzoom, minlat, minlon, maxlat, maxlon, minlat2, minlon2, maxlat2, maxlon2, midlat, midlon, attribution, merged_lm, true, description, !prevent[P_TILE_STATS], attribute_descriptions, "tippecanoe", commandline, strategies, basezoom, droprate, retain_points_multiplier); + const char *tile_format = (output_format == OUTPUT_MLT) ? "mlt" : "pbf"; + metadata m = make_metadata(fname, minzoom, maxzoom, minlat, minlon, maxlat, maxlon, minlat2, minlon2, maxlat2, maxlon2, midlat, midlon, attribution, merged_lm, tile_format, description, !prevent[P_TILE_STATS], attribute_descriptions, "tippecanoe", commandline, strategies, basezoom, droprate, retain_points_multiplier); if (outdb != NULL) { mbtiles_write_metadata(outdb, m, forcetable); } else { @@ -3160,6 +3164,9 @@ int main(int argc, char **argv) { {"no-feature-limit", no_argument, &prevent[P_FEATURE_LIMIT], 1}, {"no-tile-size-limit", no_argument, &prevent[P_KILOBYTE_LIMIT], 1}, {"no-tile-compression", no_argument, &prevent[P_TILE_COMPRESSION], 1}, + {"output-format", required_argument, 0, '~'}, + {"pretessellate", no_argument, 0, '~'}, + {"no-mlt-feature-sort", no_argument, 0, '~'}, {"no-tile-stats", no_argument, &prevent[P_TILE_STATS], 1}, {"tile-stats-attributes-limit", required_argument, 0, '~'}, {"tile-stats-sample-values-limit", required_argument, 0, '~'}, @@ -3327,6 +3334,19 @@ int main(int argc, char **argv) { unidecode_data = read_unidecode(optarg); } else if (strcmp(opt, "maximum-string-attribute-length") == 0) { maximum_string_attribute_length = atoll_require(optarg, "Maximum string attribute length"); + } else if (strcmp(opt, "output-format") == 0) { + if (strcmp(optarg, "mvt") == 0 || strcmp(optarg, "pbf") == 0) { + output_format = OUTPUT_MVT; + } else if (strcmp(optarg, "mlt") == 0) { + output_format = OUTPUT_MLT; + } else { + fprintf(stderr, "%s: --output-format must be 'mvt' or 'mlt'\n", argv[0]); + exit(EXIT_ARGS); + } + } else if (strcmp(opt, "pretessellate") == 0) { + mlt_pretessellate = true; + } else if (strcmp(opt, "no-mlt-feature-sort") == 0) { + mlt_sort_features = false; } else { fprintf(stderr, "%s: Unrecognized option --%s\n", argv[0], opt); exit(EXIT_ARGS); diff --git a/main.hpp b/main.hpp index 368016e9f..474c1eb97 100644 --- a/main.hpp +++ b/main.hpp @@ -76,4 +76,11 @@ bool progress_time(); #define MAX_ZOOM 24 +#define OUTPUT_MVT 0 +#define OUTPUT_MLT 1 + +extern int output_format; +extern bool mlt_sort_features; +extern bool mlt_pretessellate; + #endif diff --git a/maplibre-tile-spec b/maplibre-tile-spec new file mode 160000 index 000000000..d596473e2 --- /dev/null +++ b/maplibre-tile-spec @@ -0,0 +1 @@ +Subproject commit d596473e2861b873da913f53c3b70663bff2c4af diff --git a/mbtiles.cpp b/mbtiles.cpp index c7a615b0a..465727221 100644 --- a/mbtiles.cpp +++ b/mbtiles.cpp @@ -665,14 +665,14 @@ std::string version_str() { return s; } -metadata make_metadata(const char *fname, int minzoom, int maxzoom, double minlat, double minlon, double maxlat, double maxlon, double minlat2, double minlon2, double maxlat2, double maxlon2, double midlat, double midlon, const char *attribution, std::map const &layermap, bool vector, const char *description, bool do_tilestats, std::map const &attribute_descriptions, std::string const &program, std::string const &commandline, std::vector const &strategies, int basezoom, double droprate, int retain_points_multiplier) { +metadata make_metadata(const char *fname, int minzoom, int maxzoom, double minlat, double minlon, double maxlat, double maxlon, double minlat2, double minlon2, double maxlat2, double maxlon2, double midlat, double midlon, const char *attribution, std::map const &layermap, const char *tile_format, const char *description, bool do_tilestats, std::map const &attribute_descriptions, std::string const &program, std::string const &commandline, std::vector const &strategies, int basezoom, double droprate, int retain_points_multiplier) { metadata m; m.name = fname; m.description = description != NULL ? description : fname; m.version = 2; m.type = "overlay"; - m.format = vector ? "pbf" : "png"; + m.format = tile_format; m.minzoom = minzoom; m.maxzoom = maxzoom; @@ -711,7 +711,8 @@ metadata make_metadata(const char *fname, int minzoom, int maxzoom, double minla std::string("}"); } - if (vector) { + bool is_vector_format = (strcmp(tile_format, "pbf") == 0 || strcmp(tile_format, "mlt") == 0); + if (is_vector_format) { { json_writer state(&m.vector_layers_json); diff --git a/mbtiles.hpp b/mbtiles.hpp index 661f5a654..12ab51b4c 100644 --- a/mbtiles.hpp +++ b/mbtiles.hpp @@ -68,7 +68,7 @@ sqlite3 *mbtiles_open(char *dbname, char **argv, int forcetable); void mbtiles_write_tile(sqlite3 *outdb, int z, int tx, int ty, const char *data, int size); void mbtiles_erase_zoom(sqlite3 *outdb, int z); -metadata make_metadata(const char *fname, int minzoom, int maxzoom, double minlat, double minlon, double maxlat, double maxlon, double minlat2, double minlon2, double maxlat2, double maxlon2, double midlat, double midlon, const char *attribution, std::map const &layermap, bool vector, const char *description, bool do_tilestats, std::map const &attribute_descriptions, std::string const &program, std::string const &commandline, std::vector const &strategies, int basezoom, double droprate, int retain_points_multiplier); +metadata make_metadata(const char *fname, int minzoom, int maxzoom, double minlat, double minlon, double maxlat, double maxlon, double minlat2, double minlon2, double maxlat2, double maxlon2, double midlat, double midlon, const char *attribution, std::map const &layermap, const char *tile_format, const char *description, bool do_tilestats, std::map const &attribute_descriptions, std::string const &program, std::string const &commandline, std::vector const &strategies, int basezoom, double droprate, int retain_points_multiplier); void mbtiles_write_metadata(sqlite3 *db, const metadata &m, bool forcetable); void mbtiles_close(sqlite3 *outdb, const char *pgm); diff --git a/mlt.cpp b/mlt.cpp new file mode 100644 index 000000000..09120e881 --- /dev/null +++ b/mlt.cpp @@ -0,0 +1,191 @@ +#include "mlt.hpp" + +#include + +#include +#include +#include +#include + +using Vertex = mlt::Encoder::Vertex; + +static mlt::Encoder::PropertyValue convert_value(const mvt_value &val) { + switch (val.type) { + case mvt_bool: + return val.numeric_value.bool_value; + case mvt_int: + if (val.numeric_value.int_value >= INT32_MIN && val.numeric_value.int_value <= INT32_MAX) { + return static_cast(val.numeric_value.int_value); + } + return static_cast(val.numeric_value.int_value); + case mvt_uint: + if (val.numeric_value.uint_value <= UINT32_MAX) { + return static_cast(val.numeric_value.uint_value); + } + return static_cast(val.numeric_value.uint_value); + case mvt_sint: + if (val.numeric_value.sint_value >= INT32_MIN && val.numeric_value.sint_value <= INT32_MAX) { + return static_cast(val.numeric_value.sint_value); + } + return static_cast(val.numeric_value.sint_value); + case mvt_float: + return val.numeric_value.float_value; + case mvt_double: + return val.numeric_value.double_value; + case mvt_string: + return val.get_string_value(); + default: + return std::string{}; + } +} + +// Split MVT command stream into coordinate rings (sequences between moveto commands). +// Each ring is a vector of vertices. For polygons, closepath is implicit (MLT strips closing points). +static std::vector> extract_rings(const mvt_feature &feature) { + std::vector> rings; + + for (size_t i = 0; i < feature.geometry.size(); i++) { + const auto &g = feature.geometry[i]; + if (g.op == mvt_moveto) { + rings.emplace_back(); + rings.back().push_back({static_cast(g.x), static_cast(g.y)}); + } else if (g.op == mvt_lineto) { + rings.back().push_back({static_cast(g.x), static_cast(g.y)}); + } + // mvt_closepath: polygon ring close — MLT stores without closing point + } + return rings; +} + +static mlt::Encoder::Geometry convert_geometry(const mvt_feature &feature) { + mlt::Encoder::Geometry geom; + + auto rings = extract_rings(feature); + + switch (feature.type) { + case mvt_point: + if (rings.size() == 1 && rings[0].size() == 1) { + geom.type = mlt::Encoder::GeometryType::POINT; + geom.coordinates = std::move(rings[0]); + } else { + geom.type = mlt::Encoder::GeometryType::MULTIPOINT; + for (auto &ring : rings) { + for (auto &v : ring) { + geom.coordinates.push_back(v); + } + } + } + break; + + case mvt_linestring: + if (rings.size() == 1) { + geom.type = mlt::Encoder::GeometryType::LINESTRING; + geom.coordinates = std::move(rings[0]); + } else { + geom.type = mlt::Encoder::GeometryType::MULTILINESTRING; + geom.parts = std::move(rings); + } + break; + + case mvt_polygon: { + // Outer rings are clockwise (positive area), holes are counter-clockwise. + // Group into polygons: each outer ring starts a new polygon. + std::vector>> polygons; + + for (auto &ring : rings) { + // Signed area to detect winding: positive = clockwise = outer ring (in MVT screen coords) + long long area2 = 0; + for (size_t i = 0; i < ring.size(); i++) { + size_t j = (i + 1) % ring.size(); + area2 += (long long) ring[i].x * ring[j].y - (long long) ring[j].x * ring[i].y; + } + + if (area2 >= 0) { + // Outer ring — start new polygon + polygons.emplace_back(); + } + if (!polygons.empty()) { + polygons.back().push_back(std::move(ring)); + } + } + + if (polygons.size() == 1) { + geom.type = mlt::Encoder::GeometryType::POLYGON; + for (auto &ring : polygons[0]) { + geom.ringSizes.push_back(static_cast(ring.size())); + geom.coordinates.insert(geom.coordinates.end(), ring.begin(), ring.end()); + } + } else { + geom.type = mlt::Encoder::GeometryType::MULTIPOLYGON; + for (auto &poly : polygons) { + std::vector part_verts; + std::vector part_rings; + for (auto &ring : poly) { + part_rings.push_back(static_cast(ring.size())); + part_verts.insert(part_verts.end(), ring.begin(), ring.end()); + } + geom.parts.push_back(std::move(part_verts)); + geom.partRingSizes.push_back(std::move(part_rings)); + } + } + break; + } + } + + return geom; +} + +static mlt::Encoder::Layer convert_layer(const mvt_layer &layer) { + mlt::Encoder::Layer out; + out.name = layer.name; + out.extent = static_cast(layer.extent); + + for (const auto &feature : layer.features) { + mlt::Encoder::Feature f; + f.id = feature.id; + f.geometry = convert_geometry(feature); + + for (size_t t = 0; t + 1 < feature.tags.size(); t += 2) { + unsigned key_idx = feature.tags[t]; + unsigned val_idx = feature.tags[t + 1]; + if (key_idx < layer.keys.size() && val_idx < layer.values.size()) { + const auto &val = layer.values[val_idx]; + if (val.type != mvt_null) { + f.properties[layer.keys[key_idx]] = convert_value(val); + } + } + } + + out.features.push_back(std::move(f)); + } + + return out; +} + +std::string encode_as_mlt(const mvt_tile &tile, bool sort_features, bool pretessellate) { + mlt::Encoder encoder; + mlt::EncoderConfig config; + config.sortFeatures = sort_features; + config.preTessellate = pretessellate; + + bool any_has_id = false; + for (const auto &layer : tile.layers) { + for (const auto &feature : layer.features) { + if (feature.has_id) { + any_has_id = true; + break; + } + } + if (any_has_id) break; + } + config.includeIds = any_has_id; + + std::vector layers; + layers.reserve(tile.layers.size()); + for (const auto &layer : tile.layers) { + layers.push_back(convert_layer(layer)); + } + + auto bytes = encoder.encode(layers, config); + return std::string(reinterpret_cast(bytes.data()), bytes.size()); +} diff --git a/mlt.hpp b/mlt.hpp new file mode 100644 index 000000000..d5e25639a --- /dev/null +++ b/mlt.hpp @@ -0,0 +1,9 @@ +#ifndef MLT_HPP +#define MLT_HPP + +#include +#include "mvt.hpp" + +std::string encode_as_mlt(const mvt_tile &tile, bool sort_features, bool pretessellate); + +#endif diff --git a/tests/mlt/points.geojson b/tests/mlt/points.geojson new file mode 100644 index 000000000..5f71dbd36 --- /dev/null +++ b/tests/mlt/points.geojson @@ -0,0 +1,5 @@ +{"type":"FeatureCollection","features":[ +{"type":"Feature","id":1,"properties":{"name":"point1","count":100,"active":true},"geometry":{"type":"Point","coordinates":[-122.4,37.8]}}, +{"type":"Feature","id":2,"properties":{"name":"point2","count":200,"active":false},"geometry":{"type":"Point","coordinates":[-122.5,37.9]}}, +{"type":"Feature","id":3,"properties":{"name":"point3","count":300},"geometry":{"type":"Point","coordinates":[-122.6,38.0]}} +]} diff --git a/tile.cpp b/tile.cpp index a4f12c758..f2febbab5 100644 --- a/tile.cpp +++ b/tile.cpp @@ -39,6 +39,7 @@ #include "serial.hpp" #include "options.hpp" #include "main.hpp" +#include "mlt.hpp" #include "write_json.hpp" #include "milo/dtoa_milo.h" #include "evaluator.hpp" @@ -2847,7 +2848,12 @@ long long write_tile(decompressor *geoms, std::atomic *geompos_in, ch } std::string compressed; - std::string pbf = tile.encode(); + std::string pbf; + if (output_format == OUTPUT_MLT) { + pbf = encode_as_mlt(tile, mlt_sort_features, mlt_pretessellate); + } else { + pbf = tile.encode(); + } tile.layers.clear(); @@ -3022,7 +3028,8 @@ long long write_tile(decompressor *geoms, std::atomic *geompos_in, ch if (outdb != NULL) { mbtiles_write_tile(outdb, z, tx, ty, compressed.data(), compressed.size()); } else if (outdir != NULL) { - dir_write_tile(outdir, z, tx, ty, compressed); + const char *tile_ext = (output_format == OUTPUT_MLT) ? ".mlt" : ".pbf"; + dir_write_tile(outdir, z, tx, ty, compressed, tile_ext); } if (pthread_mutex_unlock(&db_lock) != 0) { From 116e40cbdf591a42e6c5b68fa3c69822ac189580 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 5 Feb 2026 09:17:26 +0300 Subject: [PATCH 2/4] Support nested JSON properties as MLT STRUCT columns Detect JSON object strings at MLT output time and convert to STRUCT columns. Nested objects/arrays within are re-stringified. MVT output unchanged. --- mlt.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/mlt.cpp b/mlt.cpp index 09120e881..adaa2da43 100644 --- a/mlt.cpp +++ b/mlt.cpp @@ -1,4 +1,5 @@ #include "mlt.hpp" +#include "jsonpull/jsonpull.h" #include @@ -9,6 +10,68 @@ using Vertex = mlt::Encoder::Vertex; +// Try to parse a JSON object string into MLT STRUCT (flat string children only) +static bool try_parse_json_object(const std::string &s, mlt::Encoder::StructValue &out) { + if (s.empty() || s[0] != '{') { + return false; + } + + json_pull *jp = json_begin_string(s.c_str()); + json_object *obj = json_read_tree(jp); + + if (obj == nullptr || obj->type != JSON_HASH) { + json_free(obj); + json_end(jp); + return false; + } + + for (size_t i = 0; i < obj->value.object.length; i++) { + json_object *key = obj->value.object.keys[i]; + json_object *val = obj->value.object.values[i]; + + if (key->type != JSON_STRING) continue; + + std::string child_key = key->value.string.string; + std::string child_val; + + switch (val->type) { + case JSON_STRING: + child_val = val->value.string.string; + break; + case JSON_NUMBER: + if (val->value.number.large_unsigned != 0) { + child_val = std::to_string(val->value.number.large_unsigned); + } else if (val->value.number.large_signed != 0) { + child_val = std::to_string(val->value.number.large_signed); + } else { + child_val = std::to_string(val->value.number.number); + } + break; + case JSON_TRUE: + child_val = "true"; + break; + case JSON_FALSE: + child_val = "false"; + break; + case JSON_NULL: + child_val = "null"; + break; + default: + // Nested object/array - stringify back + char *nested = json_stringify(val); + child_val = nested; + free(nested); + break; + } + + out[child_key] = child_val; + } + + json_free(obj); + json_end(jp); + return true; +} + static mlt::Encoder::PropertyValue convert_value(const mvt_value &val) { switch (val.type) { case mvt_bool: @@ -32,8 +95,14 @@ static mlt::Encoder::PropertyValue convert_value(const mvt_value &val) { return val.numeric_value.float_value; case mvt_double: return val.numeric_value.double_value; - case mvt_string: - return val.get_string_value(); + case mvt_string: { + std::string s = val.get_string_value(); + mlt::Encoder::StructValue struct_val; + if (try_parse_json_object(s, struct_val)) { + return struct_val; + } + return s; + } default: return std::string{}; } From 0e536bcec4f5aa1b3101ec0da6af9763633af708 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 2 Jul 2026 17:34:40 +0300 Subject: [PATCH 3/4] Update MLT encoder dependency --- .github/workflows/test.yml | 2 ++ .gitignore | 1 - .gitmodules | 4 ++-- Makefile | 18 +++++++++--------- README.md | 2 +- maplibre-tile-spec | 2 +- mlt.cpp | 15 ++++++++++++--- tile-join.cpp | 2 +- 8 files changed, 28 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb8ae19e3..87c0187ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,8 @@ jobs: version: ['Release', 'Debug'] steps: - uses: actions/checkout@v3 + with: + submodules: recursive - name: Install dependencies (Ubuntu) if: runner.os == 'Linux' run: sudo apt-get install libsqlite3-dev diff --git a/.gitignore b/.gitignore index dff9b97a0..8f53e6761 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,3 @@ mlt-build/ # Nodejs node_modules -tmp/ diff --git a/.gitmodules b/.gitmodules index 7c4755cdc..cb7e0f582 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "maplibre-tile-spec"] path = maplibre-tile-spec - url = https://github.com/dannote/maplibre-tile-spec.git - branch = feature/cpp-encoder + url = https://github.com/maplibre/maplibre-tile-spec.git + branch = main diff --git a/Makefile b/Makefile index d44357761..f56d6b424 100644 --- a/Makefile +++ b/Makefile @@ -58,19 +58,24 @@ C = $(wildcard *.c) $(wildcard *.cpp) INCLUDES = -I/usr/local/include -I. -Iclipper2/include MLT_INCLUDES = -Imaplibre-tile-spec/cpp/include -isystem maplibre-tile-spec/cpp/vendor/fsst -MLT_LIBS = mlt-build/libmlt-cpp-encoder.a mlt-build/libfsst-lib.a +MLT_LIBS = mlt-build/libmlt-cpp-encoder.a mlt-build/libfsst-lib.a mlt-build/fastpfor/libFastPFOR.a +MLT_BUILD_STAMP = mlt-build/.built LIBS = -L/usr/local/lib -mlt-build/libmlt-cpp-encoder.a: maplibre-tile-spec/cpp/CMakeLists.txt +$(MLT_BUILD_STAMP): maplibre-tile-spec/cpp/CMakeLists.txt cmake -S maplibre-tile-spec/cpp -B mlt-build \ - -DCMAKE_BUILD_TYPE=Release \ - -DMLT_WITH_FASTPFOR=OFF \ + -DCMAKE_BUILD_TYPE=$(BUILDTYPE) \ + -DMLT_WITH_FASTPFOR=ON \ + -DMLT_WITH_FASTPFOR_SIMD=OFF \ -DMLT_WITH_JSON=OFF \ -DMLT_WITH_TESTS=OFF \ -DMLT_WITH_TOOLS=OFF \ -DCMAKE_CXX_STANDARD=20 \ $(if $(VERBOSE),,--log-level=WARNING) > /dev/null cmake --build mlt-build --target mlt-cpp-encoder $(if $(VERBOSE),,-- -s) > /dev/null + touch $@ + +$(MLT_LIBS): $(MLT_BUILD_STAMP) tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o mlt.o serial.o main.o platform.o text.o dirtiles.o pmtiles_file.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o visvalingam.o compression.o clip.o sort.o attribute.o thread.o shared_borders.o clipper2/src/clipper.engine.o $(MLT_LIBS) $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread @@ -605,23 +610,18 @@ mlt-test: tippecanoe ./tippecanoe -q --output-format=mlt -z5 -f -o tests/mlt/points.mbtiles tests/mlt/points.geojson ./tippecanoe -q -z5 -f -o tests/mlt/points-mvt.mbtiles tests/mlt/points.geojson @test $$(sqlite3 tests/mlt/points.mbtiles "SELECT COUNT(*) FROM tiles") -eq $$(sqlite3 tests/mlt/points-mvt.mbtiles "SELECT COUNT(*) FROM tiles") || (echo "FAIL: MLT and MVT tile counts differ" && exit 1) - @echo "PASS: MLT tile count matches MVT" # Verify format metadata @test "$$(sqlite3 tests/mlt/points.mbtiles "SELECT value FROM metadata WHERE name='format'")" = "mlt" || (echo "FAIL: format metadata is not 'mlt'" && exit 1) - @echo "PASS: MLT format metadata correct" # Verify tiles are gzip compressed @sqlite3 tests/mlt/points.mbtiles "SELECT hex(substr(tile_data, 1, 2)) FROM tiles LIMIT 1" | grep -q "1F8B" || (echo "FAIL: MLT tiles not gzip compressed" && exit 1) - @echo "PASS: MLT tiles are gzip compressed" # Directory output with .mlt extension rm -rf tests/mlt/dir-out ./tippecanoe -q --output-format=mlt -z2 -f -e tests/mlt/dir-out tests/mlt/points.geojson @test $$(find tests/mlt/dir-out -name '*.mlt' | wc -l) -gt 0 || (echo "FAIL: No .mlt files in directory output" && exit 1) @test $$(find tests/mlt/dir-out -name '*.pbf' | wc -l) -eq 0 || (echo "FAIL: .pbf files in MLT directory output" && exit 1) - @echo "PASS: Directory output uses .mlt extension" # Pretessellate flag ./tippecanoe -q --output-format=mlt --pretessellate -z5 -f -o tests/mlt/points-tess.mbtiles tests/mlt/points.geojson @test $$(sqlite3 tests/mlt/points-tess.mbtiles "SELECT COUNT(*) FROM tiles") -gt 0 || (echo "FAIL: No tiles with pretessellate" && exit 1) - @echo "PASS: --pretessellate produces tiles" rm -f tests/mlt/points.mbtiles tests/mlt/points-mvt.mbtiles tests/mlt/points-tess.mbtiles rm -rf tests/mlt/dir-out diff --git a/README.md b/README.md index 32ea8fce3..24e229b4c 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ $ brew install tippecanoe On Ubuntu it will usually be easiest to build from the source repository: ```sh -$ git clone https://github.com/felt/tippecanoe.git +$ git clone --recurse-submodules https://github.com/felt/tippecanoe.git $ cd tippecanoe $ make -j $ make install diff --git a/maplibre-tile-spec b/maplibre-tile-spec index d596473e2..ec6808dab 160000 --- a/maplibre-tile-spec +++ b/maplibre-tile-spec @@ -1 +1 @@ -Subproject commit d596473e2861b873da913f53c3b70663bff2c4af +Subproject commit ec6808dab4ce9bd5ecb0394a6bd5f50c02d69944 diff --git a/mlt.cpp b/mlt.cpp index adaa2da43..69bf9b912 100644 --- a/mlt.cpp +++ b/mlt.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -29,7 +30,9 @@ static bool try_parse_json_object(const std::string &s, mlt::Encoder::StructValu json_object *key = obj->value.object.keys[i]; json_object *val = obj->value.object.values[i]; - if (key->type != JSON_STRING) continue; + if (key->type != JSON_STRING) { + continue; + } std::string child_key = key->value.string.string; std::string child_val; @@ -211,7 +214,11 @@ static mlt::Encoder::Layer convert_layer(const mvt_layer &layer) { for (const auto &feature : layer.features) { mlt::Encoder::Feature f; - f.id = feature.id; + if (feature.has_id) { + f.id = feature.id; + } else { + f.id = std::nullopt; + } f.geometry = convert_geometry(feature); for (size_t t = 0; t + 1 < feature.tags.size(); t += 2) { @@ -245,7 +252,9 @@ std::string encode_as_mlt(const mvt_tile &tile, bool sort_features, bool pretess break; } } - if (any_has_id) break; + if (any_has_id) { + break; + } } config.includeIds = any_has_id; diff --git a/tile-join.cpp b/tile-join.cpp index 1b057577e..347f7a104 100644 --- a/tile-join.cpp +++ b/tile-join.cpp @@ -1636,7 +1636,7 @@ int main(int argc, char **argv) { st.maxlon2 = st.maxlon; } - metadata m = make_metadata(name.c_str(), st.minzoom, st.maxzoom, st.minlat, st.minlon, st.maxlat, st.maxlon, st.minlat2, st.minlon2, st.maxlat2, st.maxlon2, st.midlat, st.midlon, attribution.size() != 0 ? attribution.c_str() : NULL, layermap, true, description.c_str(), !pg, attribute_descriptions, "tile-join", generator_options, strategies, st.maxzoom, 2.5, 1); + metadata m = make_metadata(name.c_str(), st.minzoom, st.maxzoom, st.minlat, st.minlon, st.maxlat, st.maxlon, st.minlat2, st.minlon2, st.maxlat2, st.maxlon2, st.midlat, st.midlon, attribution.size() != 0 ? attribution.c_str() : NULL, layermap, "pbf", description.c_str(), !pg, attribute_descriptions, "tile-join", generator_options, strategies, st.maxzoom, 2.5, 1); if (outdb != NULL) { mbtiles_write_metadata(outdb, m, true); From 776a7b3b84bd8c493082f16dbe4c86ede7c9d400 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 4 Jul 2026 19:56:35 +0300 Subject: [PATCH 4/4] Tighten MLT wording --- .gitignore | 1 - .gitmodules | 1 - Makefile | 5 ----- README.md | 6 +++--- mlt.cpp | 13 ++----------- 5 files changed, 5 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 8f53e6761..fc77c5500 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ tests/**/*.mbtiles tests/**/*.check tests/**/*.geobuf -# MLT build directory mlt-build/ # Vim diff --git a/.gitmodules b/.gitmodules index cb7e0f582..5dd2ba490 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,3 @@ [submodule "maplibre-tile-spec"] path = maplibre-tile-spec url = https://github.com/maplibre/maplibre-tile-spec.git - branch = main diff --git a/Makefile b/Makefile index f56d6b424..df4d7d9bc 100644 --- a/Makefile +++ b/Makefile @@ -606,20 +606,15 @@ layer-json-test: tippecanoe tippecanoe-decode rm -f tests/layer-json/out.mbtiles.json.check tests/layer-json/out.mbtiles mlt-test: tippecanoe - # Points: MLT output, verify tile count matches MVT ./tippecanoe -q --output-format=mlt -z5 -f -o tests/mlt/points.mbtiles tests/mlt/points.geojson ./tippecanoe -q -z5 -f -o tests/mlt/points-mvt.mbtiles tests/mlt/points.geojson @test $$(sqlite3 tests/mlt/points.mbtiles "SELECT COUNT(*) FROM tiles") -eq $$(sqlite3 tests/mlt/points-mvt.mbtiles "SELECT COUNT(*) FROM tiles") || (echo "FAIL: MLT and MVT tile counts differ" && exit 1) - # Verify format metadata @test "$$(sqlite3 tests/mlt/points.mbtiles "SELECT value FROM metadata WHERE name='format'")" = "mlt" || (echo "FAIL: format metadata is not 'mlt'" && exit 1) - # Verify tiles are gzip compressed @sqlite3 tests/mlt/points.mbtiles "SELECT hex(substr(tile_data, 1, 2)) FROM tiles LIMIT 1" | grep -q "1F8B" || (echo "FAIL: MLT tiles not gzip compressed" && exit 1) - # Directory output with .mlt extension rm -rf tests/mlt/dir-out ./tippecanoe -q --output-format=mlt -z2 -f -e tests/mlt/dir-out tests/mlt/points.geojson @test $$(find tests/mlt/dir-out -name '*.mlt' | wc -l) -gt 0 || (echo "FAIL: No .mlt files in directory output" && exit 1) @test $$(find tests/mlt/dir-out -name '*.pbf' | wc -l) -eq 0 || (echo "FAIL: .pbf files in MLT directory output" && exit 1) - # Pretessellate flag ./tippecanoe -q --output-format=mlt --pretessellate -z5 -f -o tests/mlt/points-tess.mbtiles tests/mlt/points.geojson @test $$(sqlite3 tests/mlt/points-tess.mbtiles "SELECT COUNT(*) FROM tiles") -gt 0 || (echo "FAIL: No tiles with pretessellate" && exit 1) rm -f tests/mlt/points.mbtiles tests/mlt/points-mvt.mbtiles tests/mlt/points-tess.mbtiles diff --git a/README.md b/README.md index 24e229b4c..a3a0e322c 100644 --- a/README.md +++ b/README.md @@ -553,9 +553,9 @@ the same layer, enclose them in an `all` expression so they will all be evaluate * `-pf` or `--no-feature-limit`: Don't limit tiles to 200,000 features * `-pk` or `--no-tile-size-limit`: Don't limit tiles to 500K bytes * `-pC` or `--no-tile-compression`: Don't compress the vector tile data. If you are getting "Unimplemented type 3" error messages from a renderer, it is probably because it expects uncompressed tiles using this option rather than the normal gzip-compressed tiles. - * `--output-format=`*format*: Set the tile encoding format. Supported values: `mvt` (default, Mapbox Vector Tiles) or `mlt` ([MapLibre Tiles](https://github.com/maplibre/maplibre-tile-spec)). MLT is a columnar format that typically produces smaller tiles than MVT. - * `--pretessellate`: When using `--output-format=mlt`, pre-triangulate polygon geometries for faster rendering. Only applies to layers where all features are polygons. - * `--no-mlt-feature-sort`: When using `--output-format=mlt`, disable within-tile spatial sorting of features by Hilbert curve index. Sorting is on by default and improves compression. + * `--output-format=`*format*: Set the tile encoding format. Supported values: `mvt` (default, Mapbox Vector Tiles) or `mlt` ([MapLibre Tiles](https://github.com/maplibre/maplibre-tile-spec)). + * `--pretessellate`: When using `--output-format=mlt`, pre-triangulate polygon geometries. Only applies to layers where all features are polygons. + * `--no-mlt-feature-sort`: When using `--output-format=mlt`, disable within-tile spatial sorting of features by Hilbert curve index. Sorting is on by default. * `-pg` or `--no-tile-stats`: Don't generate the `tilestats` row in the tileset metadata. Uploads without [tilestats](https://github.com/mapbox/mapbox-geostats) will take longer to process. * `--tile-stats-attributes-limit=`*count*: Include `tilestats` information about at most *count* attributes instead of the default 1000. * `--tile-stats-sample-values-limit=`*count*: Calculate `tilestats` attribute statistics based on *count* values instead of the default 1000. diff --git a/mlt.cpp b/mlt.cpp index 69bf9b912..bfae0cb48 100644 --- a/mlt.cpp +++ b/mlt.cpp @@ -11,8 +11,7 @@ using Vertex = mlt::Encoder::Vertex; -// Try to parse a JSON object string into MLT STRUCT (flat string children only) -static bool try_parse_json_object(const std::string &s, mlt::Encoder::StructValue &out) { +static bool parse_json_object_property(const std::string &s, mlt::Encoder::StructValue &out) { if (s.empty() || s[0] != '{') { return false; } @@ -60,7 +59,6 @@ static bool try_parse_json_object(const std::string &s, mlt::Encoder::StructValu child_val = "null"; break; default: - // Nested object/array - stringify back char *nested = json_stringify(val); child_val = nested; free(nested); @@ -101,7 +99,7 @@ static mlt::Encoder::PropertyValue convert_value(const mvt_value &val) { case mvt_string: { std::string s = val.get_string_value(); mlt::Encoder::StructValue struct_val; - if (try_parse_json_object(s, struct_val)) { + if (parse_json_object_property(s, struct_val)) { return struct_val; } return s; @@ -111,8 +109,6 @@ static mlt::Encoder::PropertyValue convert_value(const mvt_value &val) { } } -// Split MVT command stream into coordinate rings (sequences between moveto commands). -// Each ring is a vector of vertices. For polygons, closepath is implicit (MLT strips closing points). static std::vector> extract_rings(const mvt_feature &feature) { std::vector> rings; @@ -124,7 +120,6 @@ static std::vector> extract_rings(const mvt_feature &feature } else if (g.op == mvt_lineto) { rings.back().push_back({static_cast(g.x), static_cast(g.y)}); } - // mvt_closepath: polygon ring close — MLT stores without closing point } return rings; } @@ -160,12 +155,9 @@ static mlt::Encoder::Geometry convert_geometry(const mvt_feature &feature) { break; case mvt_polygon: { - // Outer rings are clockwise (positive area), holes are counter-clockwise. - // Group into polygons: each outer ring starts a new polygon. std::vector>> polygons; for (auto &ring : rings) { - // Signed area to detect winding: positive = clockwise = outer ring (in MVT screen coords) long long area2 = 0; for (size_t i = 0; i < ring.size(); i++) { size_t j = (i + 1) % ring.size(); @@ -173,7 +165,6 @@ static mlt::Encoder::Geometry convert_geometry(const mvt_feature &feature) { } if (area2 >= 0) { - // Outer ring — start new polygon polygons.emplace_back(); } if (!polygons.empty()) {