diff --git a/jvm/src/test/resources/styles/multisprite/style.json b/jvm/src/test/resources/styles/multisprite/style.json index 96dfa4b14..c49b6720b 100644 --- a/jvm/src/test/resources/styles/multisprite/style.json +++ b/jvm/src/test/resources/styles/multisprite/style.json @@ -7,8 +7,6 @@ "name": "test-sprites", "sources": { "test_label": { - "minzoom": 0, - "maxzoom": 0, "type": "geojson", "data": { "type": "FeatureCollection", diff --git a/jvm/src/test/resources/styles/style_geojson_ch_label.json b/jvm/src/test/resources/styles/style_geojson_ch_label.json index bf312a5d8..0fa0d761b 100644 --- a/jvm/src/test/resources/styles/style_geojson_ch_label.json +++ b/jvm/src/test/resources/styles/style_geojson_ch_label.json @@ -13,8 +13,6 @@ "sources": { "test_label": { - "minzoom": 0, - "maxzoom": 0, "type": "geojson", "data": { "type": "FeatureCollection", diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp index ae0d45e59..d272588bb 100644 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp +++ b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp @@ -111,7 +111,7 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ minZoom = val.value("minzoom", 0); maxZoom = val.value("maxzoom", 22); } - + std::optional<::RectCoord> bounds; std::optional coordinateReferenceSystem; @@ -214,7 +214,7 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ tileJsons[key] = val; } else if (type == "geojson") { nlohmann::json geojson; - Options options; + GeoJSONVT::Options options; if (val["minzoom"].is_number_integer()) { options.minZoom = val["minzoom"].get(); @@ -302,7 +302,7 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ metadata = json["metadata"].dump(); globalIsInteractable = parser.parseValue(json["metadata"]["interactable"]); persistingSymbolPlacement = json["metadata"].value("persistingSymbolPlacement", false); - + // XXX: make this a per sprite option? if (json["metadata"].contains("use3xSprites")) { use3xSprites = json["metadata"]["use3xSprites"].get(); diff --git a/shared/src/map/layers/tiled/vector/geojson/GeoJsonVTFactory.h b/shared/src/map/layers/tiled/vector/geojson/GeoJsonVTFactory.h index 9d1a9b609..37cd1d91f 100644 --- a/shared/src/map/layers/tiled/vector/geojson/GeoJsonVTFactory.h +++ b/shared/src/map/layers/tiled/vector/geojson/GeoJsonVTFactory.h @@ -17,7 +17,7 @@ class GeoJsonVTFactory { public: static std::shared_ptr getGeoJsonVt(const std::shared_ptr &geoJson, const std::shared_ptr &stringTable, - const Options& options = Options()) { + const GeoJSONVT::Options& options = GeoJSONVT::Options()) { return std::static_pointer_cast(std::make_shared(geoJson, stringTable, options)); } @@ -25,7 +25,7 @@ class GeoJsonVTFactory { const std::string &geoJsonUrl, const std::vector> &loaders, const std::shared_ptr &localDataProvider, const std::shared_ptr &stringTable, - const Options& options = Options()) { + const GeoJSONVT::Options& options = GeoJSONVT::Options()) { std::shared_ptr vt = std::make_shared(sourceName, geoJsonUrl, loaders, localDataProvider, stringTable, options); vt->load(); return vt; 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 e38b4aa02..b5c2627bf 100644 --- a/shared/src/map/layers/tiled/vector/geojson/geojsonvt/geojsonvt.hpp +++ b/shared/src/map/layers/tiled/vector/geojson/geojsonvt/geojsonvt.hpp @@ -14,48 +14,59 @@ #include #include -struct TileOptions { - // simplification tolerance (higher means simpler) - double tolerance = 1.0; +class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this { +private: + struct TileOptions { + // simplification tolerance (higher means simpler) + double tolerance = 1.0; - // tile extent - uint32_t extent = 4096; + // tile extent + uint32_t extent = 4096; - // tile buffer on each side - uint32_t buffer = 64; -}; + // tile buffer on each side + uint32_t buffer = 64; -struct Options : TileOptions { - // min zoom to will be visible - uint8_t minZoom = 0; + // min zoom to will be visible + uint8_t minZoom = 0; - // max zoom to preserve detail on - uint8_t maxZoom = 18; + // max zoom to preserve detail on + uint8_t maxZoom = 18; - // max zoom in the tile index - uint8_t indexMaxZoom = 5; + // max zoom in the tile index + uint8_t indexMaxZoom = 5; - // max number of points per tile in the tile index - uint32_t indexMaxPoints = 100000; -}; + // max number of points per tile in the tile index + uint32_t indexMaxPoints = 100000; + }; -inline uint64_t toID(uint8_t z, uint32_t x, uint32_t y) { - return (((1ull << z) * y + x) * 32) + z; -} + inline uint64_t toID(uint8_t z, uint32_t x, uint32_t y) { + return (((1ull << z) * y + x) * 32) + z; + } + + +public: + struct Options { + std::optional minZoom; + std::optional maxZoom; + std::optional extent; + }; -class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this { private: std::weak_ptr stringTable; -public: - Options options; + // effective parameter values for tile generation + TileOptions options; + + // parameters as configured in style + Options configuredOptions; const Tile emptyTile = Tile(); +public: GeoJSONVT(const std::shared_ptr &geoJson, const std::shared_ptr &stringTable, - const Options &options_ = Options()) - : options(options_) + Options _config = Options()) + : configuredOptions(_config) , loadingResult(DataLoaderResult(std::nullopt, std::nullopt, LoaderStatus::OK, std::nullopt)) , stringTable(stringTable) { initialize(geoJson); @@ -65,8 +76,8 @@ class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this< const std::vector> &loaders, const std::shared_ptr &localDataProvider, const std::shared_ptr &stringTable, - const Options &options_ = Options()) - : options(options_) + Options configurationOptions = Options()) + : configuredOptions(configurationOptions) , sourceName(sourceName) , geoJsonUrl(geoJsonUrl) , loaders(loaders) @@ -144,14 +155,7 @@ class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this< } void initialize(const std::shared_ptr &geoJson) { - // If the GeoJSON contains only points, there is no need to split it into smaller tiles, - // as there are no opportunities for simplification, merging, or meaningful point reduction. - // Keep point-only sources on a single zoom level only if minZoom is explicitly configured (> 0). - // For the default minZoom=0 case, collapsing to z0 creates very large matching tolerances and can - // hide unrelated points due to symbol ownership deduplication. - if (geoJson->hasOnlyPoints && options.minZoom > 0) { - options.maxZoom = options.minZoom; - } + options = getEffectiveOptions(configuredOptions, geoJson->hasOnlyPoints); const uint32_t z2 = 1u << options.maxZoom; @@ -186,10 +190,7 @@ class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this< } void reload(const std::shared_ptr &geoJson) override { - // Keep behavior consistent with initialize(). - if (geoJson->hasOnlyPoints && options.minZoom > 0) { - options.maxZoom = options.minZoom; - } + options = getEffectiveOptions(configuredOptions, geoJson->hasOnlyPoints); const uint32_t z2 = 1u << options.maxZoom; @@ -357,4 +358,36 @@ class GeoJSONVT: public GeoJSONVTInterface, public std::enable_shared_from_this< } waitingPromises.clear(); } + + static TileOptions getEffectiveOptions(const Options &config, bool hasOnlyPoints) { + TileOptions options; // initialize with defaults. + if (config.extent.has_value()) { + options.extent = *config.extent; + } + // If the GeoJSON contains only points, there is no need to split it into smaller tiles, + // as there are no opportunities for simplification, merging, or meaningful point reduction. + // Keep point-only sources on a single zoom level, unless minZoom and + // maxZoom are explicitly configured. + if (hasOnlyPoints && !(config.minZoom.has_value() && config.maxZoom.has_value())) { + if(config.minZoom.has_value()) { + options.minZoom = *config.minZoom; + options.maxZoom = *config.minZoom; + } else if (config.maxZoom.has_value()) { + options.minZoom = *config.maxZoom; + options.maxZoom = *config.maxZoom; + } else { + options.minZoom = 0; + options.maxZoom = 0; + } + } else { + if(config.minZoom.has_value()) { + options.minZoom = *config.minZoom; + } + if(config.maxZoom.has_value()) { + options.maxZoom = *config.maxZoom; + } + } + return options; + } + }; diff --git a/shared/test/TestStyleParser.cpp b/shared/test/TestStyleParser.cpp index cd9839718..9b9308d1b 100644 --- a/shared/test/TestStyleParser.cpp +++ b/shared/test/TestStyleParser.cpp @@ -21,24 +21,48 @@ class TestGeoJSONTileDelegate : public GeoJSONTileDelegate, public ActorObject { void failedToLoad() override { failedToLoadCalled = true; } }; -TEST_CASE("TestStyleParser", "[GeoJson inline]") { - auto jsonString = TestData::readFileToString("style/geojson_style_inline.json"); +static void testGeojsonInlineParse(const char* filename, int expectedMinZoom, int expectedMaxZoom) { + auto jsonString = TestData::readFileToString(filename); std::shared_ptr stringTable = std::make_shared(ValueKeys::newStringInterner()); auto result = Tiled2dMapVectorLayerParserHelper::parseStyleJsonFromString("test", jsonString, nullptr, {}, stringTable, {}); REQUIRE(result.mapDescription != nullptr); REQUIRE(!result.mapDescription->geoJsonSources.empty()); std::shared_ptr geojsonSource = result.mapDescription->geoJsonSources.begin()->second; - REQUIRE(geojsonSource->getMinZoom() == 0); - REQUIRE(geojsonSource->getMaxZoom() == 0); + REQUIRE(geojsonSource->getMinZoom() == expectedMinZoom); + REQUIRE(geojsonSource->getMaxZoom() == expectedMaxZoom); + REQUIRE(result.mapDescription->layers[0]->sourceMinZoom == expectedMinZoom); + REQUIRE(result.mapDescription->layers[0]->sourceMaxZoom == expectedMaxZoom); - REQUIRE(result.mapDescription->layers[0]->sourceMinZoom == 0); - REQUIRE(result.mapDescription->layers[0]->sourceMaxZoom == 0); REQUIRE_NOTHROW(geojsonSource->getTile(0, 0, 0)); - REQUIRE_THROWS(geojsonSource->getTile(6, 33, 22)); + if(expectedMaxZoom >= 6) { + REQUIRE_NOTHROW(geojsonSource->getTile(6, 33, 22)); + } else { + REQUIRE_THROWS(geojsonSource->getTile(6, 33, 22)); + } +} + +TEST_CASE("GeoJson inline points min and maxzoom", "[TestStyleParser]") { + testGeojsonInlineParse("style/geojson_style_points_minmaxzoom.json", 0, 25); } -TEST_CASE("TestStyleParser", "[GeoJson local provider]") { +TEST_CASE("GeoJson inline points no min/maxzoom", "[TestStyleParser]") { + testGeojsonInlineParse("style/geojson_style_points_nominmaxzoom.json", 0, 0); +}; + +TEST_CASE("GeoJson inline points minzoom", "[TestStyleParser]") { + testGeojsonInlineParse("style/geojson_style_points_minzoom.json", 5, 5); +}; + +TEST_CASE("GeoJson inline points maxzoom", "[TestStyleParser]") { + testGeojsonInlineParse("style/geojson_style_points_maxzoom.json", 25, 25); +}; + +TEST_CASE("GeoJson inline mixed no min/maxzoom", "[TestStyleParser]") { + testGeojsonInlineParse("style/geojson_style_mixed_nominmaxzoom.json", 0, 18); +}; + +TEST_CASE("GeoJson local provider", "[TestStyleParser]") { auto jsonString = TestData::readFileToString("style/geojson_style_provider.json"); auto provider = std::make_shared(std::unordered_map{{"wsource", "geojson.geojson"}}); diff --git a/shared/test/data/style/geojson_style_mixed_nominmaxzoom.json b/shared/test/data/style/geojson_style_mixed_nominmaxzoom.json new file mode 100644 index 000000000..8d681cc88 --- /dev/null +++ b/shared/test/data/style/geojson_style_mixed_nominmaxzoom.json @@ -0,0 +1,51 @@ +{ + "version": 8, + "name": "s", + "sources": { + "wsource": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": + [8.5, 46.8] + + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [102.0, 0.0], + [103.0, 1.0], + [104.0, 0.0], + [105.0, 1.0] + ] + } + } + ] + } + } + }, + "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)" + } + } + ] +} diff --git a/shared/test/data/style/geojson_style_points_maxzoom.json b/shared/test/data/style/geojson_style_points_maxzoom.json new file mode 100644 index 000000000..aeabdc82f --- /dev/null +++ b/shared/test/data/style/geojson_style_points_maxzoom.json @@ -0,0 +1,30 @@ +{ + "version": 8, + "name": "s", + "sources": { + "wsource": { + "type": "geojson", + "maxzoom": 25, + "data": { + "type": "Point", + "coordinates": [102.0, 0.5] + } + } + }, + "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)" + } + } + ] +} diff --git a/shared/test/data/style/geojson_style_inline.json b/shared/test/data/style/geojson_style_points_minmaxzoom.json similarity index 100% rename from shared/test/data/style/geojson_style_inline.json rename to shared/test/data/style/geojson_style_points_minmaxzoom.json diff --git a/shared/test/data/style/geojson_style_points_minzoom.json b/shared/test/data/style/geojson_style_points_minzoom.json new file mode 100644 index 000000000..f3b9d7971 --- /dev/null +++ b/shared/test/data/style/geojson_style_points_minzoom.json @@ -0,0 +1,30 @@ +{ + "version": 8, + "name": "s", + "sources": { + "wsource": { + "type": "geojson", + "minzoom": 5, + "data": { + "type": "Point", + "coordinates": [102.0, 0.5] + } + } + }, + "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)" + } + } + ] +} diff --git a/shared/test/data/style/geojson_style_points_nominmaxzoom.json b/shared/test/data/style/geojson_style_points_nominmaxzoom.json new file mode 100644 index 000000000..f8810b1f1 --- /dev/null +++ b/shared/test/data/style/geojson_style_points_nominmaxzoom.json @@ -0,0 +1,29 @@ +{ + "version": 8, + "name": "s", + "sources": { + "wsource": { + "type": "geojson", + "data": { + "type": "Point", + "coordinates": [102.0, 0.5] + } + } + }, + "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)" + } + } + ] +}