From ae1981b0f0a3f2197b86451ebfac6f99d54901eb Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Thu, 28 Dec 2023 16:31:01 -0500 Subject: [PATCH] use vtzero instead of libprotobuf (#625) --- CMakeLists.txt | 17 +- Dockerfile | 5 +- Makefile | 9 +- docs/INSTALL.md | 6 +- include/output_object.h | 26 +- include/pbf_processor.h | 5 +- include/vector_tile.proto | 102 -- include/vtzero/builder.hpp | 1365 +++++++++++++++++++++ include/vtzero/builder_impl.hpp | 267 ++++ include/vtzero/encoded_property_value.hpp | 244 ++++ include/vtzero/exception.hpp | 134 ++ include/vtzero/feature.hpp | 315 +++++ include/vtzero/feature_builder_impl.hpp | 126 ++ include/vtzero/geometry.hpp | 444 +++++++ include/vtzero/index.hpp | 264 ++++ include/vtzero/layer.hpp | 512 ++++++++ include/vtzero/output.hpp | 64 + include/vtzero/property.hpp | 89 ++ include/vtzero/property_mapper.hpp | 103 ++ include/vtzero/property_value.hpp | 398 ++++++ include/vtzero/types.hpp | 433 +++++++ include/vtzero/vector_tile.hpp | 290 +++++ include/vtzero/version.hpp | 36 + include/write_geometry.h | 46 - src/output_object.cpp | 78 +- src/read_shp.cpp | 1 - src/tile_worker.cpp | 285 +++-- src/tilemaker.cpp | 2 - src/write_geometry.cpp | 159 --- 29 files changed, 5325 insertions(+), 500 deletions(-) delete mode 100644 include/vector_tile.proto create mode 100644 include/vtzero/builder.hpp create mode 100644 include/vtzero/builder_impl.hpp create mode 100644 include/vtzero/encoded_property_value.hpp create mode 100644 include/vtzero/exception.hpp create mode 100644 include/vtzero/feature.hpp create mode 100644 include/vtzero/feature_builder_impl.hpp create mode 100644 include/vtzero/geometry.hpp create mode 100644 include/vtzero/index.hpp create mode 100644 include/vtzero/layer.hpp create mode 100644 include/vtzero/output.hpp create mode 100644 include/vtzero/property.hpp create mode 100644 include/vtzero/property_mapper.hpp create mode 100644 include/vtzero/property_value.hpp create mode 100644 include/vtzero/types.hpp create mode 100644 include/vtzero/vector_tile.hpp create mode 100644 include/vtzero/version.hpp delete mode 100644 include/write_geometry.h delete mode 100644 src/write_geometry.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c2e36570..04193c4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,10 +33,6 @@ ENDIF () find_package(Boost 1.66 REQUIRED COMPONENTS system filesystem program_options iostreams) -set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) -find_package(Protobuf REQUIRED) -set(CMAKE_FIND_PACKAGE_PREFER_CONFIG OFF) - find_package(libshp REQUIRED) find_package(Rapidjson REQUIRED) @@ -72,15 +68,6 @@ else() set(THREAD_LIB pthread) endif() -if(NOT PROTOBUF_PROTOC_EXECUTABLE) - set (PROTOBUF_PROTOC_EXECUTABLE "protobuf::protoc") -endif() - - -ADD_CUSTOM_COMMAND(OUTPUT vector_tile.pb.cc vector_tile.pb.h - COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} - ARGS --cpp_out ${CMAKE_BINARY_DIR} -I ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include/vector_tile.proto) - file(GLOB tilemaker_src_files src/attribute_store.cpp src/coordinates.cpp @@ -113,15 +100,13 @@ file(GLOB tilemaker_src_files src/tilemaker.cpp src/tile_worker.cpp src/way_stores.cpp - src/write_geometry.cpp ) -add_executable(tilemaker vector_tile.pb.cc ${tilemaker_src_files}) +add_executable(tilemaker ${tilemaker_src_files}) target_include_directories(tilemaker PRIVATE include) target_include_directories(tilemaker PRIVATE ${CMAKE_BINARY_DIR}) # for generated files target_link_libraries(tilemaker ${THREAD_LIB} ${CMAKE_DL_LIBS} ${LUA_LIBRARIES} - protobuf::libprotobuf shapelib::shp SQLite::SQLite3 ZLIB::ZLIB diff --git a/Dockerfile b/Dockerfile index 82fd4d50..dc172f7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,9 +9,7 @@ RUN apt-get update && \ build-essential \ liblua5.1-0 \ liblua5.1-0-dev \ - libprotobuf-dev \ libsqlite3-dev \ - protobuf-compiler \ shapelib \ libshp-dev \ libboost-program-options-dev \ @@ -36,7 +34,6 @@ FROM debian:bullseye-slim RUN apt-get update && \ apt-get install -y --no-install-recommends \ liblua5.1-0 \ - libprotobuf-dev \ libshp-dev \ libsqlite3-dev \ libboost-filesystem-dev \ @@ -49,4 +46,4 @@ COPY process.lua . COPY config.json . # Entrypoint for docker, wrapped with /bin/sh to remove requirement for executable permissions on script -ENTRYPOINT ["/bin/sh", "/resources/docker-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/bin/sh", "/resources/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile index feffd0f5..0b3db1de 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,7 @@ prefix = /usr/local MANPREFIX := /usr/share/man TM_VERSION ?= $(shell git describe --tags --abbrev=0) CXXFLAGS ?= -O3 -Wall -Wno-unknown-pragmas -Wno-sign-compare -std=c++14 -pthread -fPIE -DTM_VERSION=$(TM_VERSION) $(CONFIG) -LIB := -L$(PLATFORM_PATH)/lib -lz $(LUA_LIBS) -lboost_program_options -lsqlite3 -lboost_filesystem -lboost_system -lboost_iostreams -lprotobuf -lshp -pthread +LIB := -L$(PLATFORM_PATH)/lib -lz $(LUA_LIBS) -lboost_program_options -lsqlite3 -lboost_filesystem -lboost_system -lboost_iostreams -lshp -pthread INC := -I$(PLATFORM_PATH)/include -isystem ./include -I./src $(LUA_CFLAGS) # Targets @@ -93,7 +93,6 @@ INC := -I$(PLATFORM_PATH)/include -isystem ./include -I./src $(LUA_CFLAGS) all: tilemaker tilemaker: \ - include/vector_tile.pb.o \ src/attribute_store.o \ src/coordinates_geom.o \ src/coordinates.o \ @@ -124,8 +123,7 @@ tilemaker: \ src/tile_data.o \ src/tilemaker.o \ src/tile_worker.o \ - src/way_stores.o \ - src/write_geometry.o + src/way_stores.o $(CXX) $(CXXFLAGS) -o tilemaker $^ $(INC) $(LIB) $(LDFLAGS) test: \ @@ -194,9 +192,6 @@ test_pbf_reader: \ %.o: %.cc $(CXX) $(CXXFLAGS) -o $@ -c $< $(INC) -%.pb.cc: %.proto - protoc --proto_path=include --cpp_out=include $< - install: install -m 0755 -d $(DESTDIR)$(prefix)/bin/ install -m 0755 tilemaker $(DESTDIR)$(prefix)/bin/ diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 497cfa48..b9aa2d74 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -4,7 +4,7 @@ Install all dependencies with Homebrew: - brew install protobuf boost lua51 shapelib rapidjson + brew install boost lua51 shapelib rapidjson Then: @@ -15,7 +15,7 @@ Then: Start with: - sudo apt install build-essential libboost-dev libboost-filesystem-dev libboost-iostreams-dev libboost-program-options-dev libboost-system-dev liblua5.1-0-dev libprotobuf-dev libshp-dev libsqlite3-dev protobuf-compiler rapidjson-dev + sudo apt install build-essential libboost-dev libboost-filesystem-dev libboost-iostreams-dev libboost-program-options-dev libboost-system-dev liblua5.1-0-dev libshp-dev libsqlite3-dev rapidjson-dev Once you've installed those, then `cd` back to your Tilemaker directory and simply: @@ -28,7 +28,7 @@ If it fails, check that the LIB and INC lines in the Makefile correspond with yo Start with: - dnf install lua-devel luajit-devel sqlite-devel protobuf-devel protobuf-compiler shapelib-devel rapidjson + dnf install lua-devel luajit-devel sqlite-devel shapelib-devel rapidjson then build either with lua: diff --git a/include/output_object.h b/include/output_object.h index 9afd5cba..d994fbfa 100644 --- a/include/output_object.h +++ b/include/output_object.h @@ -10,9 +10,7 @@ #include "coordinates.h" #include "attribute_store.h" #include "osm_store.h" - -// Protobuf -#include "vector_tile.pb.h" +#include enum OutputGeometryType : unsigned int { POINT_, LINESTRING_, MULTILINESTRING_, POLYGON_ }; @@ -73,18 +71,11 @@ class OutputObject { this->attributes = attributes; } - //\brief Write attribute key/value pairs (dictionary-encoded) - void writeAttributes(std::vector *keyList, - std::vector *valueList, - AttributeStore const &attributeStore, - vector_tile::Tile_Feature *featurePtr, char zoom) const; - - /** - * \brief Find a value in the value dictionary - * (we can't easily use find() because of the different value-type encoding - - * should be possible to improve this though) - */ - int findValue(const std::vector* valueList, const AttributePair& value) const; + void writeAttributes( + const AttributeStore& attributeStore, + vtzero::feature_builder& fbuilder, + char zoom + ) const; }; #pragma pack(pop) @@ -97,9 +88,4 @@ struct OutputObjectID { bool operator==(const OutputObject& x, const OutputObject& y); bool operator==(const OutputObjectID& x, const OutputObjectID& y); -namespace vector_tile { - bool operator==(const vector_tile::Tile_Value &x, const vector_tile::Tile_Value &y); - bool operator<(const vector_tile::Tile_Value &x, const vector_tile::Tile_Value &y); -} - #endif //_OUTPUT_OBJECT_H diff --git a/include/pbf_processor.h b/include/pbf_processor.h index 65ce9ad7..d79d1ca5 100644 --- a/include/pbf_processor.h +++ b/include/pbf_processor.h @@ -11,9 +11,6 @@ #include "pbf_reader.h" #include -// Protobuf -#include "vector_tile.pb.h" - class OsmLuaProcessing; extern const std::string OptionSortTypeThenID; @@ -21,7 +18,7 @@ extern const std::string OptionLocationsOnWays; struct BlockMetadata { long int offset; - google::protobuf::int32 length; + int32_t length; bool hasNodes; bool hasWays; bool hasRelations; diff --git a/include/vector_tile.proto b/include/vector_tile.proto deleted file mode 100644 index a4be957b..00000000 --- a/include/vector_tile.proto +++ /dev/null @@ -1,102 +0,0 @@ -syntax = "proto2"; - -// Protocol Version 1 - -package vector_tile; - -// option optimize_for = LITE_RUNTIME; - -message Tile { - enum GeomType { - UNKNOWN = 0; - POINT = 1; - LINESTRING = 2; - POLYGON = 3; - } - - // Variant type encoding - message Value { - // Exactly one of these values may be present in a valid message - optional string string_value = 1; - optional float float_value = 2; - optional double double_value = 3; - optional int64 int_value = 4; - optional uint64 uint_value = 5; - optional sint64 sint_value = 6; - optional bool bool_value = 7; - - extensions 8 to max; - } - - message Feature { - optional uint64 id = 1 [ default = 0 ]; - - // Tags of this feature are encoded as repeated pairs of - // integers. Even indexed values (n, beginning with 0) are - // themselves indexes into the layer's keys list. Odd indexed - // values (n+1) are indexes into the layer's values list. - // The first (n=0) tag of a feature, therefore, has a key of - // layer.keys[feature.tags[0]] and a value of - // layer.values[feature.tags[1]]. - repeated uint32 tags = 2 [ packed = true ]; - - // The type of geometry stored in this feature. - optional GeomType type = 3 [ default = UNKNOWN ]; - - // Contains a stream of commands and parameters (vertices). The - // repeat count is shifted to the left by 3 bits. This means - // that the command has 3 bits (0-7). The repeat count - // indicates how often this command is to be repeated. Defined - // commands are: - // - MoveTo: 1 (2 parameters follow) - // - LineTo: 2 (2 parameters follow) - // - ClosePath: 7 (no parameters follow) - // - // Commands are encoded as uint32 varints. Vertex parameters - // are encoded as deltas to the previous position and, as they - // may be negative, are further "zigzag" encoded as unsigned - // 32-bit ints: - // - // n = (n << 1) ^ (n >> 31) - // - // Ex.: MoveTo(3, 6), LineTo(8, 12), LineTo(20, 34), ClosePath - // Encoded as: [ 9 6 12 18 10 12 24 44 15 ] - // | | `> [00001 111] command type 7 (ClosePath), length 1 - // | | ===== relative LineTo(+12, +22) == LineTo(20, 34) - // | | ===== relative LineTo(+5, +6) == LineTo(8, 12) - // | `> [00010 010] = command type 2 (LineTo), length 2 - // | ==== relative MoveTo(+3, +6) - // `> [00001 001] = command type 1 (MoveTo), length 1 - // - // The original position is (0,0). - repeated uint32 geometry = 4 [ packed = true ]; - } - - message Layer { - // Any compliant implementation must first read the version - // number encoded in this message and choose the correct - // implementation for this version number before proceeding to - // decode other parts of this message. - required uint32 version = 15 [ default = 1 ]; - - required string name = 1; - - // The actual features in this tile. - repeated Feature features = 2; - - // Dictionary encoding for keys - repeated string keys = 3; - - // Dictionary encoding for values - repeated Value values = 4; - - // The bounding box in this tile spans from 0..4095 units - optional uint32 extent = 5 [ default = 4096 ]; - - extensions 16 to max; - } - - repeated Layer layers = 3; - - extensions 16 to 8191; -} diff --git a/include/vtzero/builder.hpp b/include/vtzero/builder.hpp new file mode 100644 index 00000000..781758e8 --- /dev/null +++ b/include/vtzero/builder.hpp @@ -0,0 +1,1365 @@ +#ifndef VTZERO_BUILDER_HPP +#define VTZERO_BUILDER_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file builder.hpp + * + * @brief Contains the classes and functions to build vector tiles. + */ + +#include "builder_impl.hpp" +#include "feature_builder_impl.hpp" +#include "geometry.hpp" +#include "types.hpp" +#include "vector_tile.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace vtzero { + + /** + * Used to build vector tiles. Whenever you are building a new vector + * tile, start with an object of this class and add layers. After all + * the data is added, call serialize(). + * + * @code + * layer some_existing_layer = ...; + * + * tile_builder builder; + * layer_builder layer_roads{builder, "roads"}; + * builder.add_existing_layer(some_existing_layer); + * ... + * std::string data = builder.serialize(); + * @endcode + */ + class tile_builder { + + friend class layer_builder; + + std::vector> m_layers; + + /** + * Add a new layer to the vector tile based on an existing layer. The + * new layer will have the same name, version, and extent as the + * existing layer. The new layer will not contain any features. This + * method is handy when copying some (but not all) data from an + * existing layer. + */ + detail::layer_builder_impl* add_layer(const layer& layer) { + auto* ptr = new detail::layer_builder_impl{layer.name(), layer.version(), layer.extent()}; + m_layers.emplace_back(ptr); + return ptr; + } + + /** + * Add a new layer to the vector tile with the specified name, version, + * and extent. + * + * @tparam TString Some string type (const char*, std::string, + * vtzero::data_view) or something that converts to one of + * these types. + * @param name Name of this layer. + * @param version Version of this layer (only version 1 and 2 are + * supported) + * @param extent Extent used for this layer. + */ + template + detail::layer_builder_impl* add_layer(TString&& name, uint32_t version, uint32_t extent) { + auto* ptr = new detail::layer_builder_impl{std::forward(name), version, extent}; + m_layers.emplace_back(ptr); + return ptr; + } + + public: + + /// Constructor + tile_builder() = default; + + /// Destructor + ~tile_builder() noexcept = default; + + /// Tile builders can not be copied. + tile_builder(const tile_builder&) = delete; + + /// Tile builders can not be copied. + tile_builder& operator=(const tile_builder&) = delete; + + /// Tile builders can be moved. + tile_builder(tile_builder&&) = default; + + /// Tile builders can be moved. + tile_builder& operator=(tile_builder&&) = default; + + /** + * Add an existing layer to the vector tile. The layer data will be + * copied over into the new vector_tile when the serialize() method + * is called. Until then, the data referenced here must stay available. + * + * @param data Reference to some data that must be a valid encoded + * layer. + */ + void add_existing_layer(data_view&& data) { + m_layers.emplace_back(new detail::layer_builder_impl{std::forward(data)}); + } + + /** + * Add an existing layer to the vector tile. The layer data will be + * copied over into the new vector_tile when the serialize() method + * is called. Until then, the data referenced here must stay available. + * + * @param layer Reference to the layer to be copied. + */ + void add_existing_layer(const layer& layer) { + add_existing_layer(layer.data()); + } + + /** + * Serialize the data accumulated in this builder into a vector tile. + * The data will be appended to the specified buffer. The buffer + * doesn't have to be empty. + * + * @tparam TBuffer Type of buffer. Must be std:string or other buffer + * type supported by protozero. + * @param buffer Buffer to append the encoded vector tile to. + */ + template + void serialize(TBuffer& buffer) const { + const std::size_t estimated_size = std::accumulate(m_layers.cbegin(), m_layers.cend(), 0ULL, [](std::size_t sum, const std::unique_ptr& layer) { + return sum + layer->estimated_size(); + }); + + protozero::basic_pbf_builder pbf_tile_builder{buffer}; + pbf_tile_builder.reserve(estimated_size); + for (const auto& layer : m_layers) { + layer->build(pbf_tile_builder); + } + } + + /** + * Serialize the data accumulated in this builder into a vector_tile + * and return it. + * + * If you want to use an existing buffer instead, use the serialize() + * member function taking a TBuffer& as parameter. + * + * @returns std::string Buffer with encoded vector_tile data. + */ + std::string serialize() const { + std::string data; + serialize(data); + return data; + } + + }; // class tile_builder + + /** + * The layer_builder is used to add a new layer to a vector tile that is + * being built. + */ + class layer_builder { + + vtzero::detail::layer_builder_impl* m_layer; + + friend class geometry_feature_builder; + friend class point_feature_builder; + friend class linestring_feature_builder; + friend class polygon_feature_builder; + + vtzero::detail::layer_builder_impl& get_layer_impl() noexcept { + return *m_layer; + } + + template + using is_layer = std::is_same::type>::type, layer>; + + public: + + /** + * Construct a layer_builder to build a new layer with the same name, + * version, and extent as an existing layer. + * + * @param tile The tile builder we want to create this layer in. + * @param layer Existing layer we want to use the name, version, and + * extent from + */ + layer_builder(vtzero::tile_builder& tile, const layer& layer) : + m_layer(tile.add_layer(layer)) { + } + + /** + * Construct a layer_builder to build a completely new layer. + * + * @tparam TString Some string type (such as std::string or const char*) + * @param tile The tile builder we want to create this layer in. + * @param name The name of the new layer. + * @param version The vector tile spec version of the new layer. + * @param extent The extent of the new layer. + */ + template ::value, int>::type = 0> + layer_builder(vtzero::tile_builder& tile, TString&& name, uint32_t version = 2, uint32_t extent = 4096) : + m_layer(tile.add_layer(std::forward(name), version, extent)) { + } + + /** + * Add key to the keys table without checking for duplicates. This + * function is usually used when an external index is used which takes + * care of the duplication check. + * + * @param text The key. + * @returns The index value of this key. + */ + index_value add_key_without_dup_check(const data_view text) { + return m_layer->add_key_without_dup_check(text); + } + + /** + * Add key to the keys table. This function will consult the internal + * index in the layer to make sure the key is only in the table once. + * It will either return the index value of an existing key or add the + * new key and return its index value. + * + * @param text The key. + * @returns The index value of this key. + */ + index_value add_key(const data_view text) { + return m_layer->add_key(text); + } + + /** + * Add value to the values table without checking for duplicates. This + * function is usually used when an external index is used which takes + * care of the duplication check. + * + * @param value The property value. + * @returns The index value of this value. + */ + index_value add_value_without_dup_check(const property_value value) { + return m_layer->add_value_without_dup_check(value); + } + + /** + * Add value to the values table without checking for duplicates. This + * function is usually used when an external index is used which takes + * care of the duplication check. + * + * @param value The property value. + * @returns The index value of this value. + */ + index_value add_value_without_dup_check(const encoded_property_value& value) { + return m_layer->add_value_without_dup_check(value); + } + + /** + * Add value to the values table. This function will consult the + * internal index in the layer to make sure the value is only in the + * table once. It will either return the index value of an existing + * value or add the new value and return its index value. + * + * @param value The property value. + * @returns The index value of this value. + */ + index_value add_value(const property_value value) { + return m_layer->add_value(value); + } + + /** + * Add value to the values table. This function will consult the + * internal index in the layer to make sure the value is only in the + * table once. It will either return the index value of an existing + * value or add the new value and return its index value. + * + * @param value The property value. + * @returns The index value of this value. + */ + index_value add_value(const encoded_property_value& value) { + return m_layer->add_value(value); + } + + /** + * Add a feature from an existing layer to the new layer. The feature + * will be copied completely over to the new layer including its + * geometry and all its properties. + */ + void add_feature(const feature& feature); + + }; // class layer_builder + + /** + * Parent class for the point_feature_builder, linestring_feature_builder + * and polygon_feature_builder classes. You can not instantiate this class + * directly, use it through its derived classes. + */ + class feature_builder : public detail::feature_builder_base { + + class countdown_value { + + uint32_t m_value = 0; + + public: + + countdown_value() noexcept = default; + + ~countdown_value() noexcept { + assert_is_zero(); + } + + countdown_value(const countdown_value&) = delete; + + countdown_value& operator=(const countdown_value&) = delete; + + countdown_value(countdown_value&& other) noexcept : + m_value(other.m_value) { + other.m_value = 0; + } + + countdown_value& operator=(countdown_value&& other) noexcept { + m_value = other.m_value; + other.m_value = 0; + return *this; + } + + uint32_t value() const noexcept { + return m_value; + } + + void set(const uint32_t value) noexcept { + m_value = value; + } + + void decrement() { + vtzero_assert(m_value > 0 && "too many calls to set_point()"); + --m_value; + } + + void assert_is_zero() const noexcept { + vtzero_assert_in_noexcept_function(m_value == 0 && + "not enough calls to set_point()"); + } + + }; // countdown_value + + protected: + + /// Encoded geometry. + protozero::packed_field_uint32 m_pbf_geometry{}; + + /// Number of points still to be set for the geometry to be complete. + countdown_value m_num_points; + + /// Last point (used to calculate delta between coordinates) + point m_cursor{0, 0}; + + /// Constructor. + explicit feature_builder(detail::layer_builder_impl* layer) : + feature_builder_base(layer) { + } + + /// Helper function to check size isn't too large + template + uint32_t check_num_points(T size) { + if (size >= (1UL << 29U)) { + throw geometry_exception{"Maximum of 2^29 - 1 points allowed in geometry"}; + } + return static_cast(size); + } + + /// Helper function to make sure we have everything before adding a property + void prepare_to_add_property() { + if (m_pbf_geometry.valid()) { + m_num_points.assert_is_zero(); + m_pbf_geometry.commit(); + } + if (!m_pbf_tags.valid()) { + m_pbf_tags = {m_feature_writer, detail::pbf_feature::tags}; + } + } + + public: + + /** + * If the feature was not committed, the destructor will roll back all + * the changes. + */ + ~feature_builder() { + try { + rollback(); + } catch (...) { + // ignore exceptions + } + } + + /// Builder classes can not be copied + feature_builder(const feature_builder&) = delete; + + /// Builder classes can not be copied + feature_builder& operator=(const feature_builder&) = delete; + + /// Builder classes can be moved + feature_builder(feature_builder&& other) noexcept = default; + + /// Builder classes can be moved + feature_builder& operator=(feature_builder&& other) noexcept = default; + + /** + * Set the ID of this feature. + * + * You can only call this method once and it must be before calling + * any method manipulating the geometry. + * + * @param id The ID. + */ + void set_id(uint64_t id) { + vtzero_assert(m_feature_writer.valid() && + "Can not call set_id() after commit() or rollback()"); + vtzero_assert(!m_pbf_geometry.valid() && + !m_pbf_tags.valid() && + "Call set_id() before setting the geometry or adding properties"); + set_id_impl(id); + } + + /** + * Copy the ID of an existing feature to this feature. If the + * feature doesn't have an ID, no ID is set. + * + * You can only call this method once and it must be before calling + * any method manipulating the geometry. + * + * @param feature The feature to copy the ID from. + */ + void copy_id(const feature& feature) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_id() after commit() or rollback()"); + vtzero_assert(!m_pbf_geometry.valid() && + !m_pbf_tags.valid() && + "Call copy_id() before setting the geometry or adding properties"); + if (feature.has_id()) { + set_id_impl(feature.id()); + } + } + + /** + * Add a property to this feature. Can only be called after all the + * methods manipulating the geometry. + * + * @tparam TProp Can be type index_value_pair or property. + * @param prop The property to add. + */ + template + void add_property(TProp&& prop) { + vtzero_assert(m_feature_writer.valid() && + "Can not call add_property() after commit() or rollback()"); + prepare_to_add_property(); + add_property_impl(std::forward(prop)); + } + + /** + * Copy all properties of an existing feature to the one being built. + * + * @param feature The feature to copy the properties from. + */ + void copy_properties(const feature& feature) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_properties() after commit() or rollback()"); + prepare_to_add_property(); + feature.for_each_property([this](const property& prop) { + add_property_impl(prop); + return true; + }); + } + + /** + * Copy all properties of an existing feature to the one being built + * using a property_mapper. + * + * @tparam TMapper Must be the property_mapper class or something + * equivalent. + * @param feature The feature to copy the properties from. + * @param mapper Instance of the property_mapper class. + */ + template + void copy_properties(const feature& feature, TMapper& mapper) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_properties() after commit() or rollback()"); + prepare_to_add_property(); + feature.for_each_property_indexes([this, &mapper](const index_value_pair& idxs) { + add_property_impl(mapper(idxs)); + return true; + }); + } + + /** + * Add a property to this feature. Can only be called after all the + * methods manipulating the geometry. + * + * @tparam TKey Can be type index_value or data_view or anything that + * converts to it. + * @tparam TValue Can be type index_value or property_value or + * encoded_property or anything that converts to it. + * @param key The key. + * @param value The value. + */ + template + void add_property(TKey&& key, TValue&& value) { + vtzero_assert(m_feature_writer.valid() && + "Can not call add_property() after commit() or rollback()"); + prepare_to_add_property(); + add_property_impl(std::forward(key), std::forward(value)); + } + + /** + * Commit this feature. Call this after all the details of this + * feature have been added. If this is not called, the feature + * will be rolled back when the destructor of the feature_builder is + * called. + * + * Once a feature has been committed or rolled back, further calls + * to commit() or rollback() don't do anything. + */ + void commit() { + if (m_feature_writer.valid()) { + vtzero_assert((m_pbf_geometry.valid() || m_pbf_tags.valid()) && + "Can not call commit before geometry was added"); + if (m_pbf_geometry.valid()) { + m_pbf_geometry.commit(); + } + do_commit(); + } + } + + /** + * Rollback this feature. Removed all traces of this feature from + * the layer_builder. Useful when you started creating a feature + * but then find out that its geometry is invalid or something like + * it. This will also happen automatically when the feature_builder + * is destructed and commit() hasn't been called on it. + * + * Once a feature has been committed or rolled back, further calls + * to commit() or rollback() don't do anything. + */ + void rollback() { + if (m_feature_writer.valid()) { + if (m_pbf_geometry.valid()) { + m_pbf_geometry.rollback(); + } + do_rollback(); + } + } + + }; // class feature_builder + + /** + * Used for adding a feature with a point geometry to a layer. After + * creating an object of this class you can add data to the feature in a + * specific order: + * + * * Optionally add the ID using set_id(). + * * Add the (multi)point geometry using add_point(), add_points() and + * set_point(), or add_points_from_container(). + * * Optionally add any number of properties using add_property(). + * + * @code + * vtzero::tile_builder tb; + * vtzero::layer_builder lb{tb}; + * vtzero::point_feature_builder fb{lb}; + * fb.set_id(123); // optionally set ID + * fb.add_point(10, 20) // add point geometry + * fb.add_property("foo", "bar"); // add property + * @endcode + */ + class point_feature_builder : public feature_builder { + + public: + + /** + * Constructor + * + * @param layer The layer we want to create this feature in. + */ + explicit point_feature_builder(layer_builder layer) : + feature_builder(&layer.get_layer_impl()) { + m_feature_writer.add_enum(detail::pbf_feature::type, static_cast(GeomType::POINT)); + } + + /** + * Add a single point as the geometry to this feature. + * + * @param p The point to add. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_point(const point p) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_geometry.valid() && + !m_pbf_tags.valid() && + "add_point() can only be called once"); + m_pbf_geometry = {m_feature_writer, detail::pbf_feature::geometry}; + m_pbf_geometry.add_element(detail::command_move_to(1)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y)); + } + + /** + * Add a single point as the geometry to this feature. + * + * @param x X coordinate of the point to add. + * @param y Y coordinate of the point to add. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_point(const int32_t x, const int32_t y) { + add_point(point{x, y}); + } + + /** + * Add a single point as the geometry to this feature. + * + * @tparam TPoint A type that can be converted to vtzero::point using + * the create_vtzero_point function. + * @param p The point to add. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void add_point(TPoint&& p) { + add_point(create_vtzero_point(std::forward(p))); + } + + /** + * Declare the intent to add a multipoint geometry with *count* points + * to this feature. + * + * @param count The number of points in the multipoint geometry. + * + * @pre @code count > 0 && count < 2^29 @endcode + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_points(uint32_t count) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_geometry.valid() && + "can not call add_points() twice or mix with add_point()"); + vtzero_assert(!m_pbf_tags.valid() && + "add_points() has to be called before properties are added"); + vtzero_assert(count > 0 && count < (1UL << 29U) && "add_points() must be called with 0 < count < 2^29"); + m_num_points.set(count); + m_pbf_geometry = {m_feature_writer, detail::pbf_feature::geometry}; + m_pbf_geometry.add_element(detail::command_move_to(count)); + } + + /** + * Set a point in the multipoint geometry. + * + * @param p The point. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_points(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const point p) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(m_pbf_geometry.valid() && + "call add_points() before set_point()"); + vtzero_assert(!m_pbf_tags.valid() && + "set_point() has to be called before properties are added"); + m_num_points.decrement(); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + m_cursor = p; + } + + /** + * Set a point in the multipoint geometry. + * + * @param x X coordinate of the point to set. + * @param y Y coordinate of the point to set. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_points(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const int32_t x, const int32_t y) { + set_point(point{x, y}); + } + + /** + * Set a point in the multipoint geometry. + * + * @tparam TPoint A type that can be converted to vtzero::point using + * the create_vtzero_point function. + * @param p The point to add. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_points(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void set_point(TPoint&& p) { + set_point(create_vtzero_point(std::forward(p))); + } + + /** + * Add the points from the specified container as multipoint geometry + * to this feature. + * + * @tparam TContainer The container type. Must support the size() + * method, be iterable using a range for loop, and contain + * objects of type vtzero::point or something convertible to + * it. + * @param container The container to read the points from. + * + * @throws geometry_exception If there are more than 2^32-1 members in + * the container. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void add_points_from_container(const TContainer& container) { + add_points(check_num_points(container.size())); + for (const auto& element : container) { + set_point(element); + } + } + + }; // class point_feature_builder + + /** + * Used for adding a feature with a (multi)linestring geometry to a layer. + * After creating an object of this class you can add data to the + * feature in a specific order: + * + * * Optionally add the ID using set_id(). + * * Add the (multi)linestring geometry using add_linestring() or + * add_linestring_from_container(). + * * Optionally add any number of properties using add_property(). + * + * @code + * vtzero::tile_builder tb; + * vtzero::layer_builder lb{tb}; + * vtzero::linestring_feature_builder fb{lb}; + * fb.set_id(123); // optionally set ID + * fb.add_linestring(2); + * fb.set_point(10, 10); + * fb.set_point(10, 20); + * fb.add_property("foo", "bar"); // add property + * @endcode + */ + class linestring_feature_builder : public feature_builder { + + bool m_start_line = false; + + public: + + /** + * Constructor + * + * @param layer The layer we want to create this feature in. + */ + explicit linestring_feature_builder(layer_builder layer) : + feature_builder(&layer.get_layer_impl()) { + m_feature_writer.add_enum(detail::pbf_feature::type, static_cast(GeomType::LINESTRING)); + } + + /** + * Declare the intent to add a linestring geometry with *count* points + * to this feature. + * + * @param count The number of points in the linestring. + * + * @pre @code count > 1 && count < 2^29 @endcode + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_linestring(const uint32_t count) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid() && + "add_linestring() has to be called before properties are added"); + vtzero_assert(count > 1 && count < (1UL << 29U) && "add_linestring() must be called with 1 < count < 2^29"); + m_num_points.assert_is_zero(); + if (!m_pbf_geometry.valid()) { + m_pbf_geometry = {m_feature_writer, detail::pbf_feature::geometry}; + } + m_num_points.set(count); + m_start_line = true; + } + + /** + * Set a point in the multilinestring geometry opened with + * add_linestring(). + * + * @param p The point. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_linestring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const point p) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(m_pbf_geometry.valid() && + "call add_linestring() before set_point()"); + vtzero_assert(!m_pbf_tags.valid() && + "set_point() has to be called before properties are added"); + m_num_points.decrement(); + if (m_start_line) { + m_pbf_geometry.add_element(detail::command_move_to(1)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + m_pbf_geometry.add_element(detail::command_line_to(m_num_points.value())); + m_start_line = false; + } else { + if (p == m_cursor) { + throw geometry_exception{"Zero-length segments in linestrings are not allowed."}; + } + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + } + m_cursor = p; + } + + /** + * Set a point in the multilinestring geometry opened with + * add_linestring(). + * + * @param x X coordinate of the point to set. + * @param y Y coordinate of the point to set. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_linestring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const int32_t x, const int32_t y) { + set_point(point{x, y}); + } + + /** + * Set a point in the multilinestring geometry opened with + * add_linestring(). + * + * @tparam TPoint A type that can be converted to vtzero::point using + * the create_vtzero_point function. + * @param p The point to add. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_linestring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void set_point(TPoint&& p) { + set_point(create_vtzero_point(std::forward(p))); + } + + /** + * Add the points from the specified container as a linestring geometry + * to this feature. + * + * @tparam TContainer The container type. Must support the size() + * method, be iterable using a range for loop, and contain + * objects of type vtzero::point or something convertible to + * it. + * @param container The container to read the points from. + * + * @throws geometry_exception If there are more than 2^32-1 members in + * the container or if two consecutive points in the container + * are identical. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void add_linestring_from_container(const TContainer& container) { + add_linestring(check_num_points(container.size())); + for (const auto& element : container) { + set_point(element); + } + } + + }; // class linestring_feature_builder + + /** + * Used for adding a feature with a (multi)polygon geometry to a layer. + * After creating an object of this class you can add data to the + * feature in a specific order: + * + * * Optionally add the ID using set_id(). + * * Add the (multi)polygon geometry using add_ring() or + * add_ring_from_container(). + * * Optionally add any number of properties using add_property(). + * + * @code + * vtzero::tile_builder tb; + * vtzero::layer_builder lb{tb}; + * vtzero::polygon_feature_builder fb{lb}; + * fb.set_id(123); // optionally set ID + * fb.add_ring(5); + * fb.set_point(10, 10); + * ... + * fb.add_property("foo", "bar"); // add property + * @endcode + */ + class polygon_feature_builder : public feature_builder { + + point m_first_point{0, 0}; + bool m_start_ring = false; + + public: + + /** + * Constructor + * + * @param layer The layer we want to create this feature in. + */ + explicit polygon_feature_builder(layer_builder layer) : + feature_builder(&layer.get_layer_impl()) { + m_feature_writer.add_enum(detail::pbf_feature::type, static_cast(GeomType::POLYGON)); + } + + /** + * Declare the intent to add a ring with *count* points to this + * feature. + * + * @param count The number of points in the ring. + * + * @pre @code count > 3 && count < 2^29 @endcode + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_ring(const uint32_t count) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid() && + "add_ring() has to be called before properties are added"); + vtzero_assert(count > 3 && count < (1UL << 29U) && "add_ring() must be called with 3 < count < 2^29"); + m_num_points.assert_is_zero(); + if (!m_pbf_geometry.valid()) { + m_pbf_geometry = {m_feature_writer, detail::pbf_feature::geometry}; + } + m_num_points.set(count); + m_start_ring = true; + } + + /** + * Set a point in the ring opened with add_ring(). + * + * @param p The point. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * This exception is also thrown when the last point in the + * ring is not equal to the first point, because this would + * not create a closed ring. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_ring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const point p) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(m_pbf_geometry.valid() && + "call add_ring() before set_point()"); + vtzero_assert(!m_pbf_tags.valid() && + "set_point() has to be called before properties are added"); + m_num_points.decrement(); + if (m_start_ring) { + m_first_point = p; + m_pbf_geometry.add_element(detail::command_move_to(1)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + m_pbf_geometry.add_element(detail::command_line_to(m_num_points.value() - 1)); + m_start_ring = false; + m_cursor = p; + } else if (m_num_points.value() == 0) { + if (p != m_first_point) { + throw geometry_exception{"Last point in a ring must be the same as the first point."}; + } + // spec 4.3.3.3 "A ClosePath command MUST have a command count of 1" + m_pbf_geometry.add_element(detail::command_close_path()); + } else { + if (p == m_cursor) { + throw geometry_exception{"Zero-length segments in rings are not allowed."}; + } + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + m_cursor = p; + } + } + + /** + * Set a point in the ring opened with add_ring(). + * + * @param x X coordinate of the point to set. + * @param y Y coordinate of the point to set. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * This exception is also thrown when the last point in the + * ring is not equal to the first point, because this would + * not create a closed ring. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_ring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const int32_t x, const int32_t y) { + set_point(point{x, y}); + } + + /** + * Set a point in the ring opened with add_ring(). + * + * @tparam TPoint A type that can be converted to vtzero::point using + * the create_vtzero_point function. + * @param p The point to add. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * This exception is also thrown when the last point in the + * ring is not equal to the first point, because this would + * not create a closed ring. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_ring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void set_point(TPoint&& p) { + set_point(create_vtzero_point(std::forward(p))); + } + + /** + * Close a ring opened with add_ring(). This can be called for the + * last point (which will be equal to the first point) in the ring + * instead of calling set_point(). + * + * @pre There must have been *count* - 1 calls to set_point() + * already after a call to add_ring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void close_ring() { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(m_pbf_geometry.valid() && + "Call add_ring() before you can call close_ring()"); + vtzero_assert(!m_pbf_tags.valid() && + "close_ring() has to be called before properties are added"); + vtzero_assert(m_num_points.value() == 1 && + "wrong number of points in ring"); + m_pbf_geometry.add_element(detail::command_close_path()); + m_num_points.decrement(); + } + + /** + * Add the points from the specified container as a ring to this + * feature. + * + * @tparam TContainer The container type. Must support the size() + * method, be iterable using a range for loop, and contain + * objects of type vtzero::point or something convertible to + * it. + * @param container The container to read the points from. + * + * @throws geometry_exception If there are more than 2^32-1 members in + * the container or if two consecutive points in the container + * are identical or if the last point is not the same as the + * first point. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void add_ring_from_container(const TContainer& container) { + add_ring(check_num_points(container.size())); + for (const auto& element : container) { + set_point(element); + } + } + + }; // class polygon_feature_builder + + /** + * Used for adding a feature to a layer using an existing geometry. After + * creating an object of this class you can add data to the feature in a + * specific order: + * + * * Optionally add the ID using set_id(). + * * Add the geometry using set_geometry(). + * * Optionally add any number of properties using add_property(). + * + * @code + * auto geom = ... // get geometry from a feature you are reading + * ... + * vtzero::tile_builder tb; + * vtzero::layer_builder lb{tb}; + * vtzero::geometry_feature_builder fb{lb}; + * fb.set_id(123); // optionally set ID + * fb.set_geometry(geom) // add geometry + * fb.add_property("foo", "bar"); // add property + * @endcode + */ + class geometry_feature_builder : public detail::feature_builder_base { + + public: + + /** + * Constructor + * + * @param layer The layer we want to create this feature in. + */ + explicit geometry_feature_builder(layer_builder layer) : + feature_builder_base(&layer.get_layer_impl()) { + } + + /** + * If the feature was not committed, the destructor will roll back all + * the changes. + */ + ~geometry_feature_builder() noexcept { + try { + rollback(); + } catch (...) { + // ignore exceptions + } + } + + /// Feature builders can not be copied. + geometry_feature_builder(const geometry_feature_builder&) = delete; + + /// Feature builders can not be copied. + geometry_feature_builder& operator=(const geometry_feature_builder&) = delete; + + /// Feature builders can be moved. + geometry_feature_builder(geometry_feature_builder&&) noexcept = default; + + /// Feature builders can be moved. + geometry_feature_builder& operator=(geometry_feature_builder&&) noexcept = default; + + /** + * Set the ID of this feature. + * + * You can only call this function once and it must be before calling + * set_geometry(). + * + * @param id The ID. + */ + void set_id(uint64_t id) { + vtzero_assert(m_feature_writer.valid() && + "Can not call set_id() after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid()); + set_id_impl(id); + } + + /** + * Copy the ID of an existing feature to this feature. If the + * feature doesn't have an ID, no ID is set. + * + * You can only call this function once and it must be before calling + * set_geometry(). + * + * @param feature The feature to copy the ID from. + */ + void copy_id(const feature& feature) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_id() after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid()); + if (feature.has_id()) { + set_id_impl(feature.id()); + } + } + + /** + * Set the geometry of this feature. + * + * You can only call this method once and it must be before calling the + * add_property() method. + * + * @param geometry The geometry. + */ + void set_geometry(const geometry& geometry) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid()); + m_feature_writer.add_enum(detail::pbf_feature::type, static_cast(geometry.type())); + m_feature_writer.add_string(detail::pbf_feature::geometry, geometry.data()); + m_pbf_tags = {m_feature_writer, detail::pbf_feature::tags}; + } + + /** + * Add a property to this feature. Can only be called after the + * set_geometry method. + * + * @tparam TProp Can be type index_value_pair or property. + * @param prop The property to add. + */ + template + void add_property(TProp&& prop) { + vtzero_assert(m_feature_writer.valid() && + "Can not call add_property() after commit() or rollback()"); + add_property_impl(std::forward(prop)); + } + + /** + * Add a property to this feature. Can only be called after the + * set_geometry method. + * + * @tparam TKey Can be type index_value or data_view or anything that + * converts to it. + * @tparam TValue Can be type index_value or property_value or + * encoded_property or anything that converts to it. + * @param key The key. + * @param value The value. + */ + template + void add_property(TKey&& key, TValue&& value) { + vtzero_assert(m_feature_writer.valid() && + "Can not call add_property() after commit() or rollback()"); + add_property_impl(std::forward(key), std::forward(value)); + } + + /** + * Copy all properties of an existing feature to the one being built. + * + * @param feature The feature to copy the properties from. + */ + void copy_properties(const feature& feature) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_properties() after commit() or rollback()"); + feature.for_each_property([this](const property& prop) { + add_property_impl(prop); + return true; + }); + } + + /** + * Copy all properties of an existing feature to the one being built + * using a property_mapper. + * + * @tparam TMapper Must be the property_mapper class or something + * equivalent. + * @param feature The feature to copy the properties from. + * @param mapper Instance of the property_mapper class. + */ + template + void copy_properties(const feature& feature, TMapper& mapper) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_properties() after commit() or rollback()"); + feature.for_each_property_indexes([this, &mapper](const index_value_pair& idxs) { + add_property_impl(mapper(idxs)); + return true; + }); + } + + /** + * Commit this feature. Call this after all the details of this + * feature have been added. If this is not called, the feature + * will be rolled back when the destructor of the feature_builder is + * called. + * + * Once a feature has been committed or rolled back, further calls + * to commit() or rollback() don't do anything. + */ + void commit() { + if (m_feature_writer.valid()) { + vtzero_assert(m_pbf_tags.valid() && + "Can not call commit before geometry was added"); + do_commit(); + } + } + + /** + * Rollback this feature. Removed all traces of this feature from + * the layer_builder. Useful when you started creating a feature + * but then find out that its geometry is invalid or something like + * it. This will also happen automatically when the feature_builder + * is destructed and commit() hasn't been called on it. + * + * Once a feature has been committed or rolled back, further calls + * to commit() or rollback() don't do anything. + */ + void rollback() { + if (m_feature_writer.valid()) { + do_rollback(); + } + } + + }; // class geometry_feature_builder + + inline void layer_builder::add_feature(const feature& feature) { + geometry_feature_builder feature_builder{*this}; + if (feature.has_id()) { + feature_builder.set_id(feature.id()); + } + feature_builder.set_geometry(feature.geometry()); + feature.for_each_property([&feature_builder](const property& p) { + feature_builder.add_property(p); + return true; + }); + feature_builder.commit(); + } + +} // namespace vtzero + +#endif // VTZERO_BUILDER_HPP diff --git a/include/vtzero/builder_impl.hpp b/include/vtzero/builder_impl.hpp new file mode 100644 index 00000000..1b348df2 --- /dev/null +++ b/include/vtzero/builder_impl.hpp @@ -0,0 +1,267 @@ +#ifndef VTZERO_BUILDER_IMPL_HPP +#define VTZERO_BUILDER_IMPL_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file builder_impl.hpp + * + * @brief Contains classes internal to the builder. + */ + +#include "encoded_property_value.hpp" +#include "property_value.hpp" +#include "types.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace vtzero { + + namespace detail { + + class layer_builder_impl { + + // If this layer is copied from an existing layer, this points + // to the data of the original layer. For newly built layers, + // this is empty. + data_view m_data_view{}; + + // Buffer containing the encoded layer metadata and features + std::string m_data; + + // Buffer containing the encoded keys table + std::string m_keys_data; + + // Buffer containing the encoded values table + std::string m_values_data; + + protozero::pbf_builder m_pbf_message_layer{}; + protozero::pbf_builder m_pbf_message_keys{}; + protozero::pbf_builder m_pbf_message_values{}; + + // The number of features in the layer + std::size_t m_num_features = 0; + + // Vector tile spec version + uint32_t m_version = 0; + + // The number of keys in the keys table + uint32_t m_num_keys = 0; + + // The number of values in the values table + uint32_t m_num_values = 0; + + // Below this value, no index will be used to find entries in the + // key/value tables. This number is based on some initial + // benchmarking but probably needs some tuning. + // See also https://github.com/mapbox/vtzero/issues/30 + static constexpr const uint32_t max_entries_flat = 20; + + using map_type = std::unordered_map; + map_type m_keys_index; + map_type m_values_index; + + static index_value find_in_table(const data_view text, const std::string& data) { + uint32_t index = 0; + protozero::pbf_message pbf_table{data}; + + while (pbf_table.next()) { + const auto v = pbf_table.get_view(); + if (v == text) { + return index_value{index}; + } + ++index; + } + + return index_value{}; + } + + // Read the key or value table and populate an index from its + // entries. This is done once the table becomes too large to do + // linear search in it. + static void populate_index(const std::string& data, map_type& map) { + uint32_t index = 0; + protozero::pbf_message pbf_table{data}; + + while (pbf_table.next()) { + map[pbf_table.get_string()] = index++; + } + } + + index_value add_value_without_dup_check(const data_view text) { + m_pbf_message_values.add_string(detail::pbf_layer::values, text); + return m_num_values++; + } + + index_value add_value(const data_view text) { + const auto index = find_in_values_table(text); + if (index.valid()) { + return index; + } + return add_value_without_dup_check(text); + } + + index_value find_in_keys_table(const data_view text) { + if (m_num_keys < max_entries_flat) { + return find_in_table(text, m_keys_data); + } + + if (m_keys_index.empty()) { + populate_index(m_keys_data, m_keys_index); + } + + auto& v = m_keys_index[std::string(text)]; + if (!v.valid()) { + v = add_key_without_dup_check(text); + } + return v; + } + + index_value find_in_values_table(const data_view text) { + if (m_num_values < max_entries_flat) { + return find_in_table(text, m_values_data); + } + + if (m_values_index.empty()) { + populate_index(m_values_data, m_values_index); + } + + auto& v = m_values_index[std::string(text)]; + if (!v.valid()) { + v = add_value_without_dup_check(text); + } + return v; + } + + public: + + // This layer should be a copy of an existing layer + explicit layer_builder_impl(const data_view data) : + m_data_view(data) { + } + + // This layer is being created from scratch + template + layer_builder_impl(TString&& name, uint32_t version, uint32_t extent) : + m_pbf_message_layer(m_data), + m_pbf_message_keys(m_keys_data), + m_pbf_message_values(m_values_data), + m_version(version) { + m_pbf_message_layer.add_uint32(detail::pbf_layer::version, version); + m_pbf_message_layer.add_string(detail::pbf_layer::name, std::forward(name)); + m_pbf_message_layer.add_uint32(detail::pbf_layer::extent, extent); + } + + ~layer_builder_impl() noexcept = default; + + layer_builder_impl(const layer_builder_impl&) = delete; + layer_builder_impl& operator=(const layer_builder_impl&) = delete; + + layer_builder_impl(layer_builder_impl&&) = default; + layer_builder_impl& operator=(layer_builder_impl&&) = default; + + uint32_t version() const noexcept { + return m_version; + } + + index_value add_key_without_dup_check(const data_view text) { + m_pbf_message_keys.add_string(detail::pbf_layer::keys, text); + return m_num_keys++; + } + + index_value add_key(const data_view text) { + const auto index = find_in_keys_table(text); + if (index.valid()) { + return index; + } + return add_key_without_dup_check(text); + } + + index_value add_value_without_dup_check(const property_value value) { + return add_value_without_dup_check(value.data()); + } + + index_value add_value_without_dup_check(const encoded_property_value& value) { + return add_value_without_dup_check(value.data()); + } + + index_value add_value(const property_value value) { + return add_value(value.data()); + } + + index_value add_value(const encoded_property_value& value) { + return add_value(value.data()); + } + + const std::string& data() const noexcept { + return m_data; + } + + const std::string& keys_data() const noexcept { + return m_keys_data; + } + + const std::string& values_data() const noexcept { + return m_values_data; + } + + protozero::pbf_builder& message() noexcept { + return m_pbf_message_layer; + } + + void increment_feature_count() noexcept { + ++m_num_features; + } + + std::size_t estimated_size() const { + if (m_data_view.data()) { + // This is a layer created as copy from an existing layer + constexpr const std::size_t estimated_overhead_for_pbf_encoding = 8; + return m_data_view.size() + estimated_overhead_for_pbf_encoding; + } + + // This is a layer created from scratch + constexpr const std::size_t estimated_overhead_for_pbf_encoding = 8; + return data().size() + + keys_data().size() + + values_data().size() + + estimated_overhead_for_pbf_encoding; + } + + template + void build(protozero::basic_pbf_builder& pbf_tile_builder) const { + if (m_data_view.data()) { + // This is a layer created as copy from an existing layer + pbf_tile_builder.add_bytes(detail::pbf_tile::layers, m_data_view); + return; + } + + // This is a layer created from scratch + if (m_num_features > 0) { + pbf_tile_builder.add_bytes_vectored(detail::pbf_tile::layers, + data(), + keys_data(), + values_data()); + } + } + + }; // class layer_builder_impl + + } // namespace detail + +} // namespace vtzero + +#endif // VTZERO_BUILDER_IMPL_HPP diff --git a/include/vtzero/encoded_property_value.hpp b/include/vtzero/encoded_property_value.hpp new file mode 100644 index 00000000..8a573f3e --- /dev/null +++ b/include/vtzero/encoded_property_value.hpp @@ -0,0 +1,244 @@ +#ifndef VTZERO_ENCODED_PROPERTY_VALUE_HPP +#define VTZERO_ENCODED_PROPERTY_VALUE_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file encoded_property_value.hpp + * + * @brief Contains the encoded_property_value class. + */ + +#include "types.hpp" + +#include + +#include +#include + +namespace vtzero { + + /** + * A property value encoded in the vector_tile internal format. Can be + * created from values of many different types and then later added to + * a layer/feature. + */ + class encoded_property_value { + + std::string m_data; + + public: + + /// Construct from vtzero::string_value_type. + explicit encoded_property_value(string_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value.value); + } + + /// Construct from const char*. + explicit encoded_property_value(const char* value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value); + } + + /// Construct from const char* and size_t. + explicit encoded_property_value(const char* value, std::size_t size) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value, size); + } + + /// Construct from std::string. + explicit encoded_property_value(const std::string& value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value); + } + + /// Construct from vtzero::data_view. + explicit encoded_property_value(const data_view& value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value); + } + + // ------------------ + + /// Construct from vtzero::float_value_type. + explicit encoded_property_value(float_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_float(detail::pbf_value::float_value, value.value); + } + + /// Construct from float. + explicit encoded_property_value(float value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_float(detail::pbf_value::float_value, value); + } + + // ------------------ + + /// Construct from vtzero::double_value_type. + explicit encoded_property_value(double_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_double(detail::pbf_value::double_value, value.value); + } + + /// Construct from double. + explicit encoded_property_value(double value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_double(detail::pbf_value::double_value, value); + } + + // ------------------ + + /// Construct from vtzero::int_value_type. + explicit encoded_property_value(int_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_int64(detail::pbf_value::int_value, value.value); + } + + /// Construct from int64_t. + explicit encoded_property_value(int64_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_int64(detail::pbf_value::int_value, value); + } + + /// Construct from int32_t. + explicit encoded_property_value(int32_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_int64(detail::pbf_value::int_value, static_cast(value)); + } + + /// Construct from int16_t. + explicit encoded_property_value(int16_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_int64(detail::pbf_value::int_value, static_cast(value)); + } + + // ------------------ + + /// Construct from vtzero::uint_value_type. + explicit encoded_property_value(uint_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_uint64(detail::pbf_value::uint_value, value.value); + } + + /// Construct from uint64_t. + explicit encoded_property_value(uint64_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_uint64(detail::pbf_value::uint_value, value); + } + + /// Construct from uint32_t. + explicit encoded_property_value(uint32_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_uint64(detail::pbf_value::uint_value, static_cast(value)); + } + + /// Construct from uint16_t. + explicit encoded_property_value(uint16_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_uint64(detail::pbf_value::uint_value, static_cast(value)); + } + + // ------------------ + + /// Construct from vtzero::sint_value_type. + explicit encoded_property_value(sint_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_sint64(detail::pbf_value::sint_value, value.value); + } + + // ------------------ + + /// Construct from vtzero::bool_value_type. + explicit encoded_property_value(bool_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_bool(detail::pbf_value::bool_value, value.value); + } + + /// Construct from bool. + explicit encoded_property_value(bool value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_bool(detail::pbf_value::bool_value, value); + } + + // ------------------ + + /** + * Get view of the raw data stored inside. + */ + data_view data() const noexcept { + return {m_data.data(), m_data.size()}; + } + + /** + * Hash function compatible with std::hash. + */ + std::size_t hash() const noexcept { + return std::hash{}(m_data); + } + + }; // class encoded_property_value + + /// Encoded property values are equal if they contain the same data. + inline bool operator==(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() == rhs.data(); + } + + /// Encoded property values are unequal if they are not equal. + inline bool operator!=(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return !(lhs == rhs); + } + + /// Arbitrary ordering based on internal data. + inline bool operator<(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() < rhs.data(); + } + + /// Arbitrary ordering based on internal data. + inline bool operator<=(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() <= rhs.data(); + } + + /// Arbitrary ordering based on internal data. + inline bool operator>(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() > rhs.data(); + } + + /// Arbitrary ordering based on internal data. + inline bool operator>=(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() >= rhs.data(); + } + +} // namespace vtzero + +namespace std { + + /** + * Specialization of std::hash for encoded_property_value. + */ + template <> + struct hash { + + /// key vtzero::encoded_property_value + using argument_type = vtzero::encoded_property_value; + + /// hash result: size_t + using result_type = std::size_t; + + /// calculate the hash of the argument + std::size_t operator()(const vtzero::encoded_property_value& value) const noexcept { + return value.hash(); + } + + }; // struct hash + +} // namespace std + +#endif // VTZERO_ENCODED_PROPERTY_VALUE_HPP diff --git a/include/vtzero/exception.hpp b/include/vtzero/exception.hpp new file mode 100644 index 00000000..80b72595 --- /dev/null +++ b/include/vtzero/exception.hpp @@ -0,0 +1,134 @@ +#ifndef VTZERO_EXCEPTION_HPP +#define VTZERO_EXCEPTION_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file exception.hpp + * + * @brief Contains the exceptions used in the vtzero library. + */ + +#include +#include +#include + +namespace vtzero { + + /** + * Base class for all exceptions directly thrown by the vtzero library. + */ + class exception : public std::runtime_error { + + public: + + /// Constructor + explicit exception(const char* message) : + std::runtime_error(message) { + } + + /// Constructor + explicit exception(const std::string& message) : + std::runtime_error(message) { + } + + }; // class exception + + /** + * This exception is thrown when vector tile encoding isn't valid according + * to the vector tile specification. + */ + class format_exception : public exception { + + public: + + /// Constructor + explicit format_exception(const char* message) : + exception(message) { + } + + /// Constructor + explicit format_exception(const std::string& message) : + exception(message) { + } + + }; // class format_exception + + /** + * This exception is thrown when a geometry encoding isn't valid according + * to the vector tile specification. + */ + class geometry_exception : public format_exception { + + public: + + /// Constructor + explicit geometry_exception(const char* message) : + format_exception(message) { + } + + /// Constructor + explicit geometry_exception(const std::string& message) : + format_exception(message) { + } + + }; // class geometry_exception + + /** + * This exception is thrown when a property value is accessed using the + * wrong type. + */ + class type_exception : public exception { + + public: + + /// Constructor + explicit type_exception() : + exception("wrong property value type") { + } + + }; // class type_exception + + /** + * This exception is thrown when an unknown version number is found in the + * layer. + */ + class version_exception : public exception { + + public: + + /// Constructor + explicit version_exception(const uint32_t version) : + exception(std::string{"unknown vector tile version: "} + + std::to_string(version)) { + } + + }; // version_exception + + /** + * This exception is thrown when an index into the key or value table + * in a layer is out of range. This can only happen if the tile data is + * invalid. + */ + class out_of_range_exception : public exception { + + public: + + /// Constructor + explicit out_of_range_exception(const uint32_t index) : + exception(std::string{"index out of range: "} + + std::to_string(index)) { + } + + }; // out_of_range_exception + +} // namespace vtzero + +#endif // VTZERO_EXCEPTION_HPP diff --git a/include/vtzero/feature.hpp b/include/vtzero/feature.hpp new file mode 100644 index 00000000..745d49a1 --- /dev/null +++ b/include/vtzero/feature.hpp @@ -0,0 +1,315 @@ +#ifndef VTZERO_FEATURE_HPP +#define VTZERO_FEATURE_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file feature.hpp + * + * @brief Contains the feature class. + */ + +#include "exception.hpp" +#include "property.hpp" +#include "property_value.hpp" +#include "types.hpp" + +#include + +#include +#include +#include +#include + +namespace vtzero { + + class layer; + + /** + * A feature according to spec 4.2. + * + * Note that a feature will internally contain a pointer to the layer it + * came from. The layer has to stay valid as long as the feature is used. + */ + class feature { + + using uint32_it_range = protozero::iterator_range; + + const layer* m_layer = nullptr; + uint64_t m_id = 0; // defaults to 0, see https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L32 + uint32_it_range m_properties{}; + protozero::pbf_reader::const_uint32_iterator m_property_iterator{}; + std::size_t m_num_properties = 0; + data_view m_geometry{}; + GeomType m_geometry_type = GeomType::UNKNOWN; // defaults to UNKNOWN, see https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L41 + bool m_has_id = false; + + public: + + /** + * Construct an invalid feature object. + */ + feature() = default; + + /** + * Construct a feature object. + * + * @throws format_exception if the layer data is ill-formed. + */ + feature(const layer* layer, const data_view data) : + m_layer(layer) { + vtzero_assert(layer); + vtzero_assert(data.data()); + + protozero::pbf_message reader{data}; + + while (reader.next()) { + switch (reader.tag_and_type()) { + case protozero::tag_and_type(detail::pbf_feature::id, protozero::pbf_wire_type::varint): + m_id = reader.get_uint64(); + m_has_id = true; + break; + case protozero::tag_and_type(detail::pbf_feature::tags, protozero::pbf_wire_type::length_delimited): + if (m_properties.begin() != protozero::pbf_reader::const_uint32_iterator{}) { + throw format_exception{"Feature has more than one tags field"}; + } + m_properties = reader.get_packed_uint32(); + m_property_iterator = m_properties.begin(); + break; + case protozero::tag_and_type(detail::pbf_feature::type, protozero::pbf_wire_type::varint): { + const auto type = reader.get_enum(); + // spec 4.3.4 "Geometry Types" + if (type < 0 || type > 3) { + throw format_exception{"Unknown geometry type (spec 4.3.4)"}; + } + m_geometry_type = static_cast(type); + } + break; + case protozero::tag_and_type(detail::pbf_feature::geometry, protozero::pbf_wire_type::length_delimited): + if (!m_geometry.empty()) { + throw format_exception{"Feature has more than one geometry field"}; + } + m_geometry = reader.get_view(); + break; + default: + reader.skip(); // ignore unknown fields + } + } + + // spec 4.2 "A feature MUST contain a geometry field." + if (m_geometry.empty()) { + throw format_exception{"Missing geometry field in feature (spec 4.2)"}; + } + + const auto size = m_properties.size(); + if (size % 2 != 0) { + throw format_exception{"unpaired property key/value indexes (spec 4.4)"}; + } + m_num_properties = size / 2; + } + + /** + * Is this a valid feature? Valid features are those not created from + * the default constructor. + * + * Complexity: Constant. + */ + bool valid() const noexcept { + return m_geometry.data() != nullptr; + } + + /** + * Is this a valid feature? Valid features are those not created from + * the default constructor. + * + * Complexity: Constant. + */ + explicit operator bool() const noexcept { + return valid(); + } + + /** + * The ID of this feature. According to the spec IDs should be unique + * in a layer if they are set (spec 4.2). + * + * Complexity: Constant. + * + * Always returns 0 for invalid features. + */ + uint64_t id() const noexcept { + return m_id; + } + + /** + * Does this feature have an ID? + * + * Complexity: Constant. + * + * Always returns false for invalid features. + */ + bool has_id() const noexcept { + return m_has_id; + } + + /** + * The geometry type of this feature. + * + * Complexity: Constant. + * + * Always returns GeomType::UNKNOWN for invalid features. + */ + GeomType geometry_type() const noexcept { + return m_geometry_type; + } + + /** + * Get the geometry of this feature. + * + * Complexity: Constant. + * + * @pre @code valid() @endcode + */ + vtzero::geometry geometry() const noexcept { + vtzero_assert_in_noexcept_function(valid()); + return {m_geometry, m_geometry_type}; + } + + /** + * Returns true if this feature doesn't have any properties. + * + * Complexity: Constant. + * + * Always returns true for invalid features. + */ + bool empty() const noexcept { + return m_num_properties == 0; + } + + /** + * Returns the number of properties in this feature. + * + * Complexity: Constant. + * + * Always returns 0 for invalid features. + */ + std::size_t num_properties() const noexcept { + return m_num_properties; + } + + /** + * Get the next property in this feature. + * + * Complexity: Constant. + * + * @returns The next property or the invalid property if there are no + * more properties. + * @throws format_exception if the feature data is ill-formed. + * @throws any protozero exception if the protobuf encoding is invalid. + * @pre @code valid() @endcode + */ + property next_property(); + + /** + * Get the indexes into the key/value table for the next property in + * this feature. + * + * Complexity: Constant. + * + * @returns The next index_value_pair or an invalid index_value_pair + * if there are no more properties. + * @throws format_exception if the feature data is ill-formed. + * @throws out_of_range_exception if the key or value index is not + * within the range of indexes in the layer key/value table. + * @throws any protozero exception if the protobuf encoding is invalid. + * @pre @code valid() @endcode + */ + index_value_pair next_property_indexes(); + + /** + * Reset the property iterator. The next time next_property() or + * next_property_indexes() is called, it will begin from the first + * property again. + * + * Complexity: Constant. + * + * @pre @code valid() @endcode + */ + void reset_property() noexcept { + vtzero_assert_in_noexcept_function(valid()); + m_property_iterator = m_properties.begin(); + } + + /** + * Call a function for each property of this feature. + * + * @tparam TFunc The type of the function. It must take a single + * argument of type property&& and return a bool. If the + * function returns false, the iteration will be stopped. + * @param func The function to call. + * @returns true if the iteration was completed and false otherwise. + * @pre @code valid() @endcode + */ + template + bool for_each_property(TFunc&& func) const; + + /** + * Call a function for each key/value index of this feature. + * + * @tparam TFunc The type of the function. It must take a single + * argument of type index_value_pair&& and return a bool. + * If the function returns false, the iteration will be stopped. + * @param func The function to call. + * @returns true if the iteration was completed and false otherwise. + * @pre @code valid() @endcode + */ + template + bool for_each_property_indexes(TFunc&& func) const; + + }; // class feature + + /** + * Create some kind of mapping from property keys to property values. + * + * This can be used to read all properties into a std::map or similar + * object. + * + * @tparam TMap Map type (std::map, std::unordered_map, ...) Must support + * the emplace() method. + * @tparam TKey Key type, usually the key of the map type. The data_view + * of the property key is converted to this type before + * adding it to the map. + * @tparam TValue Value type, usally the value of the map type. The + * property_value is converted to this type before + * adding it to the map. + * @tparam TMapping A struct derived from property_value_mapping with the + * mapping for vtzero property value types to TValue-constructing + * types. (See convert_property_value() for details.) + * @param feature The feature to get the properties from. + * @returns An object of type TMap with all the properties. + * @pre @code feature.valid() @endcode + */ + template + TMap create_properties_map(const vtzero::feature& feature) { + TMap map; + + feature.for_each_property([&map](const property& p) { + map.emplace(TKey(p.key()), convert_property_value(p.value())); + return true; + }); + + return map; + } + +} // namespace vtzero + +#endif // VTZERO_FEATURE_HPP diff --git a/include/vtzero/feature_builder_impl.hpp b/include/vtzero/feature_builder_impl.hpp new file mode 100644 index 00000000..30674186 --- /dev/null +++ b/include/vtzero/feature_builder_impl.hpp @@ -0,0 +1,126 @@ +#ifndef VTZERO_FEATURE_BUILDER_IMPL_HPP +#define VTZERO_FEATURE_BUILDER_IMPL_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file feature_builder_impl.hpp + * + * @brief Contains classes internal to the builder. + */ + +#include "builder_impl.hpp" +#include "encoded_property_value.hpp" +#include "geometry.hpp" +#include "property.hpp" +#include "property_value.hpp" + +#include + +namespace vtzero { + + namespace detail { + + class feature_builder_base { + + layer_builder_impl* m_layer; + + void add_key_internal(index_value idx) { + vtzero_assert(idx.valid()); + m_pbf_tags.add_element(idx.value()); + } + + template + void add_key_internal(T&& key) { + add_key_internal(m_layer->add_key(data_view{std::forward(key)})); + } + + void add_value_internal(index_value idx) { + vtzero_assert(idx.valid()); + m_pbf_tags.add_element(idx.value()); + } + + void add_value_internal(property_value value) { + add_value_internal(m_layer->add_value(value)); + } + + template + void add_value_internal(T&& value) { + encoded_property_value v{std::forward(value)}; + add_value_internal(m_layer->add_value(v)); + } + + protected: + + protozero::pbf_builder m_feature_writer; + protozero::packed_field_uint32 m_pbf_tags; + + explicit feature_builder_base(layer_builder_impl* layer) : + m_layer(layer), + m_feature_writer(layer->message(), detail::pbf_layer::features) { + } + + ~feature_builder_base() noexcept = default; + + feature_builder_base(const feature_builder_base&) = delete; // NOLINT(hicpp-use-equals-delete, modernize-use-equals-delete) + + feature_builder_base& operator=(const feature_builder_base&) = delete; // NOLINT(hicpp-use-equals-delete, modernize-use-equals-delete) + // The check wants these functions to be public... + + feature_builder_base(feature_builder_base&&) noexcept = default; + + feature_builder_base& operator=(feature_builder_base&&) noexcept = default; + + uint32_t version() const noexcept { + return m_layer->version(); + } + + void set_id_impl(uint64_t id) { + m_feature_writer.add_uint64(detail::pbf_feature::id, id); + } + + void add_property_impl(const property& property) { + add_key_internal(property.key()); + add_value_internal(property.value()); + } + + void add_property_impl(const index_value_pair idxs) { + add_key_internal(idxs.key()); + add_value_internal(idxs.value()); + } + + template + void add_property_impl(TKey&& key, TValue&& value) { + add_key_internal(std::forward(key)); + add_value_internal(std::forward(value)); + } + + void do_commit() { + if (m_pbf_tags.valid()) { + m_pbf_tags.commit(); + } + m_feature_writer.commit(); + m_layer->increment_feature_count(); + } + + void do_rollback() { + if (m_pbf_tags.valid()) { + m_pbf_tags.rollback(); + } + m_feature_writer.rollback(); + } + + }; // class feature_builder_base + + } // namespace detail + +} // namespace vtzero + +#endif // VTZERO_FEATURE_BUILDER_IMPL_HPP diff --git a/include/vtzero/geometry.hpp b/include/vtzero/geometry.hpp new file mode 100644 index 00000000..42af6236 --- /dev/null +++ b/include/vtzero/geometry.hpp @@ -0,0 +1,444 @@ +#ifndef VTZERO_GEOMETRY_HPP +#define VTZERO_GEOMETRY_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file geometry.hpp + * + * @brief Contains classes and functions related to geometry handling. + */ + +#include "exception.hpp" +#include "types.hpp" + +#include + +#include +#include +#include + +namespace vtzero { + + /// A simple point class + struct point { + + /// X coordinate + int32_t x = 0; + + /// Y coordinate + int32_t y = 0; + + /// Default construct to 0 coordinates + constexpr point() noexcept = default; + + /// Constructor + constexpr point(int32_t x_, int32_t y_) noexcept : + x(x_), + y(y_) { + } + + }; // struct point + + /** + * Type of a polygon ring. This can either be "outer", "inner", or + * "invalid". Invalid is used when the area of the ring is 0. + */ + enum class ring_type { + outer = 0, + inner = 1, + invalid = 2 + }; // enum class ring_type + + /** + * Helper function to create a point from any type that has members x + * and y. + * + * If your point type doesn't have members x any y, you can overload this + * function for your type and it will be used by vtzero. + */ + template + point create_vtzero_point(const TPoint& p) noexcept { + return {p.x, p.y}; + } + + /// Points are equal if their coordinates are + inline constexpr bool operator==(const point a, const point b) noexcept { + return a.x == b.x && a.y == b.y; + } + + /// Points are not equal if their coordinates aren't + inline constexpr bool operator!=(const point a, const point b) noexcept { + return !(a == b); + } + + namespace detail { + + /// The command id type as specified in the vector tile spec + enum class CommandId : uint32_t { + MOVE_TO = 1, + LINE_TO = 2, + CLOSE_PATH = 7 + }; + + inline constexpr uint32_t command_integer(CommandId id, const uint32_t count) noexcept { + return (static_cast(id) & 0x7U) | (count << 3U); + } + + inline constexpr uint32_t command_move_to(const uint32_t count) noexcept { + return command_integer(CommandId::MOVE_TO, count); + } + + inline constexpr uint32_t command_line_to(const uint32_t count) noexcept { + return command_integer(CommandId::LINE_TO, count); + } + + inline constexpr uint32_t command_close_path() noexcept { + return command_integer(CommandId::CLOSE_PATH, 1); + } + + inline constexpr uint32_t get_command_id(const uint32_t command_integer) noexcept { + return command_integer & 0x7U; + } + + inline constexpr uint32_t get_command_count(const uint32_t command_integer) noexcept { + return command_integer >> 3U; + } + + // The maximum value for the command count according to the spec. + inline constexpr uint32_t max_command_count() noexcept { + return get_command_count(std::numeric_limits::max()); + } + + inline constexpr int64_t det(const point a, const point b) noexcept { + return static_cast(a.x) * static_cast(b.y) - + static_cast(b.x) * static_cast(a.y); + } + + template + struct get_result { + + using type = void; + + template + void operator()(TGeomHandler&& /*geom_handler*/) const noexcept { + } + + }; + + template + struct get_result().result()), void>::value>::type> { + + using type = decltype(std::declval().result()); + + template + type operator()(TGeomHandler&& geom_handler) { + return std::forward(geom_handler).result(); + } + + }; + + /** + * Decode a geometry as specified in spec 4.3 from a sequence of 32 bit + * unsigned integers. This templated base class can be instantiated + * with a different iterator type for testing than for normal use. + */ + template + class geometry_decoder { + + public: + + using iterator_type = TIterator; + + private: + + iterator_type m_it; + iterator_type m_end; + + point m_cursor{0, 0}; + + // maximum value for m_count before we throw an exception + uint32_t m_max_count; + + /** + * The current count value is set from the CommandInteger and + * then counted down with each next_point() call. So it must be + * greater than 0 when next_point() is called and 0 when + * next_command() is called. + */ + uint32_t m_count = 0; + + public: + + geometry_decoder(iterator_type begin, iterator_type end, std::size_t max) : + m_it(begin), + m_end(end), + m_max_count(static_cast(max)) { + vtzero_assert(max <= detail::max_command_count()); + } + + uint32_t count() const noexcept { + return m_count; + } + + bool done() const noexcept { + return m_it == m_end; + } + + bool next_command(const CommandId expected_command_id) { + vtzero_assert(m_count == 0); + + if (m_it == m_end) { + return false; + } + + const auto command_id = get_command_id(*m_it); + if (command_id != static_cast(expected_command_id)) { + throw geometry_exception{std::string{"expected command "} + + std::to_string(static_cast(expected_command_id)) + + " but got " + + std::to_string(command_id)}; + } + + if (expected_command_id == CommandId::CLOSE_PATH) { + // spec 4.3.3.3 "A ClosePath command MUST have a command count of 1" + if (get_command_count(*m_it) != 1) { + throw geometry_exception{"ClosePath command count is not 1"}; + } + } else { + m_count = get_command_count(*m_it); + if (m_count > m_max_count) { + throw geometry_exception{"count too large"}; + } + } + + ++m_it; + + return true; + } + + point next_point() { + vtzero_assert(m_count > 0); + + if (m_it == m_end || std::next(m_it) == m_end) { + throw geometry_exception{"too few points in geometry"}; + } + + // spec 4.3.2 "A ParameterInteger is zigzag encoded" + int64_t x = protozero::decode_zigzag32(*m_it++); + int64_t y = protozero::decode_zigzag32(*m_it++); + + // x and y are int64_t so this addition can never overflow + x += m_cursor.x; + y += m_cursor.y; + + // The cast is okay, because a valid vector tile can never + // contain values that would overflow here and we don't care + // what happens to invalid tiles here. + m_cursor.x = static_cast(x); + m_cursor.y = static_cast(y); + + --m_count; + + return m_cursor; + } + + template + typename detail::get_result::type decode_point(TGeomHandler&& geom_handler) { + // spec 4.3.4.2 "MUST consist of a single MoveTo command" + if (!next_command(CommandId::MOVE_TO)) { + throw geometry_exception{"expected MoveTo command (spec 4.3.4.2)"}; + } + + // spec 4.3.4.2 "command count greater than 0" + if (count() == 0) { + throw geometry_exception{"MoveTo command count is zero (spec 4.3.4.2)"}; + } + + geom_handler.points_begin(count()); + while (count() > 0) { + geom_handler.points_point(next_point()); + } + + // spec 4.3.4.2 "MUST consist of of a single ... command" + if (!done()) { + throw geometry_exception{"additional data after end of geometry (spec 4.3.4.2)"}; + } + + geom_handler.points_end(); + + return detail::get_result{}(std::forward(geom_handler)); + } + + template + typename detail::get_result::type decode_linestring(TGeomHandler&& geom_handler) { + // spec 4.3.4.3 "1. A MoveTo command" + while (next_command(CommandId::MOVE_TO)) { + // spec 4.3.4.3 "with a command count of 1" + if (count() != 1) { + throw geometry_exception{"MoveTo command count is not 1 (spec 4.3.4.3)"}; + } + + const auto first_point = next_point(); + + // spec 4.3.4.3 "2. A LineTo command" + if (!next_command(CommandId::LINE_TO)) { + throw geometry_exception{"expected LineTo command (spec 4.3.4.3)"}; + } + + // spec 4.3.4.3 "with a command count greater than 0" + if (count() == 0) { + throw geometry_exception{"LineTo command count is zero (spec 4.3.4.3)"}; + } + + geom_handler.linestring_begin(count() + 1); + + geom_handler.linestring_point(first_point); + while (count() > 0) { + geom_handler.linestring_point(next_point()); + } + + geom_handler.linestring_end(); + } + + return detail::get_result{}(std::forward(geom_handler)); + } + + template + typename detail::get_result::type decode_polygon(TGeomHandler&& geom_handler) { + // spec 4.3.4.4 "1. A MoveTo command" + while (next_command(CommandId::MOVE_TO)) { + // spec 4.3.4.4 "with a command count of 1" + if (count() != 1) { + throw geometry_exception{"MoveTo command count is not 1 (spec 4.3.4.4)"}; + } + + int64_t sum = 0; + const point start_point = next_point(); + point last_point = start_point; + + // spec 4.3.4.4 "2. A LineTo command" + if (!next_command(CommandId::LINE_TO)) { + throw geometry_exception{"expected LineTo command (spec 4.3.4.4)"}; + } + + geom_handler.ring_begin(count() + 2); + + geom_handler.ring_point(start_point); + + while (count() > 0) { + const point p = next_point(); + sum += detail::det(last_point, p); + last_point = p; + geom_handler.ring_point(p); + } + + // spec 4.3.4.4 "3. A ClosePath command" + if (!next_command(CommandId::CLOSE_PATH)) { + throw geometry_exception{"expected ClosePath command (spec 4.3.4.4)"}; + } + + sum += detail::det(last_point, start_point); + + geom_handler.ring_point(start_point); + + geom_handler.ring_end(sum > 0 ? ring_type::outer : + sum < 0 ? ring_type::inner : ring_type::invalid); + } + + return detail::get_result{}(std::forward(geom_handler)); + } + + }; // class geometry_decoder + + } // namespace detail + + /** + * Decode a point geometry. + * + * @tparam TGeomHandler Handler class. See tutorial for details. + * @param geometry The geometry as returned by feature.geometry(). + * @param geom_handler An object of TGeomHandler. + * @throws geometry_error If there is a problem with the geometry. + * @pre Geometry must be a point geometry. + */ + template + typename detail::get_result::type decode_point_geometry(const geometry& geometry, TGeomHandler&& geom_handler) { + vtzero_assert(geometry.type() == GeomType::POINT); + detail::geometry_decoder decoder{geometry.begin(), geometry.end(), geometry.data().size() / 2}; + return decoder.decode_point(std::forward(geom_handler)); + } + + /** + * Decode a linestring geometry. + * + * @tparam TGeomHandler Handler class. See tutorial for details. + * @param geometry The geometry as returned by feature.geometry(). + * @param geom_handler An object of TGeomHandler. + * @returns whatever geom_handler.result() returns if that function exists, + * void otherwise + * @throws geometry_error If there is a problem with the geometry. + * @pre Geometry must be a linestring geometry. + */ + template + typename detail::get_result::type decode_linestring_geometry(const geometry& geometry, TGeomHandler&& geom_handler) { + vtzero_assert(geometry.type() == GeomType::LINESTRING); + detail::geometry_decoder decoder{geometry.begin(), geometry.end(), geometry.data().size() / 2}; + return decoder.decode_linestring(std::forward(geom_handler)); + } + + /** + * Decode a polygon geometry. + * + * @tparam TGeomHandler Handler class. See tutorial for details. + * @param geometry The geometry as returned by feature.geometry(). + * @param geom_handler An object of TGeomHandler. + * @returns whatever geom_handler.result() returns if that function exists, + * void otherwise + * @throws geometry_error If there is a problem with the geometry. + * @pre Geometry must be a polygon geometry. + */ + template + typename detail::get_result::type decode_polygon_geometry(const geometry& geometry, TGeomHandler&& geom_handler) { + vtzero_assert(geometry.type() == GeomType::POLYGON); + detail::geometry_decoder decoder{geometry.begin(), geometry.end(), geometry.data().size() / 2}; + return decoder.decode_polygon(std::forward(geom_handler)); + } + + /** + * Decode a geometry. + * + * @tparam TGeomHandler Handler class. See tutorial for details. + * @param geometry The geometry as returned by feature.geometry(). + * @param geom_handler An object of TGeomHandler. + * @returns whatever geom_handler.result() returns if that function exists, + * void otherwise + * @throws geometry_error If the geometry has type UNKNOWN of if there is + * a problem with the geometry. + */ + template + typename detail::get_result::type decode_geometry(const geometry& geometry, TGeomHandler&& geom_handler) { + detail::geometry_decoder decoder{geometry.begin(), geometry.end(), geometry.data().size() / 2}; + switch (geometry.type()) { + case GeomType::POINT: + return decoder.decode_point(std::forward(geom_handler)); + case GeomType::LINESTRING: + return decoder.decode_linestring(std::forward(geom_handler)); + case GeomType::POLYGON: + return decoder.decode_polygon(std::forward(geom_handler)); + default: + break; + } + throw geometry_exception{"unknown geometry type"}; + } + +} // namespace vtzero + +#endif // VTZERO_GEOMETRY_HPP diff --git a/include/vtzero/index.hpp b/include/vtzero/index.hpp new file mode 100644 index 00000000..75f4df73 --- /dev/null +++ b/include/vtzero/index.hpp @@ -0,0 +1,264 @@ +#ifndef VTZERO_INDEX_HPP +#define VTZERO_INDEX_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file index.hpp + * + * @brief Contains classes for indexing the key/value tables inside layers. + */ + +#include "builder.hpp" +#include "types.hpp" + +#include +#include +#include + +namespace vtzero { + + /** + * Used to store the mapping between property keys and the index value + * in the table stored in a layer. + * + * @tparam TMap The map class to use (std::map, std::unordered_map or + * something compatible). + */ + template