diff --git a/.gitignore b/.gitignore index 9f2b22af..dff9b97a 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 00000000..7c4755cd --- /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 0b988399..d4435776 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 ae6ceac9..32ea8fce 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 98138bd5..38184990 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 226d4430..9dbe13a6 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 1f8b8f83..675045dd 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 368016e9..474c1eb9 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 00000000..d596473e --- /dev/null +++ b/maplibre-tile-spec @@ -0,0 +1 @@ +Subproject commit d596473e2861b873da913f53c3b70663bff2c4af diff --git a/mbtiles.cpp b/mbtiles.cpp index c7a615b0..46572722 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 661f5a65..12ab51b4 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 00000000..09120e88 --- /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 00000000..d5e25639 --- /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 00000000..5f71dbd3 --- /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 a4f12c75..f2febbab 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) {