diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp index ae0d45e59..4656d9f01 100644 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp +++ b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp @@ -212,9 +212,9 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ } else if (type == "vector" && val["tiles"].is_array()) { tileJsons[key] = val; - } else if (type == "geojson") { - nlohmann::json geojson; + } else if (type == "geojson" || type == "geobuf") { Options options; + const auto sourceDataFormat = type == "geobuf" ? GeoDataFormat::Geobuf : GeoDataFormat::GeoJson; if (val["minzoom"].is_number_integer()) { options.minZoom = val["minzoom"].get(); @@ -226,7 +226,14 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ options.extent = val["extent"].get(); } if (val["data"].is_string()) { - geojsonSources[key] = GeoJsonVTFactory::getGeoJsonVt(key, replaceUrlParams(val["data"].get(), sourceUrlParams), loaders, localDataProvider, stringTable, options); + geojsonSources[key] = GeoJsonVTFactory::getGeoJsonVt( + key, + replaceUrlParams(val["data"].get(), sourceUrlParams), + loaders, + localDataProvider, + stringTable, + sourceDataFormat, + options); } else { try { geojsonSources[key] = GeoJsonVTFactory::getGeoJsonVt(GeoJsonParser::getGeoJson(val["data"], *stringTable), stringTable, options); diff --git a/shared/src/map/layers/tiled/vector/geojson/GeoJsonVTFactory.h b/shared/src/map/layers/tiled/vector/geojson/GeoJsonVTFactory.h index 9d1a9b609..610495385 100644 --- a/shared/src/map/layers/tiled/vector/geojson/GeoJsonVTFactory.h +++ b/shared/src/map/layers/tiled/vector/geojson/GeoJsonVTFactory.h @@ -25,8 +25,10 @@ class GeoJsonVTFactory { const std::string &geoJsonUrl, const std::vector> &loaders, const std::shared_ptr &localDataProvider, const std::shared_ptr &stringTable, + GeoDataFormat dataFormat, const Options& options = Options()) { - std::shared_ptr vt = std::make_shared(sourceName, geoJsonUrl, loaders, localDataProvider, stringTable, options); + std::shared_ptr vt = + std::make_shared(sourceName, geoJsonUrl, loaders, localDataProvider, stringTable, dataFormat, options); vt->load(); return vt; } diff --git a/shared/src/map/layers/tiled/vector/geojson/GeobufParser.cpp b/shared/src/map/layers/tiled/vector/geojson/GeobufParser.cpp new file mode 100644 index 000000000..526e44f5f --- /dev/null +++ b/shared/src/map/layers/tiled/vector/geojson/GeobufParser.cpp @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#include "GeobufParser.h" + +#include "GeoJsonParser.h" +#include "json.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct DecoderContext { + std::vector keys; + std::vector values; + uint32_t dimensions = 2; + double precisionScale = 1e6; +}; + +std::string geometryTypeFromEnum(int32_t type) { + switch (type) { + case 0: + return "Point"; + case 1: + return "MultiPoint"; + case 2: + return "LineString"; + case 3: + return "MultiLineString"; + case 4: + return "Polygon"; + case 5: + return "MultiPolygon"; + case 6: + return "GeometryCollection"; + default: + return "Point"; + } +} + +nlohmann::json readValue(protozero::pbf_reader valueMessage) { + nlohmann::json value = nullptr; + while (valueMessage.next()) { + switch (valueMessage.tag()) { + case 1: + value = valueMessage.get_string(); + break; + case 2: + value = valueMessage.get_double(); + break; + case 3: { + const auto intVal = valueMessage.get_uint64(); + if (intVal <= static_cast(std::numeric_limits::max())) { + value = static_cast(intVal); + } else { + value = static_cast(intVal); + } + break; + } + case 4: { + const auto intVal = valueMessage.get_uint64(); + if (intVal <= static_cast(std::numeric_limits::max())) { + value = -static_cast(intVal); + } else { + value = -static_cast(intVal); + } + break; + } + case 5: + value = valueMessage.get_bool(); + break; + case 6: { + const auto jsonValue = valueMessage.get_string(); + try { + value = nlohmann::json::parse(jsonValue); + } catch (const nlohmann::json::exception &) { + value = jsonValue; + } + break; + } + default: + valueMessage.skip(); + break; + } + } + return value; +} + +std::vector readPackedUInt32(protozero::pbf_reader &message) { + std::vector values; + for (const auto value : message.get_packed_uint32()) { + values.push_back(value); + } + return values; +} + +std::vector readPackedSInt64(protozero::pbf_reader &message) { + std::vector values; + for (const auto value : message.get_packed_sint64()) { + values.push_back(value); + } + return values; +} + +nlohmann::json readProperties(const std::vector &propertyIndexes, DecoderContext &context) { + nlohmann::json properties = nlohmann::json::object(); + for (size_t i = 0; i + 1 < propertyIndexes.size(); i += 2) { + const auto keyIndex = propertyIndexes[i]; + const auto valueIndex = propertyIndexes[i + 1]; + + if (keyIndex >= context.keys.size() || valueIndex >= context.values.size()) { + continue; + } + properties[context.keys[keyIndex]] = context.values[valueIndex]; + } + context.values.clear(); + return properties; +} + +nlohmann::json decodeLinePart(const std::vector &coords, size_t &coordIndex, const std::optional &length, + uint32_t dimensions, double precisionScale, bool closed) { + nlohmann::json line = nlohmann::json::array(); + const auto pointDimensions = std::max(1, dimensions); + const auto maxPointsFromCoords = (coords.size() - coordIndex) / pointDimensions; + const auto pointsToRead = length ? std::min(*length, maxPointsFromCoords) : maxPointsFromCoords; + + std::vector previous(pointDimensions, 0); + for (size_t i = 0; i < pointsToRead && coordIndex + pointDimensions <= coords.size(); i++) { + nlohmann::json point = nlohmann::json::array(); + for (uint32_t d = 0; d < pointDimensions; d++) { + previous[d] += coords[coordIndex++]; + point.push_back(static_cast(previous[d]) / precisionScale); + } + line.push_back(std::move(point)); + } + + if (closed && !line.empty()) { + line.push_back(line[0]); + } + return line; +} + +nlohmann::json decodeCoordinates(int32_t type, const std::vector &lengths, const std::vector &coords, + uint32_t dimensions, double precisionScale) { + const auto pointDimensions = std::max(1, dimensions); + + if (type == 0) { // Point + nlohmann::json point = nlohmann::json::array(); + for (uint32_t d = 0; d < pointDimensions && d < coords.size(); d++) { + point.push_back(static_cast(coords[d]) / precisionScale); + } + return point; + } + + if (type == 1 || type == 2) { // MultiPoint / LineString + size_t coordIndex = 0; + return decodeLinePart(coords, coordIndex, std::nullopt, pointDimensions, precisionScale, false); + } + + if (type == 3 || type == 4) { // MultiLineString / Polygon + nlohmann::json lines = nlohmann::json::array(); + size_t coordIndex = 0; + if (lengths.empty()) { + lines.push_back(decodeLinePart(coords, coordIndex, std::nullopt, pointDimensions, precisionScale, type == 4)); + return lines; + } + for (const auto length : lengths) { + lines.push_back(decodeLinePart(coords, coordIndex, length, pointDimensions, precisionScale, type == 4)); + } + return lines; + } + + if (type == 5) { // MultiPolygon + size_t coordIndex = 0; + nlohmann::json polygons = nlohmann::json::array(); + + if (lengths.empty()) { + nlohmann::json polygon = nlohmann::json::array(); + polygon.push_back(decodeLinePart(coords, coordIndex, std::nullopt, pointDimensions, precisionScale, true)); + polygons.push_back(std::move(polygon)); + return polygons; + } + + size_t lengthIndex = 0; + const auto polygonCount = lengths[lengthIndex++]; + for (uint32_t polygonIt = 0; polygonIt < polygonCount && lengthIndex < lengths.size(); polygonIt++) { + const auto ringCount = lengths[lengthIndex++]; + nlohmann::json rings = nlohmann::json::array(); + for (uint32_t ringIt = 0; ringIt < ringCount && lengthIndex < lengths.size(); ringIt++) { + rings.push_back(decodeLinePart(coords, coordIndex, lengths[lengthIndex++], pointDimensions, precisionScale, true)); + } + polygons.push_back(std::move(rings)); + } + + return polygons; + } + + return nlohmann::json::array(); +} + +nlohmann::json decodeGeometry(protozero::pbf_reader geometryMessage, DecoderContext &context); + +nlohmann::json decodeFeature(protozero::pbf_reader featureMessage, DecoderContext &context) { + nlohmann::json feature = {{"type", "Feature"}}; + bool hasGeometry = false; + while (featureMessage.next()) { + switch (featureMessage.tag()) { + case 1: + feature["geometry"] = decodeGeometry(featureMessage.get_message(), context); + hasGeometry = true; + break; + case 11: + feature["id"] = featureMessage.get_string(); + break; + case 12: + feature["id"] = featureMessage.get_sint64(); + break; + case 13: + context.values.push_back(readValue(featureMessage.get_message())); + break; + case 14: + feature["properties"] = readProperties(readPackedUInt32(featureMessage), context); + break; + case 15: { + auto customProperties = readProperties(readPackedUInt32(featureMessage), context); + for (auto &[key, value] : customProperties.items()) { + feature[key] = value; + } + break; + } + default: + featureMessage.skip(); + break; + } + } + + if (!hasGeometry) { + feature["geometry"] = nullptr; + } + + return feature; +} + +nlohmann::json decodeFeatureCollection(protozero::pbf_reader featureCollectionMessage, DecoderContext &context) { + nlohmann::json featureCollection = { + {"type", "FeatureCollection"}, + {"features", nlohmann::json::array()}, + }; + + while (featureCollectionMessage.next()) { + switch (featureCollectionMessage.tag()) { + case 1: + featureCollection["features"].push_back(decodeFeature(featureCollectionMessage.get_message(), context)); + break; + case 13: + context.values.push_back(readValue(featureCollectionMessage.get_message())); + break; + case 15: { + auto customProperties = readProperties(readPackedUInt32(featureCollectionMessage), context); + for (auto &[key, value] : customProperties.items()) { + featureCollection[key] = value; + } + break; + } + default: + featureCollectionMessage.skip(); + break; + } + } + + return featureCollection; +} + +nlohmann::json decodeGeometry(protozero::pbf_reader geometryMessage, DecoderContext &context) { + int32_t type = 0; + std::vector lengths; + std::vector coords; + nlohmann::json geometries = nlohmann::json::array(); + nlohmann::json geometry = nlohmann::json::object(); + + while (geometryMessage.next()) { + switch (geometryMessage.tag()) { + case 1: + type = geometryMessage.get_enum(); + break; + case 2: + lengths = readPackedUInt32(geometryMessage); + break; + case 3: + coords = readPackedSInt64(geometryMessage); + break; + case 4: + geometries.push_back(decodeGeometry(geometryMessage.get_message(), context)); + break; + case 13: + context.values.push_back(readValue(geometryMessage.get_message())); + break; + case 15: { + auto customProperties = readProperties(readPackedUInt32(geometryMessage), context); + for (auto &[key, value] : customProperties.items()) { + geometry[key] = value; + } + break; + } + default: + geometryMessage.skip(); + break; + } + } + + const auto geometryType = geometryTypeFromEnum(type); + geometry["type"] = geometryType; + if (geometryType == "GeometryCollection") { + geometry["geometries"] = std::move(geometries); + } else { + geometry["coordinates"] = decodeCoordinates(type, lengths, coords, context.dimensions, context.precisionScale); + } + return geometry; +} + +nlohmann::json decodeGeobuf(const ::djinni::DataRef &geobuf) { + DecoderContext context; + nlohmann::json decodedData; + bool hasData = false; + + protozero::pbf_reader data(reinterpret_cast(geobuf.buf()), geobuf.len()); + while (data.next()) { + switch (data.tag()) { + case 1: + context.keys.push_back(data.get_string()); + break; + case 2: + context.dimensions = std::max(1, data.get_uint32()); + break; + case 3: { + const auto precision = data.get_uint32(); + context.precisionScale = std::pow(10.0, static_cast(precision)); + break; + } + case 4: + decodedData = decodeFeatureCollection(data.get_message(), context); + hasData = true; + break; + case 5: + decodedData = decodeFeature(data.get_message(), context); + hasData = true; + break; + case 6: + decodedData = decodeGeometry(data.get_message(), context); + hasData = true; + break; + default: + data.skip(); + break; + } + } + + if (!hasData) { + throw std::runtime_error("geobuf has no decodable data payload"); + } + + return decodedData; +} + +} // namespace + +std::shared_ptr GeobufParser::getGeoJson(const ::djinni::DataRef &geobuf, StringInterner &stringTable) { + if (!geobuf.buf() || geobuf.len() == 0) { + throw std::runtime_error("geobuf payload is empty"); + } + + try { + return GeoJsonParser::getGeoJson(decodeGeobuf(geobuf), stringTable); + } catch (const protozero::exception &ex) { + throw std::runtime_error(std::string("invalid geobuf payload: ") + ex.what()); + } +} diff --git a/shared/src/map/layers/tiled/vector/geojson/GeobufParser.h b/shared/src/map/layers/tiled/vector/geojson/GeobufParser.h new file mode 100644 index 000000000..b5a9d43ca --- /dev/null +++ b/shared/src/map/layers/tiled/vector/geojson/GeobufParser.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#pragma once + +#include "DataRef.hpp" +#include "GeoJsonTypes.h" +#include "StringInterner.h" + +class GeobufParser { + public: + /** + * Decode Geobuf payload into GeoJSON model. + * + * @throws std::runtime_error on malformed Geobuf payload + * @throws nlohmann::json::exception on malformed decoded GeoJSON + * @returns not-null + */ + static std::shared_ptr getGeoJson(const ::djinni::DataRef &geobuf, StringInterner &stringTable); +}; diff --git a/shared/src/map/layers/tiled/vector/geojson/geojsonvt/geojsonvt.hpp b/shared/src/map/layers/tiled/vector/geojson/geojsonvt/geojsonvt.hpp index 0b51571e8..4802f5b82 100644 --- a/shared/src/map/layers/tiled/vector/geojson/geojsonvt/geojsonvt.hpp +++ b/shared/src/map/layers/tiled/vector/geojson/geojsonvt/geojsonvt.hpp @@ -6,6 +6,7 @@ #include "GeoJsonTypes.h" #include "GeoJsonParser.h" +#include "GeobufParser.h" #include "LoaderInterface.h" #include "LoaderHelper.h" @@ -39,6 +40,11 @@ struct Options : TileOptions { uint32_t indexMaxPoints = 100000; }; +enum class GeoDataFormat { + GeoJson, + Geobuf +}; + inline uint64_t toID(uint8_t z, uint32_t x, uint32_t y) { return (((1ull << z) * y + x) * 32) + z; } @@ -57,6 +63,7 @@ class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this< const Options &options_ = Options()) : options(options_) , loadingResult(DataLoaderResult(std::nullopt, std::nullopt, LoaderStatus::OK, std::nullopt)) + , dataFormat(GeoDataFormat::GeoJson) , stringTable(stringTable) { initialize(geoJson); } @@ -65,16 +72,19 @@ class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this< const std::vector> &loaders, const std::shared_ptr &localDataProvider, const std::shared_ptr &stringTable, + GeoDataFormat dataFormat, const Options &options_ = Options()) : options(options_) , sourceName(sourceName) , geoJsonUrl(geoJsonUrl) , loaders(loaders) , localDataProvider(localDataProvider) + , dataFormat(dataFormat) , stringTable(stringTable) {} const std::string sourceName; const std::string geoJsonUrl; + const GeoDataFormat dataFormat; std::vector> loaders; std::shared_ptr localDataProvider; std::recursive_mutex mutex; @@ -113,28 +123,39 @@ class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this< self->delegate.message(MFN(&GeoJSONTileDelegate::failedToLoad)); } } else { - auto string = std::string((char*)result.data->buf(), result.data->len()); - nlohmann::json json; - try { - json = nlohmann::json::parse(string, nullptr, true, true); - auto geoJson = GeoJsonParser::getGeoJson(json, *strongStringTable); - if (geoJson) { - self->initialize(geoJson); + std::shared_ptr geoJson; + if (self->dataFormat == GeoDataFormat::GeoJson) { + auto string = std::string((char *)result.data->buf(), result.data->len()); + try { + auto json = nlohmann::json::parse(string, nullptr, true, true); + geoJson = GeoJsonParser::getGeoJson(json, *strongStringTable); + } catch (nlohmann::json::exception &ex) { std::lock_guard lock(self->mutex); - self->loadingResult = DataLoaderResult(std::nullopt, std::nullopt, result.status, result.errorCode); - - if (self->delegate) { - // avoid call to null-delegate - // setting the delegate will call didLoad, result won't be lost - self->delegate.message(MFN(&GeoJSONTileDelegate::didLoad), self->options.maxZoom); - } + self->loadingResult = DataLoaderResult(std::nullopt, std::nullopt, LoaderStatus::ERROR_OTHER, "parse error"); + LogError << "Unable to parse geoJson: " <<= ex.what(); + return; + } + } else { + try { + geoJson = GeobufParser::getGeoJson(*result.data, *strongStringTable); + } catch (std::exception &ex) { + std::lock_guard lock(self->mutex); + self->loadingResult = DataLoaderResult(std::nullopt, std::nullopt, LoaderStatus::ERROR_OTHER, "parse error"); + LogError << "Unable to parse geobuf: " <<= ex.what(); + return; } } - catch (nlohmann::json::parse_error &ex) { + + if (geoJson) { + self->initialize(geoJson); std::lock_guard lock(self->mutex); - self->loadingResult = DataLoaderResult(std::nullopt, std::nullopt, LoaderStatus::ERROR_OTHER, "parse error"); - LogError <<= "Unable to parse geoJson"; - return; + self->loadingResult = DataLoaderResult(std::nullopt, std::nullopt, result.status, result.errorCode); + + if (self->delegate) { + // avoid call to null-delegate + // setting the delegate will call didLoad, result won't be lost + self->delegate.message(MFN(&GeoJSONTileDelegate::didLoad), self->options.maxZoom); + } } } self->loaders.clear(); diff --git a/shared/test/CMakeLists.txt b/shared/test/CMakeLists.txt index 0b2665639..4bb75ba09 100644 --- a/shared/test/CMakeLists.txt +++ b/shared/test/CMakeLists.txt @@ -9,6 +9,7 @@ FetchContent_MakeAvailable(Catch2) add_executable(tests "TestActor.cpp" + "TestGeobufParser.cpp" "TestGeoJsonParser.cpp" "TestSymbolAnimationCoordinatorMap.cpp" "TestTileSource.cpp" diff --git a/shared/test/TestGeobufParser.cpp b/shared/test/TestGeobufParser.cpp new file mode 100644 index 000000000..c80ddcdb3 --- /dev/null +++ b/shared/test/TestGeobufParser.cpp @@ -0,0 +1,51 @@ +#include "GeobufParser.h" +#include "helper/TestData.h" + +#include +#include + +TEST_CASE("Geobuf Parser FeatureCollection") { + auto geobuf = TestData::readFileToBuffer("geobuf/featurecollection_point_line.pbf"); + StringInterner stringTable = ValueKeys::newStringInterner(); + + auto geoJson = GeobufParser::getGeoJson(::djinni::DataRef(geobuf.data(), geobuf.size()), stringTable); + REQUIRE(geoJson != nullptr); + REQUIRE(geoJson->geometries.size() == 2); + CHECK(geoJson->hasOnlyPoints == false); + + const auto &point = geoJson->geometries[0]; + CHECK(point->featureContext->geomType == vtzero::GeomType::POINT); + REQUIRE(point->coordinates.size() == 1); + REQUIRE(point->coordinates[0].size() == 1); + CHECK(point->coordinates[0][0].x == Catch::Approx(8.0)); + CHECK(point->coordinates[0][0].y == Catch::Approx(47.0)); + + const auto pointInfo = point->featureContext->getFeatureInfo(stringTable); + CHECK(pointInfo.properties.at("name").stringVal == "point"); + + const auto &line = geoJson->geometries[1]; + CHECK(line->featureContext->geomType == vtzero::GeomType::LINESTRING); + REQUIRE(line->coordinates.size() == 1); + REQUIRE(line->coordinates[0].size() == 3); + CHECK(line->coordinates[0][2].x == Catch::Approx(8.2)); + CHECK(line->coordinates[0][2].y == Catch::Approx(47.2)); +} + +TEST_CASE("Geobuf Parser Geometry") { + auto geobuf = TestData::readFileToBuffer("geobuf/line_geometry.pbf"); + StringInterner stringTable = ValueKeys::newStringInterner(); + + auto geoJson = GeobufParser::getGeoJson(::djinni::DataRef(geobuf.data(), geobuf.size()), stringTable); + REQUIRE(geoJson != nullptr); + REQUIRE(geoJson->geometries.size() == 1); + CHECK(geoJson->hasOnlyPoints == false); + + const auto &line = geoJson->geometries[0]; + CHECK(line->featureContext->geomType == vtzero::GeomType::LINESTRING); + REQUIRE(line->coordinates.size() == 1); + REQUIRE(line->coordinates[0].size() == 3); + CHECK(line->coordinates[0][0].x == Catch::Approx(8.0)); + CHECK(line->coordinates[0][0].y == Catch::Approx(47.0)); + CHECK(line->coordinates[0][2].x == Catch::Approx(8.2)); + CHECK(line->coordinates[0][2].y == Catch::Approx(47.2)); +} diff --git a/shared/test/TestStyleParser.cpp b/shared/test/TestStyleParser.cpp index cd9839718..1814e37cb 100644 --- a/shared/test/TestStyleParser.cpp +++ b/shared/test/TestStyleParser.cpp @@ -21,6 +21,36 @@ class TestGeoJSONTileDelegate : public GeoJSONTileDelegate, public ActorObject { void failedToLoad() override { failedToLoadCalled = true; } }; +class TestGeobufLocalDataProvider : public Tiled2dMapVectorLayerLocalDataProviderInterface { + public: + explicit TestGeobufLocalDataProvider(std::vector geobufData) + : geobufData(std::move(geobufData)) {} + + djinni::Future loadGeojson(const std::string &sourceName, const std::string &url) override { + auto promise = ::djinni::Promise(); + promise.setValue( + DataLoaderResult(::djinni::DataRef(geobufData.data(), geobufData.size()), std::nullopt, LoaderStatus::OK, std::nullopt)); + return promise.getFuture(); + } + + std::optional getStyleJson() override { return std::nullopt; } + + djinni::Future loadSpriteAsync(const std::string &spriteId, const std::string &url, int32_t scale) override { + auto promise = ::djinni::Promise(); + promise.setValue(TextureLoaderResult(nullptr, std::nullopt, LoaderStatus::ERROR_404, std::nullopt)); + return promise.getFuture(); + } + + djinni::Future loadSpriteJsonAsync(const std::string &spriteId, const std::string &url, int32_t scale) override { + auto promise = ::djinni::Promise(); + promise.setValue(DataLoaderResult(std::nullopt, std::nullopt, LoaderStatus::ERROR_404, std::nullopt)); + return promise.getFuture(); + } + + private: + std::vector geobufData; +}; + TEST_CASE("TestStyleParser", "[GeoJson inline]") { auto jsonString = TestData::readFileToString("style/geojson_style_inline.json"); std::shared_ptr stringTable = std::make_shared(ValueKeys::newStringInterner()); @@ -73,6 +103,66 @@ TEST_CASE("TestStyleParser", "[GeoJson local provider]") { REQUIRE(!tile.getFeatures().empty()); } +TEST_CASE("TestStyleParser", "[Geobuf local provider]") { + auto jsonString = R"GEOBUF_STYLE({ + "version": 8, + "name": "s", + "sources": { + "wsource": { + "type": "geobuf", + "minzoom": 0, + "maxzoom": 25, + "data": "localdata-url" + } + }, + "sprite": "localdata-sprites", + "layers": [ + { + "id": "w", + "type": "fill", + "metadata": { + "blend-mode": "multiply" + }, + "minzoom": 0, + "maxzoom": 6, + "source": "wsource", + "paint": { + "fill-color": "rgb(202, 154, 255)" + } + } + ] +})GEOBUF_STYLE"; + auto provider = std::make_shared(TestData::readFileToBuffer("geobuf/featurecollection_point_line.pbf")); + std::shared_ptr stringTable = std::make_shared(ValueKeys::newStringInterner()); + auto result = Tiled2dMapVectorLayerParserHelper::parseStyleJsonFromString("test", jsonString, provider, {}, stringTable, {}); + REQUIRE(result.mapDescription != nullptr); + REQUIRE(!result.mapDescription->geoJsonSources.empty()); + + std::shared_ptr geobufSource = result.mapDescription->geoJsonSources.begin()->second; + + auto scheduler = std::make_shared(); + auto delegate = + Actor(std::make_shared(scheduler), std::make_shared()); + + geobufSource->setDelegate(delegate.weakActor()); + + REQUIRE(!delegate.unsafe()->didLoadCalled); + + auto promise = std::make_shared<::djinni::Promise>>(); + geobufSource->waitIfNotLoaded(promise); + promise->getFuture().wait(); + + scheduler->drain(); + + REQUIRE(delegate.unsafe()->didLoadCalled); + + REQUIRE(geobufSource->getMinZoom() == 0); + REQUIRE(geobufSource->getMaxZoom() == 25); + + const auto &tile = geobufSource->getTile(6, 33, 22); + REQUIRE(!tile.getFeatures().empty()); +} + static bool equalSpriteSource(const SpriteSourceDescription &a, const SpriteSourceDescription &b) { return a.identifier == b.identifier && a.baseUrl == b.baseUrl; }; diff --git a/shared/test/data/geobuf/featurecollection_point_line.pbf b/shared/test/data/geobuf/featurecollection_point_line.pbf new file mode 100644 index 000000000..3df9606c3 Binary files /dev/null and b/shared/test/data/geobuf/featurecollection_point_line.pbf differ diff --git a/shared/test/data/geobuf/line_geometry.pbf b/shared/test/data/geobuf/line_geometry.pbf new file mode 100644 index 000000000..d9e1ec80b --- /dev/null +++ b/shared/test/data/geobuf/line_geometry.pbf @@ -0,0 +1 @@ +2€ÈЀ§é,Àš Àš Àš Àš \ No newline at end of file