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 9f2b22afb..fc77c5500 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,8 @@ tests/**/*.mbtiles tests/**/*.check tests/**/*.geobuf +mlt-build/ + # Vim *.swp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..5dd2ba490 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "maplibre-tile-spec"] + path = maplibre-tile-spec + url = https://github.com/maplibre/maplibre-tile-spec.git diff --git a/Makefile b/Makefile index 0b9883993..df4d7d9bc 100644 --- a/Makefile +++ b/Makefile @@ -57,9 +57,27 @@ 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 mlt-build/fastpfor/libFastPFOR.a +MLT_BUILD_STAMP = mlt-build/.built 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_STAMP): maplibre-tile-spec/cpp/CMakeLists.txt + cmake -S maplibre-tile-spec/cpp -B mlt-build \ + -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 tippecanoe-enumerate: enumerate.o @@ -85,11 +103,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 +118,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 +605,21 @@ 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 + ./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) + @test "$$(sqlite3 tests/mlt/points.mbtiles "SELECT value FROM metadata WHERE name='format'")" = "mlt" || (echo "FAIL: format metadata is not 'mlt'" && exit 1) + @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) + 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) + ./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 + 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..a3a0e322c 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 @@ -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)). + * `--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. @@ -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..ec6808dab --- /dev/null +++ b/maplibre-tile-spec @@ -0,0 +1 @@ +Subproject commit ec6808dab4ce9bd5ecb0394a6bd5f50c02d69944 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..bfae0cb48 --- /dev/null +++ b/mlt.cpp @@ -0,0 +1,260 @@ +#include "mlt.hpp" +#include "jsonpull/jsonpull.h" + +#include + +#include +#include +#include +#include +#include + +using Vertex = mlt::Encoder::Vertex; + +static bool parse_json_object_property(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: + 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: + 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: { + std::string s = val.get_string_value(); + mlt::Encoder::StructValue struct_val; + if (parse_json_object_property(s, struct_val)) { + return struct_val; + } + return s; + } + default: + return std::string{}; + } +} + +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)}); + } + } + 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: { + std::vector>> polygons; + + for (auto &ring : rings) { + 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) { + 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; + 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) { + 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-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); 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) {