From b7d07e6bb89a6070fba974953c5688b32b924c7d Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Sun, 23 Aug 2026 15:41:25 +0900 Subject: [PATCH 01/32] Add support for the global-state expression and state style property Implements the maplibre-gl-js global-state feature (maplibre/maplibre-native#3302) --- CHANGELOG.md | 1 + include/mln/renderer/query.hpp | 10 + .../mln/style/color_ramp_property_value.hpp | 11 +- include/mln/style/expression/expression.hpp | 13 +- .../expression/format_section_override.hpp | 12 +- include/mln/style/layer.hpp | 5 + include/mln/style/layer_properties.hpp | 5 + include/mln/style/property_expression.hpp | 40 ++- include/mln/style/style.hpp | 7 + include/mln/util/feature.hpp | 1 + scripts/style-spec-reference/v8.json | 156 ++++++++++- src/mln/geometry/feature_index.cpp | 6 +- src/mln/layout/circle_layout.hpp | 7 +- src/mln/layout/pattern_layout.hpp | 7 +- src/mln/layout/symbol_layout.cpp | 25 +- src/mln/map/map_impl.cpp | 1 + src/mln/renderer/bucket_parameters.hpp | 5 + src/mln/renderer/buckets/symbol_bucket.hpp | 10 +- .../cross_faded_property_evaluator.cpp | 8 +- .../data_driven_property_evaluator.hpp | 25 +- .../layers/render_color_relief_layer.cpp | 11 +- .../layers/render_color_relief_layer.hpp | 3 + .../renderer/layers/render_heatmap_layer.cpp | 9 +- .../renderer/layers/render_heatmap_layer.hpp | 3 + src/mln/renderer/layers/render_line_layer.cpp | 10 +- src/mln/renderer/layers/render_line_layer.hpp | 3 + .../property_evaluation_parameters.hpp | 7 + src/mln/renderer/property_evaluator.hpp | 5 +- src/mln/renderer/render_layer.cpp | 6 +- src/mln/renderer/render_layer.hpp | 4 +- src/mln/renderer/render_orchestrator.cpp | 44 +++- src/mln/renderer/render_orchestrator.hpp | 3 + src/mln/renderer/tile_parameters.hpp | 2 + src/mln/renderer/tile_pyramid.cpp | 2 + src/mln/renderer/update_parameters.hpp | 4 + .../style/expression/compound_expression.cpp | 25 ++ src/mln/style/expression/expression.cpp | 2 + src/mln/style/expression/is_constant.cpp | 9 + src/mln/style/expression/parsing_context.cpp | 4 +- src/mln/style/layer.cpp | 66 ++++- src/mln/style/layer_impl.hpp | 17 ++ .../layers/background_layer_properties.cpp | 4 + .../layers/background_layer_properties.hpp | 2 + src/mln/style/layers/circle_layer_impl.hpp | 4 + .../style/layers/circle_layer_properties.cpp | 4 + .../style/layers/circle_layer_properties.hpp | 2 + .../layers/color_relief_layer_properties.cpp | 4 + .../layers/color_relief_layer_properties.hpp | 2 + .../layers/fill_extrusion_layer_impl.hpp | 4 + .../fill_extrusion_layer_properties.cpp | 4 + .../fill_extrusion_layer_properties.hpp | 2 + src/mln/style/layers/fill_layer_impl.hpp | 4 + .../style/layers/fill_layer_properties.cpp | 4 + .../style/layers/fill_layer_properties.hpp | 2 + .../style/layers/heatmap_layer_properties.cpp | 4 + .../style/layers/heatmap_layer_properties.hpp | 2 + .../layers/hillshade_layer_properties.cpp | 4 + .../layers/hillshade_layer_properties.hpp | 2 + src/mln/style/layers/layer_properties.cpp.ejs | 4 + src/mln/style/layers/layer_properties.hpp.ejs | 2 + src/mln/style/layers/line_layer_impl.hpp | 4 + .../style/layers/line_layer_properties.cpp | 4 + .../style/layers/line_layer_properties.hpp | 2 + .../layers/location_indicator_layer_impl.hpp | 4 + .../location_indicator_layer_properties.cpp | 4 + .../location_indicator_layer_properties.hpp | 2 + .../style/layers/raster_layer_properties.cpp | 4 + .../style/layers/raster_layer_properties.hpp | 2 + src/mln/style/layers/symbol_layer_impl.hpp | 4 + .../style/layers/symbol_layer_properties.cpp | 4 + .../style/layers/symbol_layer_properties.hpp | 2 + src/mln/style/parser.cpp | 64 +++++ src/mln/style/parser.hpp | 6 + src/mln/style/properties.hpp | 4 +- src/mln/style/property_expression.cpp | 6 +- src/mln/style/style.cpp | 9 + src/mln/style/style_impl.cpp | 87 ++++++ src/mln/style/style_impl.hpp | 17 ++ src/mln/tile/custom_geometry_tile.cpp | 6 +- src/mln/tile/geojson_tile.cpp | 6 +- src/mln/tile/geometry_tile.cpp | 18 +- src/mln/tile/geometry_tile.hpp | 4 + src/mln/tile/geometry_tile_worker.cpp | 12 +- src/mln/tile/geometry_tile_worker.hpp | 4 + src/mln/tile/tile.hpp | 5 + test/CMakeLists.txt | 1 + .../style_parser/expressions.info.json | 4 +- test/style/expression/global_state.test.cpp | 247 ++++++++++++++++++ test/style/style.test.cpp | 137 ++++++++++ 89 files changed, 1248 insertions(+), 79 deletions(-) create mode 100644 test/style/expression/global_state.test.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index e83f6b6e6425..7264f6a0e3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### ✨ New features - *...Add new stuff here...* +- [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState`. The expression is supported in filters, layout and paint properties (including color ramps such as `heatmap-color` and `line-gradient`), the `visibility` layout property, and feature query filters ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [core] Locally rasterized CJK glyphs now render at 2x texture resolution, preserving fine strokes. Mirrors [maplibre-gl-js#3006](https://github.com/maplibre/maplibre-gl-js/pull/3006). - [core] Added new map observer events: onPreCompileShader, onPostCompileShader, onShaderCompileFailed, onGlyphsLoaded, onGlyphsError, onGlyphsRequested, onTileAction, onSpriteLoaded, onSpriteError, onSpriteRequested ([#2694](https://github.com/maplibre/maplibre-native/pull/2694)). - [core] Add WebP image decoding support to default platform (Linux, Windows) diff --git a/include/mln/renderer/query.hpp b/include/mln/renderer/query.hpp index fd36b27f972b..f5632067f2a2 100644 --- a/include/mln/renderer/query.hpp +++ b/include/mln/renderer/query.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include +#include #include #include #include @@ -22,6 +24,10 @@ class RenderedQueryOptions { std::optional> layerIDs; std::optional filter; + + /// The style's current global state, injected by the renderer so that + /// `global-state` expressions in the filter can be evaluated. + std::shared_ptr globalState; }; /** @@ -38,6 +44,10 @@ class SourceQueryOptions { std::optional> sourceLayers; std::optional filter; + + /// The style's current global state, injected by the renderer so that + /// `global-state` expressions in the filter can be evaluated. + std::shared_ptr globalState; }; } // namespace mln diff --git a/include/mln/style/color_ramp_property_value.hpp b/include/mln/style/color_ramp_property_value.hpp index be0b8e9de094..71a005d8a7ba 100644 --- a/include/mln/style/color_ramp_property_value.hpp +++ b/include/mln/style/color_ramp_property_value.hpp @@ -36,9 +36,14 @@ class ColorRampPropertyValue { return {}; } - Color evaluate(double rampEvaluationParameter) const { - const auto result = value->evaluate(expression::EvaluationContext({}, nullptr, {rampEvaluationParameter})); - return *expression::fromExpressionValue(*result); + Color evaluate(double rampEvaluationParameter, const GlobalStateMap* globalState = nullptr) const { + const auto result = value->evaluate( + expression::EvaluationContext({}, nullptr, {rampEvaluationParameter}).withGlobalState(globalState)); + if (!result) { + return {}; + } + const auto color = expression::fromExpressionValue(*result); + return color ? *color : Color{}; } bool isDataDriven() const noexcept { return false; } diff --git a/include/mln/style/expression/expression.hpp b/include/mln/style/expression/expression.hpp index bfe41bafba94..181b6e0148fa 100644 --- a/include/mln/style/expression/expression.hpp +++ b/include/mln/style/expression/expression.hpp @@ -85,6 +85,11 @@ class EvaluationContext { return *this; }; + EvaluationContext& withGlobalState(const GlobalStateMap* globalState_) noexcept { + globalState = globalState_; + return *this; + }; + std::optional zoom; std::optional accumulated; GeometryTileFeature const* feature = nullptr; @@ -95,6 +100,7 @@ class EvaluationContext { const FeatureState* featureState = nullptr; const std::set* availableImages = nullptr; const mln::CanonicalTileID* canonical = nullptr; + const GlobalStateMap* globalState = nullptr; }; template @@ -219,9 +225,10 @@ enum class Dependency : uint32_t { Location = 1 << 3, // Not used yet, "distance-from-center" not supported Bind = 1 << 4, // Create variable binding ("let") Var = 1 << 5, // Use variable binding - Override = 1 << 6, // Property override - Elevation = 1 << 7, // Elevation from DEM - MaskCount = 8, + Override = 1 << 6, // Property override + Elevation = 1 << 7, // Elevation from DEM + GlobalState = 1 << 8, // Global state ("global-state") + MaskCount = 9, All = (1 << MaskCount) - 1, }; diff --git a/include/mln/style/expression/format_section_override.hpp b/include/mln/style/expression/format_section_override.hpp index 6d3a30b5a56c..b4a7fbcca683 100644 --- a/include/mln/style/expression/format_section_override.hpp +++ b/include/mln/style/expression/format_section_override.hpp @@ -27,7 +27,17 @@ class FormatSectionOverride final : public Expression { } return defaultValue.match( - [&context](const style::PropertyExpression& e) { return e.getExpression().evaluate(context); }, + [&context](const style::PropertyExpression& e) { + // Fall back to the global state captured by the wrapped + // property expression, which would otherwise be bypassed by + // evaluating the raw inner expression directly. + if (context.globalState == nullptr && e.getCapturedGlobalState() != nullptr) { + EvaluationContext contextWithState = context; + contextWithState.globalState = e.getCapturedGlobalState().get(); + return e.getExpression().evaluate(contextWithState); + } + return e.getExpression().evaluate(context); + }, [](const T& t) -> EvaluationResult { return t; }); } diff --git a/include/mln/style/layer.hpp b/include/mln/style/layer.hpp index 2e71f747dff3..d47aaef06ea9 100644 --- a/include/mln/style/layer.hpp +++ b/include/mln/style/layer.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -133,6 +134,10 @@ class Layer { VisibilityType getVisibility() const; void setVisibility(VisibilityType); + /// Re-evaluates the layer's visibility expression (a "visibility" layout + /// value using "global-state"), if any, against the given global state. + void reevaluateVisibility(const GlobalStateMap&); + // Zoom range float getMinZoom() const; float getMaxZoom() const; diff --git a/include/mln/style/layer_properties.hpp b/include/mln/style/layer_properties.hpp index 1ce2959cafe3..74a22af800e6 100644 --- a/include/mln/style/layer_properties.hpp +++ b/include/mln/style/layer_properties.hpp @@ -23,6 +23,11 @@ class LayerProperties { virtual expression::Dependency getDependencies() const noexcept = 0; + /// Returns the combined dependencies of the expressions retained in the + /// evaluated properties (i.e. those that still require per-feature + /// evaluation, such as data-driven paint properties). + virtual expression::Dependency getEvaluatedDependencies() const noexcept { return expression::Dependency::None; } + protected: LayerProperties(Immutable impl) noexcept : baseImpl(std::move(impl)) {} diff --git a/include/mln/style/property_expression.hpp b/include/mln/style/property_expression.hpp index 96159ff4130b..d1f2ccb0fa92 100644 --- a/include/mln/style/property_expression.hpp +++ b/include/mln/style/property_expression.hpp @@ -45,6 +45,19 @@ class PropertyExpressionBase { bool getUseIntegerZoom() const { return useIntegerZoom_; } void setUseIntegerZoom(bool value) { useIntegerZoom_ = value; } + /// Capture a snapshot of the style's global state so that later evaluations + /// (e.g. per-feature evaluation during tile layout or in paint property + /// binders) can resolve `global-state` expressions without explicit + /// plumbing. Only retained if the expression actually depends on it. + void captureGlobalState(std::shared_ptr globalState) { + if (expression && expression->has(Dependency::GlobalState)) { + capturedGlobalState_ = std::move(globalState); + } + } + const std::shared_ptr& getCapturedGlobalState() const noexcept { + return capturedGlobalState_; + } + /// Can be used for aggregating property expressions from multiple properties(layers) into single match / case /// expression. May be removed if a better way of aggregation is found. std::shared_ptr getSharedExpression() const noexcept; @@ -59,6 +72,8 @@ class PropertyExpressionBase { protected: std::shared_ptr expression; + std::shared_ptr capturedGlobalState_; + ZoomCurvePtr zoomCurve; bool useIntegerZoom_ = false; @@ -82,18 +97,16 @@ class PropertyExpression final : public PropertyExpressionBase { defaultValue(std::move(defaultValue_)) {} T evaluate(const expression::EvaluationContext& context, T finalDefaultValue = T()) const { - const expression::EvaluationResult result = expression->evaluate(context); - if (result) { - const std::optional typed = expression::fromExpressionValue(*result); - if (typed) { - return *typed; - } + if (context.globalState == nullptr && capturedGlobalState_ != nullptr) { + expression::EvaluationContext contextWithState = context; + contextWithState.globalState = capturedGlobalState_.get(); + return evaluateContext(contextWithState, std::move(finalDefaultValue)); } - return defaultValue ? *defaultValue : finalDefaultValue; + return evaluateContext(context, std::move(finalDefaultValue)); } T evaluate(float zoom) const { - assert(!isZoomConstant()); + assert(!isZoomConstant() || (getDependencies() & expression::Dependency::GlobalState)); assert(isFeatureConstant()); return evaluate(expression::EvaluationContext(zoom)); } @@ -172,6 +185,17 @@ class PropertyExpression final : public PropertyExpressionBase { } private: + T evaluateContext(const expression::EvaluationContext& context, T finalDefaultValue) const { + const expression::EvaluationResult result = expression->evaluate(context); + if (result) { + const std::optional typed = expression::fromExpressionValue(*result); + if (typed) { + return *typed; + } + } + return defaultValue ? *defaultValue : finalDefaultValue; + } + std::optional defaultValue; }; diff --git a/include/mln/style/style.hpp b/include/mln/style/style.hpp index f656d626f290..4beafe13e902 100644 --- a/include/mln/style/style.hpp +++ b/include/mln/style/style.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,12 @@ class Style { void setLight(std::unique_ptr); + // Global state, used by the "global-state" expression. + // Setting a null value resets the property to the default defined in the + // style's root "state" property (or null if there is none). + GlobalStateMap getGlobalState() const; + void setGlobalStateProperty(const std::string& name, const Value& value); + // Images std::optional getImage(const std::string&) const; void addImage(std::unique_ptr); diff --git a/include/mln/util/feature.hpp b/include/mln/util/feature.hpp index b0d85c191563..971b96db62df 100644 --- a/include/mln/util/feature.hpp +++ b/include/mln/util/feature.hpp @@ -16,6 +16,7 @@ using GeoJSONFeature = mapbox::feature::feature; using FeatureState = mapbox::base::ValueObject; using FeatureStates = std::unordered_map; // using LayerFeatureStates = std::unordered_map; // +using GlobalStateMap = mapbox::base::ValueObject; // class Feature : public GeoJSONFeature { public: diff --git a/scripts/style-spec-reference/v8.json b/scripts/style-spec-reference/v8.json index ad9f8add2d6b..c94ce02c72e8 100644 --- a/scripts/style-spec-reference/v8.json +++ b/scripts/style-spec-reference/v8.json @@ -106,6 +106,26 @@ "intensity": 0.4 } }, + "state": { + "type": "state", + "default": {}, + "doc": "An object used to define default values when using the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression.", + "example": { + "chargerType": { + "default": ["CCS", "CHAdeMO", "Type2"] + }, + "minPreferredChargingSpeed": { + "default": 50 + } + }, + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" + } + } + }, "sky": { "type": "sky", "doc": "The map's sky configuration. **Note:** this definition is still experimental and is under development in maplibre-gl-js.", @@ -927,9 +947,20 @@ "js": "0.10.0", "android": "2.0.1", "ios": "2.0.0" + }, + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_fill": { @@ -974,9 +1005,20 @@ "js": "0.10.0", "android": "2.0.1", "ios": "2.0.0" + }, + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_circle": { @@ -1021,9 +1063,20 @@ "js": "0.10.0", "android": "2.0.1", "ios": "2.0.0" + }, + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_heatmap": { @@ -1046,7 +1099,13 @@ "ios": "4.0.0" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_fill-extrusion": { @@ -1083,7 +1142,13 @@ "ios": "3.6.0" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_line": { @@ -1242,9 +1307,20 @@ "js": "0.10.0", "android": "2.0.1", "ios": "2.0.0" + }, + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_symbol": { @@ -2697,9 +2773,20 @@ "js": "0.10.0", "android": "2.0.1", "ios": "2.0.0" + }, + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_raster": { @@ -2720,9 +2807,20 @@ "js": "0.10.0", "android": "2.0.1", "ios": "2.0.0" + }, + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_hillshade": { @@ -2745,7 +2843,13 @@ "ios": "4.0.0" } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "layout_color-relief": { @@ -2765,7 +2869,13 @@ "basic functionality": { } }, - "property-type": "constant" + "expression": { + "interpolated": false, + "parameters": [ + "global-state" + ] + }, + "property-type": "data-constant" } }, "filter": { @@ -4161,6 +4271,32 @@ } } }, + "global-state": { + "doc": "Retrieves a property value from the map's global state. Returns null if the requested property is not present in the global state. The global state can be set programmatically and defaults can be defined using the root-level `state` style property.", + "syntax": { + "overloads": [ + { + "parameters": ["property_name"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "property_name", + "type": "string" + } + ] + }, + "example": ["global-state", "chargerType"], + "group": "Feature data", + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" + } + } + }, "geometry-type": { "doc": "Returns the feature's simple geometry type: `Point`, `LineString`, or `Polygon`. `MultiPoint`, `MultiLineString`, and `MultiPolygon` are returned as `Point`, `LineString`, and `Polygon`, respectively.", "syntax": { diff --git a/src/mln/geometry/feature_index.cpp b/src/mln/geometry/feature_index.cpp index fde694a25a8c..f3b1fbbc513e 100644 --- a/src/mln/geometry/feature_index.cpp +++ b/src/mln/geometry/feature_index.cpp @@ -290,8 +290,10 @@ void FeatureIndex::addFeature(std::unordered_map(tileID.z), - geometryTileFeature.get()})) { + if (options.filter && + !(*options.filter)( + style::expression::EvaluationContext{static_cast(tileID.z), geometryTileFeature.get()} + .withGlobalState(options.globalState.get()))) { continue; } diff --git a/src/mln/layout/circle_layout.hpp b/src/mln/layout/circle_layout.hpp index e84f3672fda1..5463d6e0d75f 100644 --- a/src/mln/layout/circle_layout.hpp +++ b/src/mln/layout/circle_layout.hpp @@ -21,7 +21,9 @@ class CircleLayout final : public Layout { auto leaderLayerProperties = staticImmutableCast(group.front()); const auto& unevaluatedLayout = leaderLayerProperties->layerImpl().layout; const bool sortFeaturesByKey = !unevaluatedLayout.get().isUndefined(); - const auto& layout = unevaluatedLayout.evaluate(PropertyEvaluationParameters(zoom)); + PropertyEvaluationParameters evaluationParameters(zoom); + evaluationParameters.globalState = parameters.globalState; + const auto& layout = unevaluatedLayout.evaluate(evaluationParameters); sourceLayerID = leaderLayerProperties->layerImpl().sourceLayer; bucketLeaderID = leaderLayerProperties->layerImpl().id; @@ -34,7 +36,8 @@ class CircleLayout final : public Layout { for (size_t i = 0; i < featureCount; ++i) { auto feature = sourceLayer->getFeature(i); if (!leaderLayerProperties->layerImpl().filter(style::expression::EvaluationContext(zoom, feature.get()) - .withCanonicalTileID(¶meters.tileID.canonical))) { + .withCanonicalTileID(¶meters.tileID.canonical) + .withGlobalState(parameters.globalState.get()))) { continue; } diff --git a/src/mln/layout/pattern_layout.hpp b/src/mln/layout/pattern_layout.hpp index f348d6c4059c..839e1a86bccf 100644 --- a/src/mln/layout/pattern_layout.hpp +++ b/src/mln/layout/pattern_layout.hpp @@ -101,7 +101,9 @@ class PatternLayout : public Layout { hasPattern(false) { assert(!group.empty()); auto leaderLayerProperties = staticImmutableCast(group.front()); - layout = leaderLayerProperties->layerImpl().layout.evaluate(PropertyEvaluationParameters(zoom)); + PropertyEvaluationParameters evaluationParameters(zoom); + evaluationParameters.globalState = parameters.globalState; + layout = leaderLayerProperties->layerImpl().layout.evaluate(evaluationParameters); sourceLayerID = leaderLayerProperties->layerImpl().sourceLayer; bucketLeaderID = leaderLayerProperties->layerImpl().id; @@ -128,7 +130,8 @@ class PatternLayout : public Layout { auto feature = sourceLayer->getFeature(i); if (!leaderLayerProperties->layerImpl().filter( style::expression::EvaluationContext(this->zoom, feature.get()) - .withCanonicalTileID(¶meters.tileID.canonical))) + .withCanonicalTileID(¶meters.tileID.canonical) + .withGlobalState(parameters.globalState.get()))) continue; PatternLayerMap patternDependencyMap; diff --git a/src/mln/layout/symbol_layout.cpp b/src/mln/layout/symbol_layout.cpp index a71c27ff9ad9..04662454f669 100644 --- a/src/mln/layout/symbol_layout.cpp +++ b/src/mln/layout/symbol_layout.cpp @@ -55,9 +55,13 @@ inline const SymbolLayerProperties& toSymbolLayerProperties(const Immutable createLayout( - const SymbolLayoutProperties::Unevaluated& unevaluated, float zoom) { + const SymbolLayoutProperties::Unevaluated& unevaluated, + float zoom, + std::shared_ptr globalState) { + PropertyEvaluationParameters evaluationParameters(zoom); + evaluationParameters.globalState = std::move(globalState); auto layout = makeMutable( - unevaluated.evaluate(PropertyEvaluationParameters(zoom))); + unevaluated.evaluate(evaluationParameters)); if (layout->get() == AlignmentType::Auto) { if (layout->get() != SymbolPlacementType::Point) { @@ -128,7 +132,7 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, pixelRatio(parameters.pixelRatio), tileSize(static_cast(util::tileSize_D * overscaling)), tilePixelRatio(static_cast(util::EXTENT) / tileSize), - layout(createLayout(toSymbolLayerProperties(layers.at(0)).layerImpl().layout, zoom)) { + layout(createLayout(toSymbolLayerProperties(layers.at(0)).layerImpl().layout, zoom, parameters.globalState)) { const SymbolLayer::Impl& leader = toSymbolLayerProperties(layers.at(0)).layerImpl(); textSize = leader.layout.get(); @@ -136,6 +140,18 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, textRadialOffset = leader.layout.get(); textVariableAnchorOffset = leader.layout.get(); + // These property values are retained unevaluated; capture the global + // state so that later evaluations can resolve "global-state" expressions. + const auto captureGlobalState = [&](auto& propertyValue) { + if (propertyValue.isExpression()) { + propertyValue.asExpression().captureGlobalState(parameters.globalState); + } + }; + captureGlobalState(textSize); + captureGlobalState(iconSize); + captureGlobalState(textRadialOffset); + captureGlobalState(textVariableAnchorOffset); + const bool hasText = has(*layout) && has(*layout); const bool hasIcon = has(*layout); @@ -172,7 +188,8 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, for (size_t i = 0; i < featureCount; ++i) { auto feature = sourceLayer->getFeature(i); if (!leader.filter(expression::EvaluationContext(this->zoom, feature.get()) - .withCanonicalTileID(¶meters.tileID.canonical))) + .withCanonicalTileID(¶meters.tileID.canonical) + .withGlobalState(parameters.globalState.get()))) continue; SymbolFeature ft(std::move(feature)); diff --git a/src/mln/map/map_impl.cpp b/src/mln/map/map_impl.cpp index 64ee606e45ed..0f7431bd7472 100644 --- a/src/mln/map/map_impl.cpp +++ b/src/mln/map/map_impl.cpp @@ -128,6 +128,7 @@ void Map::Impl::onUpdate() { .images = style->impl->getImageImpls(), .sources = style->impl->getSourceImpls(), .layers = style->impl->getLayerImpls(), + .globalState = style->impl->getGlobalStateShared(), .annotationManager = annotationManager.makeWeakPtr(), .fileSource = fileSource, .prefetchZoomDelta = prefetchZoomDelta, diff --git a/src/mln/renderer/bucket_parameters.hpp b/src/mln/renderer/bucket_parameters.hpp index 3adca03ffd72..05c9a4c64545 100644 --- a/src/mln/renderer/bucket_parameters.hpp +++ b/src/mln/renderer/bucket_parameters.hpp @@ -2,6 +2,9 @@ #include #include +#include + +#include namespace mln { namespace style { @@ -14,6 +17,8 @@ class BucketParameters { const MapMode mode; const float pixelRatio; const style::LayerTypeInfo* layerType; + // The style's global state, used by "global-state" expressions. + std::shared_ptr globalState = nullptr; }; } // namespace mln diff --git a/src/mln/renderer/buckets/symbol_bucket.hpp b/src/mln/renderer/buckets/symbol_bucket.hpp index c669e5f54a14..c05f55fc55a0 100644 --- a/src/mln/renderer/buckets/symbol_bucket.hpp +++ b/src/mln/renderer/buckets/symbol_bucket.hpp @@ -107,9 +107,13 @@ class ConstantSymbolSizeBinder final : public SymbolSizeBinder { ) : layoutSize(expression_.evaluate(tileZoom + 1)), expression(expression_) { - const Range zoomLevels = expression_.getCoveringStops(tileZoom, tileZoom + 1); - coveringRanges = std::make_tuple( - zoomLevels, Range{expression_.evaluate(zoomLevels.min), expression_.evaluate(zoomLevels.max)}); + // A feature-constant expression without a zoom dependency (e.g. one + // that only depends on the global state) has no zoom curve. + if (!expression_.isZoomConstant()) { + const Range zoomLevels = expression_.getCoveringStops(tileZoom, tileZoom + 1); + coveringRanges = std::make_tuple( + zoomLevels, Range{expression_.evaluate(zoomLevels.min), expression_.evaluate(zoomLevels.max)}); + } } Range getVertexSizeData(const GeometryTileFeature&) noexcept override { return {0.0f, 0.0f}; }; diff --git a/src/mln/renderer/cross_faded_property_evaluator.cpp b/src/mln/renderer/cross_faded_property_evaluator.cpp index 7ab15b7fc1d8..1782591a1629 100644 --- a/src/mln/renderer/cross_faded_property_evaluator.cpp +++ b/src/mln/renderer/cross_faded_property_evaluator.cpp @@ -18,9 +18,11 @@ Faded CrossFadedPropertyEvaluator::operator()(const T& constant) const { template Faded CrossFadedPropertyEvaluator::operator()(const style::PropertyExpression& expression) const { - return calculate(expression.evaluate(parameters.z - 1.0f), - expression.evaluate(parameters.z), - expression.evaluate(parameters.z + 1.0f)); + using style::expression::EvaluationContext; + const auto* globalState = parameters.globalState.get(); + return calculate(expression.evaluate(EvaluationContext(parameters.z - 1.0f).withGlobalState(globalState)), + expression.evaluate(EvaluationContext(parameters.z).withGlobalState(globalState)), + expression.evaluate(EvaluationContext(parameters.z + 1.0f).withGlobalState(globalState))); } template diff --git a/src/mln/renderer/data_driven_property_evaluator.hpp b/src/mln/renderer/data_driven_property_evaluator.hpp index 296c7ae41fa3..a42c621c1265 100644 --- a/src/mln/renderer/data_driven_property_evaluator.hpp +++ b/src/mln/renderer/data_driven_property_evaluator.hpp @@ -24,18 +24,29 @@ class DataDrivenPropertyEvaluator { ResultType operator()(const T& constant) const { return ResultType(constant); } ResultType operator()(const style::PropertyExpression& expression) const { + // Expressions that depend on the feature or on runtime-loaded images + // cannot be evaluated yet and are kept for later per-feature + // evaluation. Global-state-only expressions can be evaluated + // immediately with the state carried by the evaluation parameters. + const bool needsFeature = !expression.isFeatureConstant() || + (expression.getDependencies() & style::expression::Dependency::Image); if constexpr (useIntegerZoom) { // Compiler will optimize out the unused branch. - if (!expression.isFeatureConstant() || !expression.isRuntimeConstant()) { + if (needsFeature) { auto returnExpression = expression; returnExpression.setUseIntegerZoom(true); + returnExpression.captureGlobalState(parameters.globalState); return ResultType(returnExpression); } - return ResultType(expression.evaluate(std::floor(parameters.z))); + return ResultType(expression.evaluate(style::expression::EvaluationContext(std::floor(parameters.z)) + .withGlobalState(parameters.globalState.get()))); } else { - if (!expression.isFeatureConstant() || !expression.isRuntimeConstant()) { - return ResultType(expression); + if (needsFeature) { + auto returnExpression = expression; + returnExpression.captureGlobalState(parameters.globalState); + return ResultType(returnExpression); } - return ResultType(expression.evaluate(parameters.z)); + return ResultType(expression.evaluate( + style::expression::EvaluationContext(parameters.z).withGlobalState(parameters.globalState.get()))); } } @@ -62,7 +73,9 @@ class DataDrivenPropertyEvaluator> { ResultType operator()(const style::PropertyExpression& expression) const { if (!expression.isFeatureConstant() || !expression.isRuntimeConstant()) { - return ResultType(expression); + auto returnExpression = expression; + returnExpression.captureGlobalState(parameters.globalState); + return ResultType(returnExpression); } else { const T evaluated = expression.evaluate(std::floor(parameters.z)); return ResultType(calculate(evaluated, evaluated, evaluated)); diff --git a/src/mln/renderer/layers/render_color_relief_layer.cpp b/src/mln/renderer/layers/render_color_relief_layer.cpp index 29bad1684505..7602dedd235b 100644 --- a/src/mln/renderer/layers/render_color_relief_layer.cpp +++ b/src/mln/renderer/layers/render_color_relief_layer.cpp @@ -61,6 +61,14 @@ void RenderColorReliefLayer::transition(const TransitionParameters& parameters) } void RenderColorReliefLayer::evaluate(const PropertyEvaluationParameters& parameters) { + if (colorRampGlobalState != parameters.globalState) { + colorRampGlobalState = parameters.globalState; + if (unevaluated.get().getValue().getDependencies() & + style::expression::Dependency::GlobalState) { + updateColorRamp(); + } + } + const auto previousProperties = staticImmutableCast(evaluatedProperties); auto properties = makeMutable( staticImmutableCast(baseImpl), @@ -134,6 +142,7 @@ void RenderColorReliefLayer::updateColorRamp() { // Create evaluation context with elevation as color ramp parameter expression::EvaluationContext context(std::nullopt, nullptr, static_cast(elevation)); + context.globalState = colorRampGlobalState.get(); expression::EvaluationResult result = expr.evaluate(context); @@ -158,7 +167,7 @@ void RenderColorReliefLayer::updateColorRamp() { float elevation = minElevation + t * (maxElevation - minElevation); elevationStopsVector.push_back(elevation); - Color color = colorValue.evaluate(static_cast(elevation)); + Color color = colorValue.evaluate(static_cast(elevation), colorRampGlobalState.get()); colorStopsVector.push_back(color); } } diff --git a/src/mln/renderer/layers/render_color_relief_layer.hpp b/src/mln/renderer/layers/render_color_relief_layer.hpp index 3f4371b44e3a..95058d4abb21 100644 --- a/src/mln/renderer/layers/render_color_relief_layer.hpp +++ b/src/mln/renderer/layers/render_color_relief_layer.hpp @@ -52,6 +52,9 @@ class RenderColorReliefLayer final : public RenderLayer { // Color ramp data uint32_t colorRampSize = 256; bool colorRampChanged = true; + // The style's global state as of the last evaluation, used to resolve + // "global-state" expressions in the color ramp. + std::shared_ptr colorRampGlobalState; // FIX 1: Changed type and name to correctly hold float elevation data std::shared_ptr> elevationStopsData; // Elevation values for each stop diff --git a/src/mln/renderer/layers/render_heatmap_layer.cpp b/src/mln/renderer/layers/render_heatmap_layer.cpp index 552b403d48a0..eedb040bac31 100644 --- a/src/mln/renderer/layers/render_heatmap_layer.cpp +++ b/src/mln/renderer/layers/render_heatmap_layer.cpp @@ -59,6 +59,13 @@ void RenderHeatmapLayer::layerChanged(const TransitionParameters& parameters, } void RenderHeatmapLayer::evaluate(const PropertyEvaluationParameters& parameters) { + if (colorRampGlobalState != parameters.globalState) { + colorRampGlobalState = parameters.globalState; + if (unevaluated.get().getValue().getDependencies() & expression::Dependency::GlobalState) { + updateColorRamp(); + } + } + const auto previousProperties = staticImmutableCast(evaluatedProperties); auto properties = makeMutable( staticImmutableCast(baseImpl), @@ -92,7 +99,7 @@ void RenderHeatmapLayer::updateColorRamp() { colorValue = HeatmapLayer::getDefaultHeatmapColor(); } - applyColorRamp(colorValue, *colorRamp); + applyColorRamp(colorValue, *colorRamp, colorRampGlobalState.get()); } } diff --git a/src/mln/renderer/layers/render_heatmap_layer.hpp b/src/mln/renderer/layers/render_heatmap_layer.hpp index 30b3d212d86a..53d13ec1bdea 100644 --- a/src/mln/renderer/layers/render_heatmap_layer.hpp +++ b/src/mln/renderer/layers/render_heatmap_layer.hpp @@ -60,6 +60,9 @@ class RenderHeatmapLayer final : public RenderLayer { // Paint properties style::HeatmapPaintProperties::Unevaluated unevaluated; std::shared_ptr colorRamp; + // The style's global state as of the last evaluation, used to resolve + // "global-state" expressions in the color ramp. + std::shared_ptr colorRampGlobalState; std::unique_ptr renderTexture; SegmentVector segments; diff --git a/src/mln/renderer/layers/render_line_layer.cpp b/src/mln/renderer/layers/render_line_layer.cpp index b7d577fc5762..60609910d4cf 100644 --- a/src/mln/renderer/layers/render_line_layer.cpp +++ b/src/mln/renderer/layers/render_line_layer.cpp @@ -67,6 +67,14 @@ void RenderLineLayer::transition(const TransitionParameters& parameters) { } void RenderLineLayer::evaluate(const PropertyEvaluationParameters& parameters) { + if (colorRampGlobalState != parameters.globalState) { + colorRampGlobalState = parameters.globalState; + if (unevaluated.get().getValue().getDependencies() & + style::expression::Dependency::GlobalState) { + updateColorRamp(); + } + } + const auto previousProperties = staticImmutableCast(evaluatedProperties); auto properties = makeMutable(staticImmutableCast(baseImpl), parameters.getCrossfadeParameters(), @@ -184,7 +192,7 @@ bool RenderLineLayer::queryIntersectsFeature(const GeometryCoordinates& queryGeo void RenderLineLayer::updateColorRamp() { const style::ColorRampPropertyValue colorValue = unevaluated.get().getValue(); - if (!colorRamp || !applyColorRamp(colorValue, *colorRamp)) { + if (!colorRamp || !applyColorRamp(colorValue, *colorRamp, colorRampGlobalState.get())) { return; } diff --git a/src/mln/renderer/layers/render_line_layer.hpp b/src/mln/renderer/layers/render_line_layer.hpp index a813b10a87d4..6df975e5ae8d 100644 --- a/src/mln/renderer/layers/render_line_layer.hpp +++ b/src/mln/renderer/layers/render_line_layer.hpp @@ -58,6 +58,9 @@ class RenderLineLayer final : public RenderLayer { void updateColorRamp(); std::shared_ptr colorRamp; + // The style's global state as of the last evaluation, used to resolve + // "global-state" expressions in the color ramp. + std::shared_ptr colorRampGlobalState; gfx::Texture2DPtr colorRampTexture2D; gfx::ShaderGroupPtr lineShaderGroup; diff --git a/src/mln/renderer/property_evaluation_parameters.hpp b/src/mln/renderer/property_evaluation_parameters.hpp index 8e86716fa868..95301b9d3f51 100644 --- a/src/mln/renderer/property_evaluation_parameters.hpp +++ b/src/mln/renderer/property_evaluation_parameters.hpp @@ -2,6 +2,9 @@ #include #include +#include + +#include namespace mln { @@ -43,9 +46,13 @@ class PropertyEvaluationParameters { ZoomHistory zoomHistory; Duration defaultFadeDuration; + /// The style's current global state, used to evaluate `global-state` expressions. + std::shared_ptr globalState; + bool zoomChanged = true; bool layerChanged = false; bool hasCrossfade = true; + bool globalStateChanged = false; }; } // namespace mln diff --git a/src/mln/renderer/property_evaluator.hpp b/src/mln/renderer/property_evaluator.hpp index bc2c8ba65328..1182f10bbbbf 100644 --- a/src/mln/renderer/property_evaluator.hpp +++ b/src/mln/renderer/property_evaluator.hpp @@ -17,7 +17,10 @@ class PropertyEvaluator { T operator()(const style::Undefined&) const { return defaultValue; } T operator()(const T& constant) const { return constant; } - T operator()(const style::PropertyExpression& fn) const { return fn.evaluate(parameters.z); } + T operator()(const style::PropertyExpression& fn) const { + return fn.evaluate( + style::expression::EvaluationContext(parameters.z).withGlobalState(parameters.globalState.get())); + } private: const PropertyEvaluationParameters& parameters; diff --git a/src/mln/renderer/render_layer.cpp b/src/mln/renderer/render_layer.cpp index 9b661068c2ee..e54fc2689fd9 100644 --- a/src/mln/renderer/render_layer.cpp +++ b/src/mln/renderer/render_layer.cpp @@ -232,7 +232,9 @@ void RenderLayer::activateLayerGroup(const LayerGroupBasePtr& layerGroup_, } } -bool RenderLayer::applyColorRamp(const style::ColorRampPropertyValue& colorValue, PremultipliedImage& image) { +bool RenderLayer::applyColorRamp(const style::ColorRampPropertyValue& colorValue, + PremultipliedImage& image, + const GlobalStateMap* globalState) { if (colorValue.isUndefined()) { return false; } @@ -240,7 +242,7 @@ bool RenderLayer::applyColorRamp(const style::ColorRampPropertyValue& colorValue const auto length = image.bytes(); for (uint32_t i = 0; i < length; i += 4) { - const auto color = colorValue.evaluate(static_cast(i) / length); + const auto color = colorValue.evaluate(static_cast(i) / length, globalState); image.data[i + 0] = static_cast(std::floor(color.r * 255.f)); image.data[i + 1] = static_cast(std::floor(color.g * 255.f)); image.data[i + 2] = static_cast(std::floor(color.b * 255.f)); diff --git a/src/mln/renderer/render_layer.hpp b/src/mln/renderer/render_layer.hpp index 7dc4524a48af..5670c900b8fc 100644 --- a/src/mln/renderer/render_layer.hpp +++ b/src/mln/renderer/render_layer.hpp @@ -281,7 +281,9 @@ class RenderLayer { /// unchanged bool setRenderTileBucketID(const OverscaledTileID&, util::SimpleIdentity bucketID); - static bool applyColorRamp(const style::ColorRampPropertyValue&, PremultipliedImage&); + static bool applyColorRamp(const style::ColorRampPropertyValue&, + PremultipliedImage&, + const GlobalStateMap* globalState = nullptr); protected: // Stores current set of tiles to be rendered for this layer. diff --git a/src/mln/renderer/render_orchestrator.cpp b/src/mln/renderer/render_orchestrator.cpp index f6a5cb12a668..5e354bbca047 100644 --- a/src/mln/renderer/render_orchestrator.cpp +++ b/src/mln/renderer/render_orchestrator.cpp @@ -183,8 +183,13 @@ std::unique_ptr RenderOrchestrator::createRenderTree( const auto transitionDuration = transitionOptions.duration.value_or( isMapModeContinuous ? util::DEFAULT_TRANSITION_DURATION : Duration::zero()); + const bool globalStateChanged = globalState != updateParameters->globalState; + globalState = updateParameters->globalState; + PropertyEvaluationParameters evaluationParameters{zoomHistory, updateParameters->timePoint, transitionDuration}; evaluationParameters.zoomChanged = zoomChanged; + evaluationParameters.globalState = globalState; + evaluationParameters.globalStateChanged = globalStateChanged; TileParameters tileParameters{.pixelRatio = updateParameters->pixelRatio, .debugOptions = updateParameters->debugOptions, @@ -201,7 +206,8 @@ std::unique_ptr RenderOrchestrator::createRenderTree( .tileLodPitchThreshold = updateParameters->tileLodPitchThreshold, .tileLodZoomShift = updateParameters->tileLodZoomShift, .tileLodMode = updateParameters->tileLodMode, - .dynamicTextureAtlas = dynamicTextureAtlas}; + .dynamicTextureAtlas = dynamicTextureAtlas, + .globalState = globalState}; glyphManager->setURL(updateParameters->glyphURL); glyphManager->setFontFaces(updateParameters->fontFaces); @@ -214,7 +220,7 @@ std::unique_ptr RenderOrchestrator::createRenderTree( renderLight.transition(transitionParameters); } - if (lightChanged || zoomChanged || renderLight.hasTransition()) { + if (lightChanged || zoomChanged || globalStateChanged || renderLight.hasTransition()) { renderLight.evaluate(evaluationParameters); } @@ -315,8 +321,13 @@ std::unique_ptr RenderOrchestrator::createRenderTree( const bool zoomChangedAndMatters = zoomChanged && !layerAddedOrChanged && (layer.getStyleDependencies() & Dependency::Zoom); - if (layerAddedOrChanged || zoomChangedAndMatters || evaluationParameters.hasCrossfade || - layer.hasTransition()) { + // Similarly, only re-evaluate on a global state change if the layer + // references the global state. + const bool globalStateChangedAndMatters = globalStateChanged && !layerAddedOrChanged && + (layer.getStyleDependencies() & Dependency::GlobalState); + + if (layerAddedOrChanged || zoomChangedAndMatters || globalStateChangedAndMatters || + evaluationParameters.hasCrossfade || layer.hasTransition()) { const auto previousMask = layer.evaluatedProperties->constantsMask(); layer.evaluate(evaluationParameters); if (previousMask != layer.evaluatedProperties->constantsMask()) { @@ -383,9 +394,18 @@ std::unique_ptr RenderOrchestrator::createRenderTree( if (layerInfo->source != LayerTypeInfo::Source::NotRequired) { if (layer.baseImpl->source == sourceImpl->id) { const std::string& layerId = layer.getID(); + // A global state change requires a relayout of this + // layer's source if the state is referenced by anything + // that is baked into the tile's buckets: the filter, + // layout properties, or data-driven paint properties + // (i.e. expressions retained in the evaluated properties). + const bool globalStateRequiresRelayout = + globalStateChanged && ((layer.baseImpl->getLayoutDependencies() | + layer.evaluatedProperties->getEvaluatedDependencies()) & + expression::Dependency::GlobalState); sourceNeedsRelayout = (sourceNeedsRelayout || hasImageDiff || constantsMaskChanged.contains(layerId) || - hasLayoutDifference(layerDiff, layerId)); + hasLayoutDifference(layerDiff, layerId) || globalStateRequiresRelayout); if (layerIsVisible) { filteredLayersForSource.push_back(layer.evaluatedProperties); if (zoomFitsLayer) { @@ -627,10 +647,15 @@ void RenderOrchestrator::queryRenderedSymbols(std::unordered_map RenderOrchestrator::queryRenderedFeatures( const ScreenLineString& geometry, - const RenderedQueryOptions& options, + const RenderedQueryOptions& options_, const std::unordered_map& layers) const { MLN_TRACE_FUNC(); + // Inject the current global state so that "global-state" expressions in + // the query filter evaluate against the state that was rendered. + RenderedQueryOptions options = options_; + options.globalState = globalState; + std::unordered_set sourceIDs; std::unordered_map filteredLayers; for (const auto& pair : layers) { @@ -700,12 +725,17 @@ std::vector RenderOrchestrator::queryShapeAnnotations(const ScreenLineS } std::vector RenderOrchestrator::querySourceFeatures(const std::string& sourceID, - const SourceQueryOptions& options) const { + const SourceQueryOptions& options_) const { MLN_TRACE_FUNC(); const RenderSource* source = getRenderSource(sourceID); if (!source) return {}; + // Inject the current global state so that "global-state" expressions in + // the query filter evaluate against the current state. + SourceQueryOptions options = options_; + options.globalState = globalState; + return source->querySourceFeatures(options); } diff --git a/src/mln/renderer/render_orchestrator.hpp b/src/mln/renderer/render_orchestrator.hpp index 15f1f6665aeb..cfd41691c764 100644 --- a/src/mln/renderer/render_orchestrator.hpp +++ b/src/mln/renderer/render_orchestrator.hpp @@ -215,6 +215,9 @@ class RenderOrchestrator final : public GlyphManagerObserver, public ImageManage Immutable>> sourceImpls; Immutable>> layerImpls; + // The style's global state as of the last update, used by "global-state" expressions. + std::shared_ptr globalState; + std::unordered_map> renderSources; std::unordered_map> renderLayers; RenderLight renderLight; diff --git a/src/mln/renderer/tile_parameters.hpp b/src/mln/renderer/tile_parameters.hpp index 291858c93da2..2598d93e255a 100644 --- a/src/mln/renderer/tile_parameters.hpp +++ b/src/mln/renderer/tile_parameters.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -39,6 +40,7 @@ class TileParameters { double tileLodZoomShift = 0; TileLodMode tileLodMode = TileLodMode::Default; gfx::DynamicTextureAtlasPtr dynamicTextureAtlas; + std::shared_ptr globalState = nullptr; bool isUpdateSynchronous = false; }; diff --git a/src/mln/renderer/tile_pyramid.cpp b/src/mln/renderer/tile_pyramid.cpp index 41e899dfd2a9..67319431ea28 100644 --- a/src/mln/renderer/tile_pyramid.cpp +++ b/src/mln/renderer/tile_pyramid.cpp @@ -164,6 +164,7 @@ void TilePyramid::update(const std::vector>& l } if (needsRelayout) { + tile.setGlobalState(parameters.globalState); tile.setLayers(layers); } }; @@ -190,6 +191,7 @@ void TilePyramid::update(const std::vector>& l if (!tile) { tile = createTile(tileID, observer); if (!tile) return nullptr; + tile->setGlobalState(parameters.globalState); tile->setLayers(layers); } diff --git a/src/mln/renderer/update_parameters.hpp b/src/mln/renderer/update_parameters.hpp index 64ee8d52fccc..863a1891d5a8 100644 --- a/src/mln/renderer/update_parameters.hpp +++ b/src/mln/renderer/update_parameters.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -38,6 +39,9 @@ class UpdateParameters { const Immutable>> sources; const Immutable>> layers; + // The style's global state, used by "global-state" expressions. + const std::shared_ptr globalState; + mapbox::base::WeakPtr annotationManager; std::shared_ptr fileSource; diff --git a/src/mln/style/expression/compound_expression.cpp b/src/mln/style/expression/compound_expression.cpp index da025387dcc3..d198137895ee 100644 --- a/src/mln/style/expression/compound_expression.cpp +++ b/src/mln/style/expression/compound_expression.cpp @@ -786,6 +786,23 @@ const auto& featureStateCompoundExpression() { return signature; } +const auto& globalStateCompoundExpression() { + static auto signature = detail::makeSignature( + "global-state", + [](const EvaluationContext& params, const std::string& key) -> Result { + mln::Value state; + if (params.globalState != nullptr) { + auto it = params.globalState->find(key); + if (it != params.globalState->end()) { + state = mln::Value(it->second); + } + } + return toExpressionValue(state); + }, + Dependency::GlobalState); + return signature; +} + // Legacy Filters const auto& filterEqualsCompoundExpression() { static auto signature = detail::makeSignature( @@ -1110,6 +1127,7 @@ constexpr const auto compoundExpressionRegistry = {"resolved-locale", resolvedLocaleCompoundExpression}, {"error", errorCompoundExpression}, {"feature-state", featureStateCompoundExpression}, + {"global-state", globalStateCompoundExpression}, // Legacy Filters {"filter-==", filterEqualsCompoundExpression}, {"filter-id-==", filterIdEqualsCompoundExpression}, @@ -1250,6 +1268,13 @@ ParseResult parseCompoundExpression(const std::string& name, const Convertible& auto length = arrayLength(value); + // Per the style specification, the "global-state" property name must be a + // string literal, not the result of a sub-expression. + if (name == "global-state" && length == 2 && !toString(arrayMember(value, 1))) { + ctx.error("Global state property must be a string literal.", 1); + return ParseResult(); + } + for (auto it = definitions.first; it != definitions.second; ++it) { const auto& signature = it->second(); diff --git a/src/mln/style/expression/expression.cpp b/src/mln/style/expression/expression.cpp index 91c177210aa4..5774899d554f 100644 --- a/src/mln/style/expression/expression.cpp +++ b/src/mln/style/expression/expression.cpp @@ -30,6 +30,8 @@ std::ostream& operator<<(std::ostream& os, const Dependency deps) { add(Dependency::Bind, "Bind"); add(Dependency::Var, "Var"); add(Dependency::Override, "Override"); + add(Dependency::Elevation, "Elevation"); + add(Dependency::GlobalState, "GlobalState"); return os; } diff --git a/src/mln/style/expression/is_constant.cpp b/src/mln/style/expression/is_constant.cpp index e322faae64f1..564b35057150 100644 --- a/src/mln/style/expression/is_constant.cpp +++ b/src/mln/style/expression/is_constant.cpp @@ -64,6 +64,15 @@ bool isRuntimeConstant(const Expression& expression) { return false; } + if (expression.getKind() == Kind::CompoundExpression) { + const auto* e = static_cast(&expression); + if (e->getOperator() == "global-state") { + // The result depends on the style's global state, which can be + // changed at runtime, so the expression is not runtime constant. + return false; + } + } + bool runtimeConstant = true; expression.eachChild([&](const Expression& e) { if (runtimeConstant && !isRuntimeConstant(e)) { diff --git a/src/mln/style/expression/parsing_context.cpp b/src/mln/style/expression/parsing_context.cpp index 8f315fbe8c26..fa9e95774966 100644 --- a/src/mln/style/expression/parsing_context.cpp +++ b/src/mln/style/expression/parsing_context.cpp @@ -44,8 +44,8 @@ namespace style { namespace expression { namespace { -const auto requiredProps = std::array{ - "zoom", "heatmap-density", "line-progress", "accumulated", "elevation"}; +const auto requiredProps = std::array{ + "zoom", "heatmap-density", "line-progress", "accumulated", "elevation", "global-state"}; bool isConstant(const Expression& expression) { const auto kind = expression.getKind(); diff --git a/src/mln/style/layer.cpp b/src/mln/style/layer.cpp index cec0388c4c94..156d3a7accab 100644 --- a/src/mln/style/layer.cpp +++ b/src/mln/style/layer.cpp @@ -1,12 +1,15 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #include namespace mln { @@ -82,7 +85,39 @@ VisibilityType Layer::getVisibility() const { } void Layer::setVisibility(VisibilityType value) { - if (value == getVisibility()) return; + if (value == getVisibility() && !baseImpl->visibilityExpression) return; + auto impl_ = mutableBaseImpl(); + impl_->visibility = value; + // Setting a constant visibility replaces any visibility expression. + impl_->visibilityExpression = nullptr; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); +} + +namespace { + +VisibilityType evaluateVisibilityExpression(const expression::Expression& expression, + const GlobalStateMap* globalState) { + const expression::EvaluationResult result = expression.evaluate( + expression::EvaluationContext().withGlobalState(globalState)); + if (result) { + if (const auto string = expression::fromExpressionValue(*result)) { + if (const auto visibility = Enum::toEnum(*string)) { + return *visibility; + } + } + } + Log::Warning(Event::ParseStyle, + "visibility expression did not evaluate to \"visible\" or \"none\"; defaulting to \"visible\""); + return VisibilityType::Visible; +} + +} // namespace + +void Layer::reevaluateVisibility(const GlobalStateMap& globalState) { + if (!baseImpl->visibilityExpression) return; + const VisibilityType value = evaluateVisibilityExpression(*baseImpl->visibilityExpression, &globalState); + if (value == baseImpl->visibility) return; auto impl_ = mutableBaseImpl(); impl_->visibility = value; baseImpl = std::move(impl_); @@ -140,7 +175,10 @@ Value Layer::serialize() const { result.emplace(std::make_pair("maxzoom", getMaxZoom())); } - if (getVisibility() == VisibilityType::None) { + if (baseImpl->visibilityExpression) { + result["layout"] = mapbox::base::ValueObject{ + std::make_pair("visibility", baseImpl->visibilityExpression->serialize())}; + } else if (getVisibility() == VisibilityType::None) { result["layout"] = mapbox::base::ValueObject{std::make_pair("visibility", "none")}; } @@ -221,6 +259,30 @@ std::optional Layer::setVisibility(const conversion::Converti return std::nullopt; } + if (isArray(value)) { + // Visibility given as an expression. Per the style specification such + // expressions are data-constant and may only use "global-state". + expression::ParsingContext ctx(expression::type::String); + expression::ParseResult parsed = ctx.parseExpression(value); + if (!parsed) { + return Error{ctx.getCombinedErrors()}; + } + constexpr auto allowed = expression::Dependency::GlobalState | expression::Dependency::Bind | + expression::Dependency::Var; + if (((*parsed)->dependencies | allowed) != allowed) { + return Error{"visibility expressions may only use the \"global-state\" expression"}; + } + + std::shared_ptr expression = std::move(*parsed); + const VisibilityType visibility = evaluateVisibilityExpression(*expression, nullptr); + auto impl_ = mutableBaseImpl(); + impl_->visibilityExpression = std::move(expression); + impl_->visibility = visibility; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); + return std::nullopt; + } + Error error; std::optional visibility = convert(value, error); if (!visibility) { diff --git a/src/mln/style/layer_impl.hpp b/src/mln/style/layer_impl.hpp index 0c2f43a21a24..5e614c686819 100644 --- a/src/mln/style/layer_impl.hpp +++ b/src/mln/style/layer_impl.hpp @@ -51,6 +51,23 @@ class Layer::Impl { /// Collect the style dependencies for this layer virtual expression::Dependency getDependencies() const noexcept { return expression::Dependency::None; } + /// Collect the dependencies of properties that affect tile layout: + /// the filter and the layout properties. Derived classes with layout + /// properties combine those with the filter dependencies from this base + /// implementation. + virtual expression::Dependency getLayoutDependencies() const noexcept { return getFilterDependencies(); } + + expression::Dependency getFilterDependencies() const noexcept { + return (filter.expression && *filter.expression) ? (**filter.expression).dependencies + : expression::Dependency::None; + } + + // Optional expression backing the "visibility" layout value. Per the + // style specification, such expressions may only depend on the global + // state; the evaluated result is kept in `visibility` and re-evaluated + // by the style whenever the global state changes. + std::shared_ptr visibilityExpression; + std::string id; std::string source; std::string sourceLayer; diff --git a/src/mln/style/layers/background_layer_properties.cpp b/src/mln/style/layers/background_layer_properties.cpp index a996950f4230..608ef02d590a 100644 --- a/src/mln/style/layers/background_layer_properties.cpp +++ b/src/mln/style/layers/background_layer_properties.cpp @@ -35,6 +35,10 @@ expression::Dependency BackgroundLayerProperties::getDependencies() const noexce return layerImpl().paint.getDependencies(); } +expression::Dependency BackgroundLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/background_layer_properties.hpp b/src/mln/style/layers/background_layer_properties.hpp index f6a18b0f9420..9b138337a3ba 100644 --- a/src/mln/style/layers/background_layer_properties.hpp +++ b/src/mln/style/layers/background_layer_properties.hpp @@ -47,6 +47,8 @@ class BackgroundLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const BackgroundLayer::Impl& layerImpl() const noexcept; // Data members. CrossfadeParameters crossfade; diff --git a/src/mln/style/layers/circle_layer_impl.hpp b/src/mln/style/layers/circle_layer_impl.hpp index f14a9a8ba521..8030b3324925 100644 --- a/src/mln/style/layers/circle_layer_impl.hpp +++ b/src/mln/style/layers/circle_layer_impl.hpp @@ -14,6 +14,10 @@ class CircleLayer::Impl : public Layer::Impl { bool hasLayoutDifference(const Layer::Impl&) const override; void stringifyLayout(rapidjson::Writer&) const override; + expression::Dependency getLayoutDependencies() const noexcept override { + return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); + } + CircleLayoutProperties::Unevaluated layout; CirclePaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/circle_layer_properties.cpp b/src/mln/style/layers/circle_layer_properties.cpp index 8ba147ba5da2..4d4c8b7c6578 100644 --- a/src/mln/style/layers/circle_layer_properties.cpp +++ b/src/mln/style/layers/circle_layer_properties.cpp @@ -33,6 +33,10 @@ expression::Dependency CircleLayerProperties::getDependencies() const noexcept { return layerImpl().paint.getDependencies() | layerImpl().layout.getDependencies(); } +expression::Dependency CircleLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/circle_layer_properties.hpp b/src/mln/style/layers/circle_layer_properties.hpp index 16462d34686f..43599b593e3c 100644 --- a/src/mln/style/layers/circle_layer_properties.hpp +++ b/src/mln/style/layers/circle_layer_properties.hpp @@ -95,6 +95,8 @@ class CircleLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const CircleLayer::Impl& layerImpl() const noexcept; // Data members. CirclePaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/color_relief_layer_properties.cpp b/src/mln/style/layers/color_relief_layer_properties.cpp index 1119d5e716ae..e7b1863abe53 100644 --- a/src/mln/style/layers/color_relief_layer_properties.cpp +++ b/src/mln/style/layers/color_relief_layer_properties.cpp @@ -33,6 +33,10 @@ expression::Dependency ColorReliefLayerProperties::getDependencies() const noexc return layerImpl().paint.getDependencies(); } +expression::Dependency ColorReliefLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/color_relief_layer_properties.hpp b/src/mln/style/layers/color_relief_layer_properties.hpp index d1875fa3e585..e7f9623964a7 100644 --- a/src/mln/style/layers/color_relief_layer_properties.hpp +++ b/src/mln/style/layers/color_relief_layer_properties.hpp @@ -40,6 +40,8 @@ class ColorReliefLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const ColorReliefLayer::Impl& layerImpl() const noexcept; // Data members. ColorReliefPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/fill_extrusion_layer_impl.hpp b/src/mln/style/layers/fill_extrusion_layer_impl.hpp index b377c176974b..6b69750364b9 100644 --- a/src/mln/style/layers/fill_extrusion_layer_impl.hpp +++ b/src/mln/style/layers/fill_extrusion_layer_impl.hpp @@ -14,6 +14,10 @@ class FillExtrusionLayer::Impl : public Layer::Impl { bool hasLayoutDifference(const Layer::Impl&) const override; void stringifyLayout(rapidjson::Writer&) const override; + expression::Dependency getLayoutDependencies() const noexcept override { + return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); + } + FillExtrusionLayoutProperties::Unevaluated layout; FillExtrusionPaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/fill_extrusion_layer_properties.cpp b/src/mln/style/layers/fill_extrusion_layer_properties.cpp index f9f06d91a308..90b1baab6a38 100644 --- a/src/mln/style/layers/fill_extrusion_layer_properties.cpp +++ b/src/mln/style/layers/fill_extrusion_layer_properties.cpp @@ -35,6 +35,10 @@ expression::Dependency FillExtrusionLayerProperties::getDependencies() const noe return layerImpl().paint.getDependencies() | layerImpl().layout.getDependencies(); } +expression::Dependency FillExtrusionLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/fill_extrusion_layer_properties.hpp b/src/mln/style/layers/fill_extrusion_layer_properties.hpp index 3ca9b9f4b421..a6e67f08ba16 100644 --- a/src/mln/style/layers/fill_extrusion_layer_properties.hpp +++ b/src/mln/style/layers/fill_extrusion_layer_properties.hpp @@ -81,6 +81,8 @@ class FillExtrusionLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const FillExtrusionLayer::Impl& layerImpl() const noexcept; // Data members. CrossfadeParameters crossfade; diff --git a/src/mln/style/layers/fill_layer_impl.hpp b/src/mln/style/layers/fill_layer_impl.hpp index 8af951b4ca9e..9438b74f0fdd 100644 --- a/src/mln/style/layers/fill_layer_impl.hpp +++ b/src/mln/style/layers/fill_layer_impl.hpp @@ -14,6 +14,10 @@ class FillLayer::Impl : public Layer::Impl { bool hasLayoutDifference(const Layer::Impl&) const override; void stringifyLayout(rapidjson::Writer&) const override; + expression::Dependency getLayoutDependencies() const noexcept override { + return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); + } + FillLayoutProperties::Unevaluated layout; FillPaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/fill_layer_properties.cpp b/src/mln/style/layers/fill_layer_properties.cpp index 3a76b70dd84f..f6c5dfbfc215 100644 --- a/src/mln/style/layers/fill_layer_properties.cpp +++ b/src/mln/style/layers/fill_layer_properties.cpp @@ -35,6 +35,10 @@ expression::Dependency FillLayerProperties::getDependencies() const noexcept { return layerImpl().paint.getDependencies() | layerImpl().layout.getDependencies(); } +expression::Dependency FillLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/fill_layer_properties.hpp b/src/mln/style/layers/fill_layer_properties.hpp index b249d08b6cac..b65f64f083eb 100644 --- a/src/mln/style/layers/fill_layer_properties.hpp +++ b/src/mln/style/layers/fill_layer_properties.hpp @@ -76,6 +76,8 @@ class FillLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const FillLayer::Impl& layerImpl() const noexcept; // Data members. CrossfadeParameters crossfade; diff --git a/src/mln/style/layers/heatmap_layer_properties.cpp b/src/mln/style/layers/heatmap_layer_properties.cpp index 8e23c46582ef..55b4d1be32ba 100644 --- a/src/mln/style/layers/heatmap_layer_properties.cpp +++ b/src/mln/style/layers/heatmap_layer_properties.cpp @@ -33,6 +33,10 @@ expression::Dependency HeatmapLayerProperties::getDependencies() const noexcept return layerImpl().paint.getDependencies(); } +expression::Dependency HeatmapLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/heatmap_layer_properties.hpp b/src/mln/style/layers/heatmap_layer_properties.hpp index 96d313ca882c..366ee7fb3865 100644 --- a/src/mln/style/layers/heatmap_layer_properties.hpp +++ b/src/mln/style/layers/heatmap_layer_properties.hpp @@ -55,6 +55,8 @@ class HeatmapLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const HeatmapLayer::Impl& layerImpl() const noexcept; // Data members. HeatmapPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/hillshade_layer_properties.cpp b/src/mln/style/layers/hillshade_layer_properties.cpp index 9cbcf5b0d0ff..601d0429cadb 100644 --- a/src/mln/style/layers/hillshade_layer_properties.cpp +++ b/src/mln/style/layers/hillshade_layer_properties.cpp @@ -33,6 +33,10 @@ expression::Dependency HillshadeLayerProperties::getDependencies() const noexcep return layerImpl().paint.getDependencies(); } +expression::Dependency HillshadeLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/hillshade_layer_properties.hpp b/src/mln/style/layers/hillshade_layer_properties.hpp index ca0c0ad4d8a7..aba4e30599a2 100644 --- a/src/mln/style/layers/hillshade_layer_properties.hpp +++ b/src/mln/style/layers/hillshade_layer_properties.hpp @@ -71,6 +71,8 @@ class HillshadeLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const HillshadeLayer::Impl& layerImpl() const noexcept; // Data members. HillshadePaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/layer_properties.cpp.ejs b/src/mln/style/layers/layer_properties.cpp.ejs index 1c1d3cbff729..642aa0ccdf58 100644 --- a/src/mln/style/layers/layer_properties.cpp.ejs +++ b/src/mln/style/layers/layer_properties.cpp.ejs @@ -44,6 +44,10 @@ expression::Dependency <%- camelize(type) %>LayerProperties::getDependencies() c return layerImpl().paint.getDependencies()<% if (layoutProperties.length) { -%> | layerImpl().layout.getDependencies()<% } -%>; } +expression::Dependency <%- camelize(type) %>LayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/layer_properties.hpp.ejs b/src/mln/style/layers/layer_properties.hpp.ejs index 775a11ed50af..ea3c7e5164fe 100644 --- a/src/mln/style/layers/layer_properties.hpp.ejs +++ b/src/mln/style/layers/layer_properties.hpp.ejs @@ -78,6 +78,8 @@ public: expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const <%- camelize(type) %>Layer::Impl& layerImpl() const noexcept; // Data members. <% if (type === 'background' || type === 'fill' || type === 'line' || type === 'fill-extrusion') { -%> diff --git a/src/mln/style/layers/line_layer_impl.hpp b/src/mln/style/layers/line_layer_impl.hpp index a742fc81ea0a..789a8e5e5ea9 100644 --- a/src/mln/style/layers/line_layer_impl.hpp +++ b/src/mln/style/layers/line_layer_impl.hpp @@ -18,6 +18,10 @@ class LineLayer::Impl : public Layer::Impl { return layout.getDependencies() | paint.getDependencies(); } + expression::Dependency getLayoutDependencies() const noexcept override { + return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); + } + LineLayoutProperties::Unevaluated layout; LinePaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/line_layer_properties.cpp b/src/mln/style/layers/line_layer_properties.cpp index 20a53108911b..ede0e11e67bf 100644 --- a/src/mln/style/layers/line_layer_properties.cpp +++ b/src/mln/style/layers/line_layer_properties.cpp @@ -35,6 +35,10 @@ expression::Dependency LineLayerProperties::getDependencies() const noexcept { return layerImpl().paint.getDependencies() | layerImpl().layout.getDependencies(); } +expression::Dependency LineLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/line_layer_properties.hpp b/src/mln/style/layers/line_layer_properties.hpp index 406f4ef54ee0..fc9e0d3e48f8 100644 --- a/src/mln/style/layers/line_layer_properties.hpp +++ b/src/mln/style/layers/line_layer_properties.hpp @@ -125,6 +125,8 @@ class LineLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const LineLayer::Impl& layerImpl() const noexcept; // Data members. CrossfadeParameters crossfade; diff --git a/src/mln/style/layers/location_indicator_layer_impl.hpp b/src/mln/style/layers/location_indicator_layer_impl.hpp index 924d6e53b50a..7ea3d1be9dca 100644 --- a/src/mln/style/layers/location_indicator_layer_impl.hpp +++ b/src/mln/style/layers/location_indicator_layer_impl.hpp @@ -25,6 +25,10 @@ class LocationIndicatorLayer::Impl : public Layer::Impl { bool hasLayoutDifference(const Layer::Impl &) const override; void stringifyLayout(rapidjson::Writer &) const override; + expression::Dependency getLayoutDependencies() const noexcept override { + return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); + } + LocationIndicatorLayoutProperties::Unevaluated layout; LocationIndicatorPaintProperties::Transitionable paint; DECLARE_LAYER_TYPE_INFO; diff --git a/src/mln/style/layers/location_indicator_layer_properties.cpp b/src/mln/style/layers/location_indicator_layer_properties.cpp index 561c17d96657..99ef5fb17f27 100644 --- a/src/mln/style/layers/location_indicator_layer_properties.cpp +++ b/src/mln/style/layers/location_indicator_layer_properties.cpp @@ -33,6 +33,10 @@ expression::Dependency LocationIndicatorLayerProperties::getDependencies() const return layerImpl().paint.getDependencies() | layerImpl().layout.getDependencies(); } +expression::Dependency LocationIndicatorLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/location_indicator_layer_properties.hpp b/src/mln/style/layers/location_indicator_layer_properties.hpp index b0bd1cfd892d..312d477ae30b 100644 --- a/src/mln/style/layers/location_indicator_layer_properties.hpp +++ b/src/mln/style/layers/location_indicator_layer_properties.hpp @@ -102,6 +102,8 @@ class LocationIndicatorLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const LocationIndicatorLayer::Impl& layerImpl() const noexcept; // Data members. LocationIndicatorPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/raster_layer_properties.cpp b/src/mln/style/layers/raster_layer_properties.cpp index a5ede5d380d9..2cb993bb7103 100644 --- a/src/mln/style/layers/raster_layer_properties.cpp +++ b/src/mln/style/layers/raster_layer_properties.cpp @@ -33,6 +33,10 @@ expression::Dependency RasterLayerProperties::getDependencies() const noexcept { return layerImpl().paint.getDependencies(); } +expression::Dependency RasterLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/raster_layer_properties.hpp b/src/mln/style/layers/raster_layer_properties.hpp index 63f482c94c60..bf159cfadfbb 100644 --- a/src/mln/style/layers/raster_layer_properties.hpp +++ b/src/mln/style/layers/raster_layer_properties.hpp @@ -71,6 +71,8 @@ class RasterLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const RasterLayer::Impl& layerImpl() const noexcept; // Data members. RasterPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/symbol_layer_impl.hpp b/src/mln/style/layers/symbol_layer_impl.hpp index fef8cc1c755a..9058f56f7cf8 100644 --- a/src/mln/style/layers/symbol_layer_impl.hpp +++ b/src/mln/style/layers/symbol_layer_impl.hpp @@ -129,6 +129,10 @@ class SymbolLayer::Impl : public Layer::Impl { void stringifyLayout(rapidjson::Writer&) const override; void populateFontStack(std::set& fontStack) const final; + expression::Dependency getLayoutDependencies() const noexcept override { + return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); + } + SymbolLayoutProperties::Unevaluated layout; SymbolPaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/symbol_layer_properties.cpp b/src/mln/style/layers/symbol_layer_properties.cpp index 7b6d3df12bda..d3e8c5a4bcaf 100644 --- a/src/mln/style/layers/symbol_layer_properties.cpp +++ b/src/mln/style/layers/symbol_layer_properties.cpp @@ -33,6 +33,10 @@ expression::Dependency SymbolLayerProperties::getDependencies() const noexcept { return layerImpl().paint.getDependencies() | layerImpl().layout.getDependencies(); } +expression::Dependency SymbolLayerProperties::getEvaluatedDependencies() const noexcept { + return evaluated.getDependencies(); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/symbol_layer_properties.hpp b/src/mln/style/layers/symbol_layer_properties.hpp index 83d2dea56ba1..bebf735029a3 100644 --- a/src/mln/style/layers/symbol_layer_properties.hpp +++ b/src/mln/style/layers/symbol_layer_properties.hpp @@ -365,6 +365,8 @@ class SymbolLayerProperties final : public LayerProperties { expression::Dependency getDependencies() const noexcept override; + expression::Dependency getEvaluatedDependencies() const noexcept override; + const SymbolLayer::Impl& layerImpl() const noexcept; // Data members. SymbolPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/parser.cpp b/src/mln/style/parser.cpp index b4c9891e6bc3..dd9f664f6a63 100644 --- a/src/mln/style/parser.cpp +++ b/src/mln/style/parser.cpp @@ -112,6 +112,10 @@ StyleParseResult Parser::parse(const std::string& json) { parseLight(document["light"]); } + if (document.HasMember("state")) { + parseState(document["state"]); + } + if (document.HasMember("sources")) { parseSources(document["sources"]); } @@ -236,6 +240,66 @@ void Parser::parseLight(const JSValue& value) { light = *converted; } +namespace { + +// Deep conversion of a style JSON value to an mln::Value (unlike +// conversion::toValue, which only handles scalar values). +mln::Value parseStateValue(const JSValue& value) { + switch (value.GetType()) { + case rapidjson::kNullType: + return NullValue(); + case rapidjson::kFalseType: + return false; + case rapidjson::kTrueType: + return true; + case rapidjson::kStringType: + return std::string{value.GetString(), value.GetStringLength()}; + case rapidjson::kNumberType: + if (value.IsUint64()) return value.GetUint64(); + if (value.IsInt64()) return value.GetInt64(); + return value.GetDouble(); + case rapidjson::kArrayType: { + mapbox::base::ValueArray result; + result.reserve(value.Size()); + for (const auto& element : value.GetArray()) { + result.emplace_back(parseStateValue(element)); + } + return result; + } + case rapidjson::kObjectType: { + mapbox::base::ValueObject result; + for (const auto& member : value.GetObject()) { + result.emplace(std::string{member.name.GetString(), member.name.GetStringLength()}, + parseStateValue(member.value)); + } + return result; + } + } + return NullValue(); +} + +} // namespace + +void Parser::parseState(const JSValue& value) { + if (!value.IsObject()) { + Log::Warning(Event::ParseStyle, "state must be an object"); + return; + } + + for (const auto& member : value.GetObject()) { + std::string propertyName{member.name.GetString(), member.name.GetStringLength()}; + if (!member.value.IsObject()) { + Log::Warning(Event::ParseStyle, "state property \"" + propertyName + "\" must be an object"); + continue; + } + mln::Value defaultValue; + if (member.value.HasMember("default")) { + defaultValue = parseStateValue(member.value["default"]); + } + globalStateDefaults.emplace(std::move(propertyName), std::move(defaultValue)); + } +} + void Parser::parseSources(const JSValue& value) { if (!value.IsObject()) { Log::Warning(Event::ParseStyle, "sources must be an object"); diff --git a/src/mln/style/parser.hpp b/src/mln/style/parser.hpp index bae2a43f4a93..d78c0e8bda1e 100644 --- a/src/mln/style/parser.hpp +++ b/src/mln/style/parser.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,10 @@ class Parser { TransitionOptions transition{{util::DEFAULT_TRANSITION_DURATION}}; Light light; + // Default values of the root "state" property, keyed by state property + // name, used by the "global-state" expression. + GlobalStateMap globalStateDefaults; + std::string name; LatLng latLng; double zoom = 0; @@ -54,6 +59,7 @@ class Parser { private: void parseTransition(const JSValue&); void parseLight(const JSValue&); + void parseState(const JSValue&); void parseSources(const JSValue&); void parseSprites(const JSValue&); void parseLayers(const JSValue&); diff --git a/src/mln/style/properties.hpp b/src/mln/style/properties.hpp index 39da5fe41c27..e18d7af499e6 100644 --- a/src/mln/style/properties.hpp +++ b/src/mln/style/properties.hpp @@ -311,7 +311,9 @@ class Properties { using Evaluator = typename P::EvaluatorType; const auto& property = this->template get

(); const bool needEvaluate = parameters.layerChanged || parameters.hasCrossfade || property.hasTransition() || - (parameters.zoomChanged && (getDependencies(property) & Dependency::Zoom)); + (parameters.zoomChanged && (getDependencies(property) & Dependency::Zoom)) || + (parameters.globalStateChanged && + (getDependencies(property) & Dependency::GlobalState)); return needEvaluate ? property.evaluate(Evaluator(parameters, P::defaultValue()), parameters.now) : oldResult; } diff --git a/src/mln/style/property_expression.cpp b/src/mln/style/property_expression.cpp index a158e93bbe54..a2a953b5c4e0 100644 --- a/src/mln/style/property_expression.cpp +++ b/src/mln/style/property_expression.cpp @@ -23,7 +23,7 @@ PropertyExpressionBase::PropertyExpressionBase(std::unique_ptrhas(Dependency::Zoom)), isFeatureConstant_(!expression->has(Dependency::Feature)), - isRuntimeConstant_(!expression->has(Dependency::Image)), + isRuntimeConstant_(!expression->has(Dependency::Image | Dependency::GlobalState)), isGPUCapable_(checkGPUCapable(*expression, zoomCurve)) { assert(isZoomConstant_ == expression::isZoomConstant(*expression)); assert(isFeatureConstant_ == expression::isFeatureConstant(*expression)); @@ -32,6 +32,7 @@ PropertyExpressionBase::PropertyExpressionBase(std::unique_ptrgetLight(); } +GlobalStateMap Style::getGlobalState() const { + return impl->getGlobalState(); +} + +void Style::setGlobalStateProperty(const std::string& name, const Value& value) { + impl->mutated = true; + impl->setGlobalStateProperty(name, value); +} + const Light* Style::getLight() const { return impl->getLight(); } diff --git a/src/mln/style/style_impl.cpp b/src/mln/style/style_impl.cpp index e5deefdaacfe..16f6076e3583 100644 --- a/src/mln/style/style_impl.cpp +++ b/src/mln/style/style_impl.cpp @@ -23,6 +23,8 @@ #include #include #include + +#include #include namespace mln { @@ -105,6 +107,9 @@ void Style::Impl::parse(const std::string& json_) { transitionOptions = parser.transition; + globalStateDefaults = parser.globalStateDefaults; + globalState = std::make_shared(globalStateDefaults); + for (auto& source : parser.sources) { addSource(std::move(source)); } @@ -158,6 +163,85 @@ void Style::Impl::setTransitionOptions(const TransitionOptions& options) { transitionOptions = options; } +namespace { + +// Equality on state values that, unlike the exact-type variant equality, +// treats numerically equal numbers of different arithmetic types as equal +// (e.g. a style default parsed as an integer vs. a runtime-set double). +bool stateValuesEqual(const Value& a, const Value& b) { + if (a.is() || a.is() || a.is()) { + if (!(b.is() || b.is() || b.is())) { + return false; + } + const auto toDouble = [](const Value& v) { + if (v.is()) return static_cast(v.get()); + if (v.is()) return static_cast(v.get()); + return v.get(); + }; + return toDouble(a) == toDouble(b); + } + if (a.is() && b.is()) { + const auto& arrayA = a.get(); + const auto& arrayB = b.get(); + return std::equal(arrayA.begin(), arrayA.end(), arrayB.begin(), arrayB.end(), stateValuesEqual); + } + if (a.is() && b.is()) { + const auto& objectA = a.get(); + const auto& objectB = b.get(); + if (objectA.size() != objectB.size()) { + return false; + } + return std::all_of(objectA.begin(), objectA.end(), [&](const auto& entry) { + const auto it = objectB.find(entry.first); + return it != objectB.end() && stateValuesEqual(entry.second, it->second); + }); + } + return a == b; +} + +} // namespace + +void Style::Impl::setGlobalStateProperty(const std::string& property, const Value& value) { + // A null value resets the property to the default defined in the style's + // root "state" property, if any. + const Value* newValue = &value; + if (value.is()) { + const auto def = globalStateDefaults.find(property); + if (def != globalStateDefaults.end()) { + newValue = &def->second; + } + } + + const auto current = globalState->find(property); + // A missing property already evaluates to null, so setting an absent + // property to null is a no-op as well. + const bool unchanged = current != globalState->end() ? stateValuesEqual(current->second, *newValue) + : newValue->is(); + if (unchanged) { + return; + } + + auto newState = std::make_shared(*globalState); + (*newState)[property] = *newValue; + globalState = std::move(newState); + reevaluateLayerVisibilities(); + observer->onUpdate(); +} + +void Style::Impl::reevaluateLayerVisibilities() { + for (const auto& layer : layers) { + layer->reevaluateVisibility(*globalState); + } +} + +GlobalStateMap Style::Impl::getGlobalState() const { + return *globalState; +} + +std::shared_ptr Style::Impl::getGlobalStateShared() const { + return globalState; +} + TransitionOptions Style::Impl::getTransitionOptions() const { return transitionOptions; } @@ -224,6 +308,9 @@ Layer* Style::Impl::addLayer(std::unique_ptr layer, const std::optionalsetObserver(this); Layer* result = layers.add(std::move(layer), before); + // Resolve any "global-state" backed visibility expression against the + // current global state (during parsing it could not be applied yet). + result->reevaluateVisibility(*globalState); observer->onUpdate(); return result; diff --git a/src/mln/style/style_impl.hpp b/src/mln/style/style_impl.hpp index a69926379cef..bf4039dd55f8 100644 --- a/src/mln/style/style_impl.hpp +++ b/src/mln/style/style_impl.hpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -85,6 +86,16 @@ class Style::Impl : public SpriteLoaderObserver, void setLight(std::unique_ptr); Light* getLight() const; + /// Set a global state property. A null value resets the property to the + /// default defined in the style's root "state" property (or null). + void setGlobalStateProperty(const std::string& property, const Value& value); + GlobalStateMap getGlobalState() const; + std::shared_ptr getGlobalStateShared() const; + + /// Re-evaluate the visibility expressions of all layers against the + /// current global state. + void reevaluateLayerVisibilities(); + std::optional> getImage(const std::string&) const; void addImage(std::unique_ptr); void removeImage(const std::string&); @@ -123,6 +134,12 @@ class Style::Impl : public SpriteLoaderObserver, std::unique_ptr light; std::unordered_map spritesLoadingStatus; + // Global state for the "global-state" expression. The map itself is + // immutable and replaced wholesale on change so that it can be safely + // shared across threads. + std::shared_ptr globalState = std::make_shared(); + GlobalStateMap globalStateDefaults; + // Defaults std::string name; CameraOptions defaultCamera; diff --git a/src/mln/tile/custom_geometry_tile.cpp b/src/mln/tile/custom_geometry_tile.cpp index 377f43c9af0d..5ad61f68dc3f 100644 --- a/src/mln/tile/custom_geometry_tile.cpp +++ b/src/mln/tile/custom_geometry_tile.cpp @@ -79,8 +79,10 @@ void CustomGeometryTile::querySourceFeatures(std::vector& result, const auto feature = layer->getFeature(i); // Apply filter, if any - if (queryOptions.filter && !(*queryOptions.filter)(style::expression::EvaluationContext{ - static_cast(id.overscaledZ), feature.get()})) { + if (queryOptions.filter && + !(*queryOptions.filter)( + style::expression::EvaluationContext{static_cast(id.overscaledZ), feature.get()} + .withGlobalState(queryOptions.globalState.get()))) { continue; } diff --git a/src/mln/tile/geojson_tile.cpp b/src/mln/tile/geojson_tile.cpp index bb1461489ffe..050794074b9a 100644 --- a/src/mln/tile/geojson_tile.cpp +++ b/src/mln/tile/geojson_tile.cpp @@ -47,8 +47,10 @@ void GeoJSONTile::querySourceFeatures(std::vector& result, const Source auto feature = layer->getFeature(i); // Apply filter, if any - if (options.filter && !(*options.filter)(style::expression::EvaluationContext{ - static_cast(this->id.overscaledZ), feature.get()})) { + if (options.filter && + !(*options.filter)( + style::expression::EvaluationContext{static_cast(this->id.overscaledZ), feature.get()} + .withGlobalState(options.globalState.get()))) { continue; } diff --git a/src/mln/tile/geometry_tile.cpp b/src/mln/tile/geometry_tile.cpp index 6de108030369..fbe5c1b7f66f 100644 --- a/src/mln/tile/geometry_tile.cpp +++ b/src/mln/tile/geometry_tile.cpp @@ -192,6 +192,7 @@ GeometryTile::GeometryTile(const OverscaledTileID& id_, TileObserver* observer_) : Tile(Kind::Geometry, id_, std::move(sourceID_), observer_), ImageRequestor(parameters.imageManager), + globalState(parameters.globalState), threadPool(parameters.threadPool), mailbox(std::make_shared(*Scheduler::GetCurrent())), worker(parameters.isUpdateSynchronous, @@ -322,8 +323,15 @@ void GeometryTile::setLayers(const std::vector>& laye } ++correlationID; - worker.self().invoke( - &GeometryTileWorker::setLayers, std::move(impls), imageManager->getAvailableImages(), correlationID); + worker.self().invoke(&GeometryTileWorker::setLayers, + std::move(impls), + globalState, + imageManager->getAvailableImages(), + correlationID); +} + +void GeometryTile::setGlobalState(const std::shared_ptr& globalState_) { + globalState = globalState_; } void GeometryTile::setShowCollisionBoxes(const bool showCollisionBoxes_) { @@ -559,8 +567,10 @@ void GeometryTile::querySourceFeatures(std::vector& result, const Sourc auto feature = layer->getFeature(i); // Apply filter, if any - if (options.filter && !(*options.filter)(style::expression::EvaluationContext{ - static_cast(this->id.overscaledZ), feature.get()})) { + if (options.filter && + !(*options.filter)( + style::expression::EvaluationContext{static_cast(this->id.overscaledZ), feature.get()} + .withGlobalState(options.globalState.get()))) { continue; } diff --git a/src/mln/tile/geometry_tile.hpp b/src/mln/tile/geometry_tile.hpp index 92ada761ae86..1c04ae0c8285 100644 --- a/src/mln/tile/geometry_tile.hpp +++ b/src/mln/tile/geometry_tile.hpp @@ -42,6 +42,7 @@ class GeometryTile : public Tile, public GlyphRequestor, public ImageRequestor { std::unique_ptr createRenderData() override; void setLayers(const std::vector>&) override; + void setGlobalState(const std::shared_ptr&) override; void setShowCollisionBoxes(bool showCollisionBoxes) override; void onGlyphsAvailable(GlyphMap, HBShapeRequests) override; @@ -110,6 +111,9 @@ class GeometryTile : public Tile, public GlyphRequestor, public ImageRequestor { // Used to signal the worker that it should abandon parsing this tile as soon as possible. std::atomic obsolete{false}; + // The style's global state, forwarded to the worker for use during layout. + std::shared_ptr globalState; + private: void markObsolete(); diff --git a/src/mln/tile/geometry_tile_worker.cpp b/src/mln/tile/geometry_tile_worker.cpp index 459142dd2864..63ebbfa9a476 100644 --- a/src/mln/tile/geometry_tile_worker.cpp +++ b/src/mln/tile/geometry_tile_worker.cpp @@ -161,12 +161,14 @@ void GeometryTileWorker::setData(std::unique_ptr data_, } void GeometryTileWorker::setLayers(std::vector> layers_, + std::shared_ptr globalState_, std::set availableImages_, uint64_t correlationID_) { MLN_TRACE_FUNC(); try { layers = std::move(layers_); + globalState = std::move(globalState_); correlationID = correlationID_; availableImages = std::move(availableImages_); @@ -456,8 +458,11 @@ void GeometryTileWorker::parse() { } const style::Layer::Impl& leaderImpl = *(group.at(0)->baseImpl); - BucketParameters parameters{ - .tileID = id, .mode = mode, .pixelRatio = pixelRatio, .layerType = leaderImpl.getTypeInfo()}; + BucketParameters parameters{.tileID = id, + .mode = mode, + .pixelRatio = pixelRatio, + .layerType = leaderImpl.getTypeInfo(), + .globalState = globalState}; auto geometryLayer = (*data)->getLayer(leaderImpl.sourceLayer); if (!geometryLayer) { @@ -500,7 +505,8 @@ void GeometryTileWorker::parse() { std::unique_ptr feature = geometryLayer->getFeature(i); if (!filter(expression::EvaluationContext(static_cast(this->id.overscaledZ), feature.get()) - .withCanonicalTileID(&id.canonical))) + .withCanonicalTileID(&id.canonical) + .withGlobalState(globalState.get()))) continue; const GeometryCollection& geometries = feature->getGeometries(); diff --git a/src/mln/tile/geometry_tile_worker.hpp b/src/mln/tile/geometry_tile_worker.hpp index 67172512e875..95d4110b5783 100644 --- a/src/mln/tile/geometry_tile_worker.hpp +++ b/src/mln/tile/geometry_tile_worker.hpp @@ -51,6 +51,7 @@ class GeometryTileWorker { void setObserver(TileObserver* observer); void setLayers(std::vector>, + std::shared_ptr globalState_, std::set availableImages, uint64_t correlationID); void setData(std::unique_ptr, @@ -110,6 +111,9 @@ class GeometryTileWorker { std::optional>> layers; std::optional> data; + // The style's global state, used by "global-state" expressions during layout. + std::shared_ptr globalState; + std::vector> layouts; GlyphDependencies pendingGlyphDependencies; diff --git a/src/mln/tile/tile.hpp b/src/mln/tile/tile.hpp index 13615273b175..6f7337d7e7d8 100644 --- a/src/mln/tile/tile.hpp +++ b/src/mln/tile/tile.hpp @@ -83,6 +83,11 @@ class Tile : public TileLoaderObserver { virtual void setLayers(const std::vector>&) {} virtual void setMask(TileMask&&) {} + // Notifies this tile of the style's current global state, used to + // evaluate "global-state" expressions during layout. Must be called + // before setLayers() to take effect for the subsequent re-layout. + virtual void setGlobalState(const std::shared_ptr&) {} + virtual void queryRenderedFeatures(std::unordered_map>& result, const GeometryCoordinates& queryGeometry, const TransformState&, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 188e9eb407d1..b1d23806d3f6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -58,6 +58,7 @@ add_library( ${PROJECT_SOURCE_DIR}/test/style/conversion/tileset.test.cpp ${PROJECT_SOURCE_DIR}/test/style/expression/dependency.test.cpp ${PROJECT_SOURCE_DIR}/test/style/expression/expression.test.cpp + ${PROJECT_SOURCE_DIR}/test/style/expression/global_state.test.cpp ${PROJECT_SOURCE_DIR}/test/style/expression/util.test.cpp ${PROJECT_SOURCE_DIR}/test/style/expression/utf8_op_helpers.test.cpp ${PROJECT_SOURCE_DIR}/test/style/filter.test.cpp diff --git a/test/fixtures/style_parser/expressions.info.json b/test/fixtures/style_parser/expressions.info.json index af8a6053e070..a37cc2a9e497 100644 --- a/test/fixtures/style_parser/expressions.info.json +++ b/test/fixtures/style_parser/expressions.info.json @@ -4,7 +4,9 @@ [1, "WARNING", "ParseStyle", "Expected color but found number instead."], [1, "WARNING", "ParseStyle", "Expected arguments of type (number, number, number, number), but found (number, string, number, number) instead."], [1, "WARNING", "ParseStyle", "\"zoom\" expression may only be used as input to a top-level \"step\" or \"interpolate\" expression."], - [3, "WARNING", "ParseStyle", "value must be a string"], + [1, "WARNING", "ParseStyle", "value must be a string"], + [1, "WARNING", "ParseStyle", "Expected string but found boolean instead."], + [1, "WARNING", "ParseStyle", "[0]: Expression name must be a string, but found number instead. If you wanted a literal array, use [\"literal\", [...]]."], [1, "WARNING", "ParseStyle", "data expressions not supported"], [1, "WARNING", "ParseStyle", "Type array is not interpolatable."] ] diff --git a/test/style/expression/global_state.test.cpp b/test/style/expression/global_state.test.cpp new file mode 100644 index 000000000000..b87465faa21c --- /dev/null +++ b/test/style/expression/global_state.test.cpp @@ -0,0 +1,247 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace mln; +using namespace mln::style; +using namespace mln::style::expression; + +using ExprValue = mln::style::expression::Value; + +namespace { + +std::unique_ptr parseExpression(const std::string& json, std::optional expected = {}) { + rapidjson::GenericDocument, rapidjson::CrtAllocator> document; + document.Parse<0>(json.c_str()); + assert(!document.HasParseError()); + const JSValue* expression = &document; + ParsingContext ctx = expected ? ParsingContext(*expected) : ParsingContext(); + ParseResult parsed = ctx.parseExpression(conversion::Convertible(expression)); + return parsed ? std::move(*parsed) : nullptr; +} + +} // namespace + +TEST(GlobalStateExpression, Parse) { + auto expr = parseExpression(R"(["global-state", "showLabels"])"); + ASSERT_TRUE(expr); + EXPECT_EQ(Kind::CompoundExpression, expr->getKind()); + EXPECT_EQ("global-state", expr->getOperator()); + EXPECT_TRUE(expr->getType().is()); + EXPECT_TRUE(expr->dependencies & Dependency::GlobalState); +} + +TEST(GlobalStateExpression, ParseErrors) { + // Missing argument. + EXPECT_FALSE(parseExpression(R"(["global-state"])")); + // Too many arguments. + EXPECT_FALSE(parseExpression(R"(["global-state", "a", "b"])")); + // Non-string argument. + EXPECT_FALSE(parseExpression(R"(["global-state", 1])")); + // The property name must be a string literal, not a sub-expression. + EXPECT_FALSE(parseExpression(R"(["global-state", ["get", "key"]])")); + EXPECT_FALSE(parseExpression(R"(["global-state", ["concat", "a", "b"]])")); + EXPECT_FALSE(parseExpression(R"(["global-state", ["literal", "a"]])")); +} + +TEST(GlobalStateExpression, IsNotConstant) { + auto expr = parseExpression(R"(["global-state", "showLabels"])"); + ASSERT_TRUE(expr); + // A global-state expression with constant arguments must not be folded + // into a literal at parse time. + EXPECT_NE(Kind::Literal, expr->getKind()); + EXPECT_TRUE(isFeatureConstant(*expr)); + EXPECT_TRUE(isZoomConstant(*expr)); + EXPECT_FALSE(isRuntimeConstant(*expr)); +} + +TEST(GlobalStateExpression, Evaluate) { + auto expr = parseExpression(R"(["global-state", "size"])"); + ASSERT_TRUE(expr); + + GlobalStateMap state{{"size", 12.0}}; + + EvaluationContext context; + context.globalState = &state; + auto result = expr->evaluate(context); + ASSERT_TRUE(result); + EXPECT_EQ(ExprValue(12.0), *result); + + // Missing property evaluates to null. + auto missing = parseExpression(R"(["global-state", "missing"])"); + ASSERT_TRUE(missing); + result = missing->evaluate(context); + ASSERT_TRUE(result); + EXPECT_TRUE(result->is()); + + // No global state provided evaluates to null. + result = expr->evaluate(EvaluationContext()); + ASSERT_TRUE(result); + EXPECT_TRUE(result->is()); +} + +TEST(GlobalStateExpression, EvaluateArray) { + auto expr = parseExpression(R"(["global-state", "categories"])"); + ASSERT_TRUE(expr); + + GlobalStateMap state{{"categories", mapbox::base::ValueArray{"restaurant", "hotel"}}}; + + EvaluationContext context; + context.globalState = &state; + auto result = expr->evaluate(context); + ASSERT_TRUE(result); + ASSERT_TRUE(result->is>()); + const auto& array = result->get>(); + ASSERT_EQ(2u, array.size()); + EXPECT_EQ(ExprValue(std::string("restaurant")), array[0]); + EXPECT_EQ(ExprValue(std::string("hotel")), array[1]); +} + +TEST(GlobalStateExpression, EvaluateInComposition) { + auto expr = parseExpression(R"(["case", ["to-boolean", ["global-state", "enabled"]], 10.0, 5.0])", + {type::Number}); + ASSERT_TRUE(expr); + EXPECT_TRUE(expr->dependencies & Dependency::GlobalState); + + GlobalStateMap enabled{{"enabled", true}}; + GlobalStateMap disabled{{"enabled", false}}; + + EvaluationContext context; + context.globalState = &enabled; + auto result = expr->evaluate(context); + ASSERT_TRUE(result); + EXPECT_EQ(ExprValue(10.0), *result); + + context.globalState = &disabled; + result = expr->evaluate(context); + ASSERT_TRUE(result); + EXPECT_EQ(ExprValue(5.0), *result); + + // Without state, "enabled" is null -> false. + result = expr->evaluate(EvaluationContext()); + ASSERT_TRUE(result); + EXPECT_EQ(ExprValue(5.0), *result); +} + +TEST(GlobalStateExpression, PropertyExpressionCapturesGlobalState) { + auto expr = parseExpression(R"(["to-number", ["global-state", "width"]])", {type::Number}); + ASSERT_TRUE(expr); + + PropertyExpression propertyExpression(std::move(expr)); + EXPECT_TRUE(propertyExpression.isFeatureConstant()); + EXPECT_TRUE(propertyExpression.isZoomConstant()); + EXPECT_FALSE(propertyExpression.isRuntimeConstant()); + + // Without any state, the expression falls back to the default. + EXPECT_EQ(0.0f, propertyExpression.evaluate(EvaluationContext(0.0f), 0.0f)); + + auto state = std::make_shared(GlobalStateMap{{"width", 3.0}}); + + // State provided via the evaluation context. + EXPECT_EQ(3.0f, + propertyExpression.evaluate(EvaluationContext(0.0f).withGlobalState(state.get()), 0.0f)); + + // Captured state is used when the context has none. + propertyExpression.captureGlobalState(state); + EXPECT_EQ(3.0f, propertyExpression.evaluate(EvaluationContext(0.0f), 0.0f)); + + // An explicitly provided state takes precedence over the captured one. + GlobalStateMap other{{"width", 7.0}}; + EXPECT_EQ(7.0f, propertyExpression.evaluate(EvaluationContext(0.0f).withGlobalState(&other), 0.0f)); +} + +TEST(GlobalStateExpression, PropertyValueConversionKeepsExpression) { + rapidjson::GenericDocument, rapidjson::CrtAllocator> document; + document.Parse<0>(R"(["to-number", ["global-state", "width"]])"); + ASSERT_FALSE(document.HasParseError()); + const JSValue* json = &document; + + conversion::Error error; + auto converted = conversion::convert>( + conversion::Convertible(json), error, /*allowDataExpressions*/ true, /*convertTokens*/ false); + ASSERT_TRUE(converted); + // The property value must keep the expression rather than folding it into + // a constant, so that changes to the global state can be picked up. + EXPECT_TRUE(converted->isExpression()); + EXPECT_TRUE(converted->getDependencies() & Dependency::GlobalState); +} + +TEST(GlobalStateExpression, Filter) { + rapidjson::GenericDocument, rapidjson::CrtAllocator> document; + document.Parse<0>(R"(["to-boolean", ["global-state", "showFeatures"]])"); + ASSERT_FALSE(document.HasParseError()); + const JSValue* json = &document; + + conversion::Error error; + auto filter = conversion::convert(conversion::Convertible(json), error); + ASSERT_TRUE(filter); + + GlobalStateMap shown{{"showFeatures", true}}; + GlobalStateMap hidden{{"showFeatures", false}}; + + EXPECT_TRUE((*filter)(EvaluationContext().withGlobalState(&shown))); + EXPECT_FALSE((*filter)(EvaluationContext().withGlobalState(&hidden))); + EXPECT_FALSE((*filter)(EvaluationContext())); +} + +TEST(GlobalStateExpression, ColorRamp) { + rapidjson::GenericDocument, rapidjson::CrtAllocator> document; + document.Parse<0>(R"(["to-color", ["global-state", "hotColor"]])"); + ASSERT_FALSE(document.HasParseError()); + const JSValue* json = &document; + + ParsingContext ctx(type::Color); + ParseResult parsed = ctx.parseExpression(conversion::Convertible(json)); + ASSERT_TRUE(parsed); + + ColorRampPropertyValue ramp(std::move(*parsed)); + EXPECT_TRUE(ramp.getDependencies() & Dependency::GlobalState); + + GlobalStateMap state{{"hotColor", std::string("red")}}; + EXPECT_EQ(*Color::parse("red"), ramp.evaluate(0.5, &state)); + + // Without global state the expression fails to produce a color; the + // evaluation must degrade to a default color instead of crashing. + EXPECT_EQ(Color{}, ramp.evaluate(0.5)); +} + +TEST(GlobalStateExpression, FormatSectionOverrideUsesCapturedState) { + auto expr = parseExpression(R"(["to-color", ["global-state", "labelColor"]])", {type::Color}); + ASSERT_TRUE(expr); + + PropertyExpression propertyExpression(std::move(expr)); + auto state = std::make_shared(GlobalStateMap{{"labelColor", std::string("red")}}); + propertyExpression.captureGlobalState(state); + + PossiblyEvaluatedPropertyValue defaultValue(std::move(propertyExpression)); + FormatSectionOverride overrideExpression(type::Color, std::move(defaultValue), "text-color"); + + // Evaluating without a formatted section and without global state in the + // context must fall back to the state captured by the wrapped expression. + auto result = overrideExpression.evaluate(EvaluationContext()); + ASSERT_TRUE(result); + EXPECT_EQ(ExprValue(*Color::parse("red")), *result); +} + +TEST(GlobalStateExpression, Serialize) { + auto expr = parseExpression(R"(["global-state", "showLabels"])"); + ASSERT_TRUE(expr); + auto serialized = expr->serialize(); + ASSERT_TRUE(serialized.is()); + const auto& array = serialized.get(); + ASSERT_EQ(2u, array.size()); + EXPECT_EQ(mln::Value("global-state"), array[0]); + EXPECT_EQ(mln::Value("showLabels"), array[1]); +} diff --git a/test/style/style.test.cpp b/test/style/style.test.cpp index 2682473efe22..6f8944863ed6 100644 --- a/test/style/style.test.cpp +++ b/test/style/style.test.cpp @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include #include #include @@ -71,6 +73,141 @@ TEST(Style, Properties) { ASSERT_EQ(0, *style.getDefaultCamera().pitch); } +TEST(Style, GlobalState) { + util::RunLoop loop; + + auto fileSource = std::make_shared(); + Style::Impl style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; + + // Defaults from the root "state" property are used as the initial global state. + style.loadJSON(R"STYLE({ + "version": 8, + "state": { + "showLabels": {"default": true}, + "categories": {"default": ["restaurant", "hotel"]}, + "noDefault": {} + }, + "sources": {}, + "layers": [] + })STYLE"); + + auto state = style.getGlobalState(); + ASSERT_EQ(3u, state.size()); + EXPECT_EQ(Value(true), state.at("showLabels")); + EXPECT_EQ(Value(mapbox::base::ValueArray({Value("restaurant"), Value("hotel")})), state.at("categories")); + EXPECT_EQ(Value(NullValue()), state.at("noDefault")); + + // Setting a property updates the state. + style.setGlobalStateProperty("showLabels", false); + EXPECT_EQ(Value(false), style.getGlobalState().at("showLabels")); + + // Properties that are not defined in the style can also be set. + style.setGlobalStateProperty("custom", 42.0); + EXPECT_EQ(Value(42.0), style.getGlobalState().at("custom")); + + // A null value resets the property to its default. + style.setGlobalStateProperty("showLabels", NullValue()); + EXPECT_EQ(Value(true), style.getGlobalState().at("showLabels")); + + // A null value on a property without default resets to null. + style.setGlobalStateProperty("custom", NullValue()); + EXPECT_EQ(Value(NullValue()), style.getGlobalState().at("custom")); + + // Loading a new style resets the global state. + style.loadJSON(R"STYLE({"version": 8, "sources": {}, "layers": []})STYLE"); + EXPECT_TRUE(style.getGlobalState().empty()); + + // Setting an absent property to null is a no-op: a missing property + // already evaluates to null. + style.setGlobalStateProperty("neverSet", NullValue()); + EXPECT_TRUE(style.getGlobalState().empty()); +} + +TEST(Style, GlobalStateNumericEquality) { + util::RunLoop loop; + + auto fileSource = std::make_shared(); + Style::Impl style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; + + style.loadJSON(R"STYLE({ + "version": 8, + "state": {"minSpeed": {"default": 50}}, + "sources": {}, + "layers": [] + })STYLE"); + + // Setting the same numeric value with a different arithmetic type + // (integer default vs. runtime double) must be treated as unchanged. + const auto before = style.getGlobalStateShared(); + style.setGlobalStateProperty("minSpeed", 50.0); + EXPECT_EQ(before, style.getGlobalStateShared()); + + style.setGlobalStateProperty("minSpeed", 51.0); + EXPECT_NE(before, style.getGlobalStateShared()); +} + +TEST(Style, GlobalStateVisibility) { + util::RunLoop loop; + + auto fileSource = std::make_shared(); + Style::Impl style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; + + style.loadJSON(R"STYLE({ + "version": 8, + "state": {"showBackground": {"default": true}}, + "sources": {}, + "layers": [{ + "id": "background", + "type": "background", + "layout": { + "visibility": ["case", ["to-boolean", ["global-state", "showBackground"]], "visible", "none"] + } + }] + })STYLE"); + + Layer* layer = style.getLayer("background"); + ASSERT_TRUE(layer); + // The default from the root "state" property applies. + EXPECT_EQ(VisibilityType::Visible, layer->getVisibility()); + + style.setGlobalStateProperty("showBackground", false); + EXPECT_EQ(VisibilityType::None, layer->getVisibility()); + + style.setGlobalStateProperty("showBackground", NullValue()); + EXPECT_EQ(VisibilityType::Visible, layer->getVisibility()); + + // Setting a constant visibility replaces the expression: further state + // changes no longer apply. + layer->setVisibility(VisibilityType::None); + style.setGlobalStateProperty("showBackground", true); + EXPECT_EQ(VisibilityType::None, layer->getVisibility()); +} + +TEST(Style, VisibilityExpressionRejectsOtherDependencies) { + util::RunLoop loop; + + auto fileSource = std::make_shared(); + Style::Impl style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; + + style.loadJSON(R"STYLE({ + "version": 8, + "sources": {}, + "layers": [{"id": "background", "type": "background"}] + })STYLE"); + + Layer* layer = style.getLayer("background"); + ASSERT_TRUE(layer); + + // Visibility expressions may only depend on the global state. + rapidjson::GenericDocument, rapidjson::CrtAllocator> document; + document.Parse<0>(R"(["case", [">", ["zoom"], 10], "visible", "none"])"); + ASSERT_FALSE(document.HasParseError()); + const JSValue* json = &document; + auto error = layer->setProperty("visibility", conversion::Convertible(json)); + EXPECT_TRUE(error); + EXPECT_EQ(VisibilityType::Visible, layer->getVisibility()); +} + TEST(Style, DuplicateSource) { util::RunLoop loop; From 738b3b7107c4e71ac7ed27bffeeb6559269a77e2 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Sun, 23 Aug 2026 18:17:13 +0900 Subject: [PATCH 02/32] perf(core): track global-state refs per key to limit re-evaluation --- CHANGELOG.md | 2 +- .../mln/style/color_ramp_property_value.hpp | 15 ++++- include/mln/style/expression/expression.hpp | 18 ++++++ include/mln/style/layer_properties.hpp | 7 +++ include/mln/style/property_expression.hpp | 7 +++ include/mln/style/property_value.hpp | 8 +++ .../layers/render_color_relief_layer.cpp | 7 ++- .../renderer/layers/render_heatmap_layer.cpp | 6 +- src/mln/renderer/layers/render_line_layer.cpp | 7 ++- .../possibly_evaluated_property_value.hpp | 16 ++++++ .../property_evaluation_parameters.hpp | 7 +++ src/mln/renderer/render_orchestrator.cpp | 55 +++++++++++++++--- src/mln/style/expression/expression.cpp | 30 ++++++++++ src/mln/style/layer_impl.hpp | 11 ++++ .../layers/background_layer_properties.cpp | 4 ++ .../layers/background_layer_properties.hpp | 2 + src/mln/style/layers/circle_layer_impl.hpp | 5 ++ .../style/layers/circle_layer_properties.cpp | 4 ++ .../style/layers/circle_layer_properties.hpp | 2 + .../layers/color_relief_layer_properties.cpp | 4 ++ .../layers/color_relief_layer_properties.hpp | 2 + .../layers/fill_extrusion_layer_impl.hpp | 5 ++ .../fill_extrusion_layer_properties.cpp | 4 ++ .../fill_extrusion_layer_properties.hpp | 2 + src/mln/style/layers/fill_layer_impl.hpp | 5 ++ .../style/layers/fill_layer_properties.cpp | 4 ++ .../style/layers/fill_layer_properties.hpp | 2 + .../style/layers/heatmap_layer_properties.cpp | 4 ++ .../style/layers/heatmap_layer_properties.hpp | 2 + .../layers/hillshade_layer_properties.cpp | 4 ++ .../layers/hillshade_layer_properties.hpp | 2 + src/mln/style/layers/layer_properties.cpp.ejs | 4 ++ src/mln/style/layers/layer_properties.hpp.ejs | 2 + src/mln/style/layers/line_layer_impl.hpp | 5 ++ .../style/layers/line_layer_properties.cpp | 4 ++ .../style/layers/line_layer_properties.hpp | 2 + .../layers/location_indicator_layer_impl.hpp | 5 ++ .../location_indicator_layer_properties.cpp | 4 ++ .../location_indicator_layer_properties.hpp | 2 + .../style/layers/raster_layer_properties.cpp | 4 ++ .../style/layers/raster_layer_properties.hpp | 2 + src/mln/style/layers/symbol_layer_impl.hpp | 5 ++ .../style/layers/symbol_layer_properties.cpp | 4 ++ .../style/layers/symbol_layer_properties.hpp | 2 + src/mln/style/properties.hpp | 57 ++++++++++++++++++- src/mln/style/property_expression.cpp | 14 +++++ test/style/expression/global_state.test.cpp | 42 ++++++++++++++ 47 files changed, 394 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7264f6a0e3ca..5c0bd432c266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### ✨ New features - *...Add new stuff here...* -- [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState`. The expression is supported in filters, layout and paint properties (including color ramps such as `heatmap-color` and `line-gradient`), the `visibility` layout property, and feature query filters ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). +- [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState`. The expression is supported in filters, layout and paint properties (including color ramps such as `heatmap-color` and `line-gradient`), the `visibility` layout property, and feature query filters. State changes track the referenced property names, so only the layers and sources that reference a changed property are re-evaluated or relayouted ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [core] Locally rasterized CJK glyphs now render at 2x texture resolution, preserving fine strokes. Mirrors [maplibre-gl-js#3006](https://github.com/maplibre/maplibre-gl-js/pull/3006). - [core] Added new map observer events: onPreCompileShader, onPostCompileShader, onShaderCompileFailed, onGlyphsLoaded, onGlyphsError, onGlyphsRequested, onTileAction, onSpriteLoaded, onSpriteError, onSpriteRequested ([#2694](https://github.com/maplibre/maplibre-native/pull/2694)). - [core] Add WebP image decoding support to default platform (Linux, Windows) diff --git a/include/mln/style/color_ramp_property_value.hpp b/include/mln/style/color_ramp_property_value.hpp index 71a005d8a7ba..fd63f5554c07 100644 --- a/include/mln/style/color_ramp_property_value.hpp +++ b/include/mln/style/color_ramp_property_value.hpp @@ -14,6 +14,7 @@ namespace style { class ColorRampPropertyValue { private: std::shared_ptr value; + std::shared_ptr> globalStateRefs_; friend bool operator==(const ColorRampPropertyValue& lhs, const ColorRampPropertyValue& rhs) noexcept { return (lhs.isUndefined() && rhs.isUndefined()) || (lhs.value && rhs.value && *(lhs.value) == *(rhs.value)); @@ -25,8 +26,14 @@ class ColorRampPropertyValue { public: ColorRampPropertyValue() noexcept = default; - ColorRampPropertyValue(std::shared_ptr value_) noexcept - : value(std::move(value_)) {} + ColorRampPropertyValue(std::shared_ptr value_) + : value(std::move(value_)) { + if (value && value->has(expression::Dependency::GlobalState)) { + auto refs = std::make_shared>(); + expression::collectGlobalStateRefs(*value, *refs); + globalStateRefs_ = std::move(refs); + } + } bool isUndefined() const noexcept { return value == nullptr; } @@ -53,6 +60,10 @@ class ColorRampPropertyValue { using Dependency = style::expression::Dependency; Dependency getDependencies() const noexcept { return value ? value->dependencies : Dependency::None; } + + /// The names of the global-state properties referenced by the expression, + /// or null if none are referenced. + const std::set* getGlobalStateRefs() const noexcept { return globalStateRefs_.get(); } }; } // namespace style diff --git a/include/mln/style/expression/expression.hpp b/include/mln/style/expression/expression.hpp index 181b6e0148fa..897811566106 100644 --- a/include/mln/style/expression/expression.hpp +++ b/include/mln/style/expression/expression.hpp @@ -9,10 +9,13 @@ #include #include +#include #include #include #include #include +#include +#include #include namespace mln { @@ -383,6 +386,21 @@ class Expression { type::Type type; }; +/// Collect the names of the global-state properties referenced by +/// "global-state" expressions in the given expression tree. +void collectGlobalStateRefs(const Expression&, std::set& refs); + +/// Whether any of the global-state keys referenced by an expression is among +/// the changed keys. A null `refs` means the referenced keys are unknown and +/// a null `changedKeys` means the changed keys are unknown; either is +/// conservatively treated as a match. +inline bool globalStateRefsIntersect(const std::set* refs, const std::set* changedKeys) { + if (refs == nullptr || changedKeys == nullptr) { + return true; + } + return std::any_of(refs->begin(), refs->end(), [&](const std::string& key) { return changedKeys->count(key); }); +} + } // namespace expression } // namespace style diff --git a/include/mln/style/layer_properties.hpp b/include/mln/style/layer_properties.hpp index 74a22af800e6..f64773606311 100644 --- a/include/mln/style/layer_properties.hpp +++ b/include/mln/style/layer_properties.hpp @@ -1,6 +1,9 @@ #pragma once #include +#include +#include + namespace mln { namespace style { @@ -28,6 +31,10 @@ class LayerProperties { /// evaluation, such as data-driven paint properties). virtual expression::Dependency getEvaluatedDependencies() const noexcept { return expression::Dependency::None; } + /// Collect the names of the global-state properties referenced by the + /// expressions retained in the evaluated properties. + virtual void collectEvaluatedGlobalStateRefs(std::set&) const {} + protected: LayerProperties(Immutable impl) noexcept : baseImpl(std::move(impl)) {} diff --git a/include/mln/style/property_expression.hpp b/include/mln/style/property_expression.hpp index d1f2ccb0fa92..26b3c550448f 100644 --- a/include/mln/style/property_expression.hpp +++ b/include/mln/style/property_expression.hpp @@ -58,6 +58,11 @@ class PropertyExpressionBase { return capturedGlobalState_; } + /// The names of the global-state properties referenced by the expression, + /// collected once at construction. Null if the expression does not + /// reference the global state. + const std::set* getGlobalStateRefs() const noexcept { return globalStateRefs_.get(); } + /// Can be used for aggregating property expressions from multiple properties(layers) into single match / case /// expression. May be removed if a better way of aggregation is found. std::shared_ptr getSharedExpression() const noexcept; @@ -74,6 +79,8 @@ class PropertyExpressionBase { std::shared_ptr capturedGlobalState_; + std::shared_ptr> globalStateRefs_; + ZoomCurvePtr zoomCurve; bool useIntegerZoom_ = false; diff --git a/include/mln/style/property_value.hpp b/include/mln/style/property_value.hpp index 36f9153d0101..a4fe565e6b06 100644 --- a/include/mln/style/property_value.hpp +++ b/include/mln/style/property_value.hpp @@ -72,6 +72,14 @@ class PropertyValue { [](const T&) { return Dependency::None; }, [](const PropertyExpression& ex) { return ex.getDependencies(); }); } + + /// The names of the global-state properties referenced by a contained + /// expression, or null if none are referenced. + const std::set* getGlobalStateRefs() const noexcept { + return value.match([](const Undefined&) -> const std::set* { return nullptr; }, + [](const T&) -> const std::set* { return nullptr; }, + [](const PropertyExpression& ex) { return ex.getGlobalStateRefs(); }); + } }; } // namespace style diff --git a/src/mln/renderer/layers/render_color_relief_layer.cpp b/src/mln/renderer/layers/render_color_relief_layer.cpp index 7602dedd235b..476c947d63a6 100644 --- a/src/mln/renderer/layers/render_color_relief_layer.cpp +++ b/src/mln/renderer/layers/render_color_relief_layer.cpp @@ -62,9 +62,12 @@ void RenderColorReliefLayer::transition(const TransitionParameters& parameters) void RenderColorReliefLayer::evaluate(const PropertyEvaluationParameters& parameters) { if (colorRampGlobalState != parameters.globalState) { + const auto& rampValue = unevaluated.get().getValue(); + const bool rampAffected = (rampValue.getDependencies() & style::expression::Dependency::GlobalState) && + style::expression::globalStateRefsIntersect( + rampValue.getGlobalStateRefs(), parameters.changedGlobalStateKeys.get()); colorRampGlobalState = parameters.globalState; - if (unevaluated.get().getValue().getDependencies() & - style::expression::Dependency::GlobalState) { + if (rampAffected) { updateColorRamp(); } } diff --git a/src/mln/renderer/layers/render_heatmap_layer.cpp b/src/mln/renderer/layers/render_heatmap_layer.cpp index eedb040bac31..b71e0202aed3 100644 --- a/src/mln/renderer/layers/render_heatmap_layer.cpp +++ b/src/mln/renderer/layers/render_heatmap_layer.cpp @@ -60,8 +60,12 @@ void RenderHeatmapLayer::layerChanged(const TransitionParameters& parameters, void RenderHeatmapLayer::evaluate(const PropertyEvaluationParameters& parameters) { if (colorRampGlobalState != parameters.globalState) { + const auto& rampValue = unevaluated.get().getValue(); + const bool rampAffected = (rampValue.getDependencies() & expression::Dependency::GlobalState) && + expression::globalStateRefsIntersect(rampValue.getGlobalStateRefs(), + parameters.changedGlobalStateKeys.get()); colorRampGlobalState = parameters.globalState; - if (unevaluated.get().getValue().getDependencies() & expression::Dependency::GlobalState) { + if (rampAffected) { updateColorRamp(); } } diff --git a/src/mln/renderer/layers/render_line_layer.cpp b/src/mln/renderer/layers/render_line_layer.cpp index 60609910d4cf..2e10739036e0 100644 --- a/src/mln/renderer/layers/render_line_layer.cpp +++ b/src/mln/renderer/layers/render_line_layer.cpp @@ -68,9 +68,12 @@ void RenderLineLayer::transition(const TransitionParameters& parameters) { void RenderLineLayer::evaluate(const PropertyEvaluationParameters& parameters) { if (colorRampGlobalState != parameters.globalState) { + const auto& rampValue = unevaluated.get().getValue(); + const bool rampAffected = (rampValue.getDependencies() & style::expression::Dependency::GlobalState) && + style::expression::globalStateRefsIntersect( + rampValue.getGlobalStateRefs(), parameters.changedGlobalStateKeys.get()); colorRampGlobalState = parameters.globalState; - if (unevaluated.get().getValue().getDependencies() & - style::expression::Dependency::GlobalState) { + if (rampAffected) { updateColorRamp(); } } diff --git a/src/mln/renderer/possibly_evaluated_property_value.hpp b/src/mln/renderer/possibly_evaluated_property_value.hpp index 1cf8fb8ea35e..4a1ee0927d51 100644 --- a/src/mln/renderer/possibly_evaluated_property_value.hpp +++ b/src/mln/renderer/possibly_evaluated_property_value.hpp @@ -70,6 +70,14 @@ class PossiblyEvaluatedPropertyValue { return value.match([](const T&) { return Dependency::None; }, [](const style::PropertyExpression& expression) { return expression.getDependencies(); }); } + + /// The names of the global-state properties referenced by a retained + /// expression, or null if none are referenced. + const std::set* getGlobalStateRefs() const noexcept { + return value.match( + [](const T&) -> const std::set* { return nullptr; }, + [](const style::PropertyExpression& expression) { return expression.getGlobalStateRefs(); }); + } }; template @@ -124,6 +132,14 @@ class PossiblyEvaluatedPropertyValue> { return value.match([](const Faded&) { return Dependency::None; }, [](const style::PropertyExpression& expression) { return expression.getDependencies(); }); } + + /// The names of the global-state properties referenced by a retained + /// expression, or null if none are referenced. + const std::set* getGlobalStateRefs() const noexcept { + return value.match( + [](const Faded&) -> const std::set* { return nullptr; }, + [](const style::PropertyExpression& expression) { return expression.getGlobalStateRefs(); }); + } }; namespace util { diff --git a/src/mln/renderer/property_evaluation_parameters.hpp b/src/mln/renderer/property_evaluation_parameters.hpp index 95301b9d3f51..4d7ff6fc46f9 100644 --- a/src/mln/renderer/property_evaluation_parameters.hpp +++ b/src/mln/renderer/property_evaluation_parameters.hpp @@ -5,6 +5,8 @@ #include #include +#include +#include namespace mln { @@ -49,6 +51,11 @@ class PropertyEvaluationParameters { /// The style's current global state, used to evaluate `global-state` expressions. std::shared_ptr globalState; + /// The names of the global-state properties that changed since the last + /// evaluation. Only meaningful when `globalStateChanged` is set; null + /// means the changed keys are unknown (treat all as changed). + std::shared_ptr> changedGlobalStateKeys; + bool zoomChanged = true; bool layerChanged = false; bool hasCrossfade = true; diff --git a/src/mln/renderer/render_orchestrator.cpp b/src/mln/renderer/render_orchestrator.cpp index 5e354bbca047..673c87ba4a31 100644 --- a/src/mln/renderer/render_orchestrator.cpp +++ b/src/mln/renderer/render_orchestrator.cpp @@ -62,6 +62,25 @@ RendererObserver& nullObserver() { return observer; } +// The keys whose values differ between two global state snapshots. Values of +// unchanged keys are copied verbatim between snapshots, so exact equality is +// sufficient here. +std::set diffGlobalStateKeys(const GlobalStateMap& previous, const GlobalStateMap& current) { + std::set keys; + for (const auto& entry : current) { + const auto it = previous.find(entry.first); + if (it == previous.end() || !(it->second == entry.second)) { + keys.insert(entry.first); + } + } + for (const auto& entry : previous) { + if (current.find(entry.first) == current.end()) { + keys.insert(entry.first); + } + } + return keys; +} + class RenderTreeImpl final : public RenderTree { public: RenderTreeImpl(std::unique_ptr parameters_, @@ -184,12 +203,21 @@ std::unique_ptr RenderOrchestrator::createRenderTree( isMapModeContinuous ? util::DEFAULT_TRANSITION_DURATION : Duration::zero()); const bool globalStateChanged = globalState != updateParameters->globalState; + // The keys whose values changed, so that only the layers and properties + // referencing those keys are re-evaluated or relayouted. Null when the + // previous state is unknown (treat all keys as changed). + std::shared_ptr> changedGlobalStateKeys; + if (globalStateChanged && globalState && updateParameters->globalState) { + changedGlobalStateKeys = std::make_shared>( + diffGlobalStateKeys(*globalState, *updateParameters->globalState)); + } globalState = updateParameters->globalState; PropertyEvaluationParameters evaluationParameters{zoomHistory, updateParameters->timePoint, transitionDuration}; evaluationParameters.zoomChanged = zoomChanged; evaluationParameters.globalState = globalState; evaluationParameters.globalStateChanged = globalStateChanged; + evaluationParameters.changedGlobalStateKeys = changedGlobalStateKeys; TileParameters tileParameters{.pixelRatio = updateParameters->pixelRatio, .debugOptions = updateParameters->debugOptions, @@ -395,14 +423,25 @@ std::unique_ptr RenderOrchestrator::createRenderTree( if (layer.baseImpl->source == sourceImpl->id) { const std::string& layerId = layer.getID(); // A global state change requires a relayout of this - // layer's source if the state is referenced by anything - // that is baked into the tile's buckets: the filter, - // layout properties, or data-driven paint properties - // (i.e. expressions retained in the evaluated properties). - const bool globalStateRequiresRelayout = - globalStateChanged && ((layer.baseImpl->getLayoutDependencies() | - layer.evaluatedProperties->getEvaluatedDependencies()) & - expression::Dependency::GlobalState); + // layer's source if one of the changed state properties + // is referenced by anything that is baked into the tile's + // buckets: the filter, layout properties, or data-driven + // paint properties (i.e. expressions retained in the + // evaluated properties). + const bool globalStateRequiresRelayout = [&] { + if (!globalStateChanged || !((layer.baseImpl->getLayoutDependencies() | + layer.evaluatedProperties->getEvaluatedDependencies()) & + expression::Dependency::GlobalState)) { + return false; + } + if (!changedGlobalStateKeys) { + return true; + } + std::set refs; + layer.baseImpl->collectLayoutGlobalStateRefs(refs); + layer.evaluatedProperties->collectEvaluatedGlobalStateRefs(refs); + return expression::globalStateRefsIntersect(&refs, changedGlobalStateKeys.get()); + }(); sourceNeedsRelayout = (sourceNeedsRelayout || hasImageDiff || constantsMaskChanged.contains(layerId) || hasLayoutDifference(layerDiff, layerId) || globalStateRequiresRelayout); diff --git a/src/mln/style/expression/expression.cpp b/src/mln/style/expression/expression.cpp index 5774899d554f..4083c7f51233 100644 --- a/src/mln/style/expression/expression.cpp +++ b/src/mln/style/expression/expression.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -9,6 +10,35 @@ namespace mln { using Dependency = style::expression::Dependency; +namespace style { +namespace expression { + +void collectGlobalStateRefs(const Expression& expression, std::set& refs) { + if (!(expression.dependencies & Dependency::GlobalState)) { + return; + } + if (expression.getKind() == Kind::CompoundExpression) { + const auto* compound = static_cast(&expression); + if (compound->getOperator() == "global-state") { + // The property name argument is enforced to be a string literal + // at parse time. + expression.eachChild([&](const Expression& child) { + if (child.getKind() == Kind::Literal) { + const auto& value = static_cast(&child)->getValue(); + if (value.is()) { + refs.insert(value.get()); + } + } + }); + return; + } + } + expression.eachChild([&](const Expression& child) { collectGlobalStateRefs(child, refs); }); +} + +} // namespace expression +} // namespace style + std::ostream& operator<<(std::ostream& os, const Dependency deps) { if (deps == Dependency::None) { os << "None"; diff --git a/src/mln/style/layer_impl.hpp b/src/mln/style/layer_impl.hpp index 5e614c686819..1e70a470ecaa 100644 --- a/src/mln/style/layer_impl.hpp +++ b/src/mln/style/layer_impl.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -62,6 +63,16 @@ class Layer::Impl { : expression::Dependency::None; } + /// Collect the names of the global-state properties referenced by + /// expressions that affect tile layout: the filter and the layout + /// properties. Derived classes with layout properties combine those with + /// the filter references from this base implementation. + virtual void collectLayoutGlobalStateRefs(std::set& refs) const { + if (filter.expression && *filter.expression) { + expression::collectGlobalStateRefs(**filter.expression, refs); + } + } + // Optional expression backing the "visibility" layout value. Per the // style specification, such expressions may only depend on the global // state; the evaluated result is kept in `visibility` and re-evaluated diff --git a/src/mln/style/layers/background_layer_properties.cpp b/src/mln/style/layers/background_layer_properties.cpp index 608ef02d590a..15c86fec8221 100644 --- a/src/mln/style/layers/background_layer_properties.cpp +++ b/src/mln/style/layers/background_layer_properties.cpp @@ -39,6 +39,10 @@ expression::Dependency BackgroundLayerProperties::getEvaluatedDependencies() con return evaluated.getDependencies(); } +void BackgroundLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/background_layer_properties.hpp b/src/mln/style/layers/background_layer_properties.hpp index 9b138337a3ba..5af45f7717b8 100644 --- a/src/mln/style/layers/background_layer_properties.hpp +++ b/src/mln/style/layers/background_layer_properties.hpp @@ -49,6 +49,8 @@ class BackgroundLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const BackgroundLayer::Impl& layerImpl() const noexcept; // Data members. CrossfadeParameters crossfade; diff --git a/src/mln/style/layers/circle_layer_impl.hpp b/src/mln/style/layers/circle_layer_impl.hpp index 8030b3324925..37634463b77e 100644 --- a/src/mln/style/layers/circle_layer_impl.hpp +++ b/src/mln/style/layers/circle_layer_impl.hpp @@ -18,6 +18,11 @@ class CircleLayer::Impl : public Layer::Impl { return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); } + void collectLayoutGlobalStateRefs(std::set& refs) const override { + layout.collectGlobalStateRefs(refs); + Layer::Impl::collectLayoutGlobalStateRefs(refs); + } + CircleLayoutProperties::Unevaluated layout; CirclePaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/circle_layer_properties.cpp b/src/mln/style/layers/circle_layer_properties.cpp index 4d4c8b7c6578..8e9526937b4d 100644 --- a/src/mln/style/layers/circle_layer_properties.cpp +++ b/src/mln/style/layers/circle_layer_properties.cpp @@ -37,6 +37,10 @@ expression::Dependency CircleLayerProperties::getEvaluatedDependencies() const n return evaluated.getDependencies(); } +void CircleLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/circle_layer_properties.hpp b/src/mln/style/layers/circle_layer_properties.hpp index 43599b593e3c..45a1f36462d5 100644 --- a/src/mln/style/layers/circle_layer_properties.hpp +++ b/src/mln/style/layers/circle_layer_properties.hpp @@ -97,6 +97,8 @@ class CircleLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const CircleLayer::Impl& layerImpl() const noexcept; // Data members. CirclePaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/color_relief_layer_properties.cpp b/src/mln/style/layers/color_relief_layer_properties.cpp index e7b1863abe53..c26a8ab5eaa2 100644 --- a/src/mln/style/layers/color_relief_layer_properties.cpp +++ b/src/mln/style/layers/color_relief_layer_properties.cpp @@ -37,6 +37,10 @@ expression::Dependency ColorReliefLayerProperties::getEvaluatedDependencies() co return evaluated.getDependencies(); } +void ColorReliefLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/color_relief_layer_properties.hpp b/src/mln/style/layers/color_relief_layer_properties.hpp index e7f9623964a7..f28a1df8b0be 100644 --- a/src/mln/style/layers/color_relief_layer_properties.hpp +++ b/src/mln/style/layers/color_relief_layer_properties.hpp @@ -42,6 +42,8 @@ class ColorReliefLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const ColorReliefLayer::Impl& layerImpl() const noexcept; // Data members. ColorReliefPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/fill_extrusion_layer_impl.hpp b/src/mln/style/layers/fill_extrusion_layer_impl.hpp index 6b69750364b9..5bbd57b9521b 100644 --- a/src/mln/style/layers/fill_extrusion_layer_impl.hpp +++ b/src/mln/style/layers/fill_extrusion_layer_impl.hpp @@ -18,6 +18,11 @@ class FillExtrusionLayer::Impl : public Layer::Impl { return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); } + void collectLayoutGlobalStateRefs(std::set& refs) const override { + layout.collectGlobalStateRefs(refs); + Layer::Impl::collectLayoutGlobalStateRefs(refs); + } + FillExtrusionLayoutProperties::Unevaluated layout; FillExtrusionPaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/fill_extrusion_layer_properties.cpp b/src/mln/style/layers/fill_extrusion_layer_properties.cpp index 90b1baab6a38..61e5d8558b49 100644 --- a/src/mln/style/layers/fill_extrusion_layer_properties.cpp +++ b/src/mln/style/layers/fill_extrusion_layer_properties.cpp @@ -39,6 +39,10 @@ expression::Dependency FillExtrusionLayerProperties::getEvaluatedDependencies() return evaluated.getDependencies(); } +void FillExtrusionLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/fill_extrusion_layer_properties.hpp b/src/mln/style/layers/fill_extrusion_layer_properties.hpp index a6e67f08ba16..a864ee311962 100644 --- a/src/mln/style/layers/fill_extrusion_layer_properties.hpp +++ b/src/mln/style/layers/fill_extrusion_layer_properties.hpp @@ -83,6 +83,8 @@ class FillExtrusionLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const FillExtrusionLayer::Impl& layerImpl() const noexcept; // Data members. CrossfadeParameters crossfade; diff --git a/src/mln/style/layers/fill_layer_impl.hpp b/src/mln/style/layers/fill_layer_impl.hpp index 9438b74f0fdd..3ccfde1c52a2 100644 --- a/src/mln/style/layers/fill_layer_impl.hpp +++ b/src/mln/style/layers/fill_layer_impl.hpp @@ -18,6 +18,11 @@ class FillLayer::Impl : public Layer::Impl { return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); } + void collectLayoutGlobalStateRefs(std::set& refs) const override { + layout.collectGlobalStateRefs(refs); + Layer::Impl::collectLayoutGlobalStateRefs(refs); + } + FillLayoutProperties::Unevaluated layout; FillPaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/fill_layer_properties.cpp b/src/mln/style/layers/fill_layer_properties.cpp index f6c5dfbfc215..64a91f2b4e1b 100644 --- a/src/mln/style/layers/fill_layer_properties.cpp +++ b/src/mln/style/layers/fill_layer_properties.cpp @@ -39,6 +39,10 @@ expression::Dependency FillLayerProperties::getEvaluatedDependencies() const noe return evaluated.getDependencies(); } +void FillLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/fill_layer_properties.hpp b/src/mln/style/layers/fill_layer_properties.hpp index b65f64f083eb..3836727e7578 100644 --- a/src/mln/style/layers/fill_layer_properties.hpp +++ b/src/mln/style/layers/fill_layer_properties.hpp @@ -78,6 +78,8 @@ class FillLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const FillLayer::Impl& layerImpl() const noexcept; // Data members. CrossfadeParameters crossfade; diff --git a/src/mln/style/layers/heatmap_layer_properties.cpp b/src/mln/style/layers/heatmap_layer_properties.cpp index 55b4d1be32ba..b5fe92d6f019 100644 --- a/src/mln/style/layers/heatmap_layer_properties.cpp +++ b/src/mln/style/layers/heatmap_layer_properties.cpp @@ -37,6 +37,10 @@ expression::Dependency HeatmapLayerProperties::getEvaluatedDependencies() const return evaluated.getDependencies(); } +void HeatmapLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/heatmap_layer_properties.hpp b/src/mln/style/layers/heatmap_layer_properties.hpp index 366ee7fb3865..f5ef8e482c99 100644 --- a/src/mln/style/layers/heatmap_layer_properties.hpp +++ b/src/mln/style/layers/heatmap_layer_properties.hpp @@ -57,6 +57,8 @@ class HeatmapLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const HeatmapLayer::Impl& layerImpl() const noexcept; // Data members. HeatmapPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/hillshade_layer_properties.cpp b/src/mln/style/layers/hillshade_layer_properties.cpp index 601d0429cadb..824dab81e715 100644 --- a/src/mln/style/layers/hillshade_layer_properties.cpp +++ b/src/mln/style/layers/hillshade_layer_properties.cpp @@ -37,6 +37,10 @@ expression::Dependency HillshadeLayerProperties::getEvaluatedDependencies() cons return evaluated.getDependencies(); } +void HillshadeLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/hillshade_layer_properties.hpp b/src/mln/style/layers/hillshade_layer_properties.hpp index aba4e30599a2..0fdb700f0919 100644 --- a/src/mln/style/layers/hillshade_layer_properties.hpp +++ b/src/mln/style/layers/hillshade_layer_properties.hpp @@ -73,6 +73,8 @@ class HillshadeLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const HillshadeLayer::Impl& layerImpl() const noexcept; // Data members. HillshadePaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/layer_properties.cpp.ejs b/src/mln/style/layers/layer_properties.cpp.ejs index 642aa0ccdf58..4b41b0482fab 100644 --- a/src/mln/style/layers/layer_properties.cpp.ejs +++ b/src/mln/style/layers/layer_properties.cpp.ejs @@ -48,6 +48,10 @@ expression::Dependency <%- camelize(type) %>LayerProperties::getEvaluatedDepende return evaluated.getDependencies(); } +void <%- camelize(type) %>LayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/layer_properties.hpp.ejs b/src/mln/style/layers/layer_properties.hpp.ejs index ea3c7e5164fe..a84253166497 100644 --- a/src/mln/style/layers/layer_properties.hpp.ejs +++ b/src/mln/style/layers/layer_properties.hpp.ejs @@ -80,6 +80,8 @@ public: expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const <%- camelize(type) %>Layer::Impl& layerImpl() const noexcept; // Data members. <% if (type === 'background' || type === 'fill' || type === 'line' || type === 'fill-extrusion') { -%> diff --git a/src/mln/style/layers/line_layer_impl.hpp b/src/mln/style/layers/line_layer_impl.hpp index 789a8e5e5ea9..6cb044bce9fc 100644 --- a/src/mln/style/layers/line_layer_impl.hpp +++ b/src/mln/style/layers/line_layer_impl.hpp @@ -22,6 +22,11 @@ class LineLayer::Impl : public Layer::Impl { return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); } + void collectLayoutGlobalStateRefs(std::set& refs) const override { + layout.collectGlobalStateRefs(refs); + Layer::Impl::collectLayoutGlobalStateRefs(refs); + } + LineLayoutProperties::Unevaluated layout; LinePaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/line_layer_properties.cpp b/src/mln/style/layers/line_layer_properties.cpp index ede0e11e67bf..72737b357f45 100644 --- a/src/mln/style/layers/line_layer_properties.cpp +++ b/src/mln/style/layers/line_layer_properties.cpp @@ -39,6 +39,10 @@ expression::Dependency LineLayerProperties::getEvaluatedDependencies() const noe return evaluated.getDependencies(); } +void LineLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/line_layer_properties.hpp b/src/mln/style/layers/line_layer_properties.hpp index fc9e0d3e48f8..37821126c82f 100644 --- a/src/mln/style/layers/line_layer_properties.hpp +++ b/src/mln/style/layers/line_layer_properties.hpp @@ -127,6 +127,8 @@ class LineLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const LineLayer::Impl& layerImpl() const noexcept; // Data members. CrossfadeParameters crossfade; diff --git a/src/mln/style/layers/location_indicator_layer_impl.hpp b/src/mln/style/layers/location_indicator_layer_impl.hpp index 7ea3d1be9dca..d89968de02d8 100644 --- a/src/mln/style/layers/location_indicator_layer_impl.hpp +++ b/src/mln/style/layers/location_indicator_layer_impl.hpp @@ -29,6 +29,11 @@ class LocationIndicatorLayer::Impl : public Layer::Impl { return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); } + void collectLayoutGlobalStateRefs(std::set& refs) const override { + layout.collectGlobalStateRefs(refs); + Layer::Impl::collectLayoutGlobalStateRefs(refs); + } + LocationIndicatorLayoutProperties::Unevaluated layout; LocationIndicatorPaintProperties::Transitionable paint; DECLARE_LAYER_TYPE_INFO; diff --git a/src/mln/style/layers/location_indicator_layer_properties.cpp b/src/mln/style/layers/location_indicator_layer_properties.cpp index 99ef5fb17f27..ee1546fd70ed 100644 --- a/src/mln/style/layers/location_indicator_layer_properties.cpp +++ b/src/mln/style/layers/location_indicator_layer_properties.cpp @@ -37,6 +37,10 @@ expression::Dependency LocationIndicatorLayerProperties::getEvaluatedDependencie return evaluated.getDependencies(); } +void LocationIndicatorLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/location_indicator_layer_properties.hpp b/src/mln/style/layers/location_indicator_layer_properties.hpp index 312d477ae30b..6796683e9637 100644 --- a/src/mln/style/layers/location_indicator_layer_properties.hpp +++ b/src/mln/style/layers/location_indicator_layer_properties.hpp @@ -104,6 +104,8 @@ class LocationIndicatorLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const LocationIndicatorLayer::Impl& layerImpl() const noexcept; // Data members. LocationIndicatorPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/raster_layer_properties.cpp b/src/mln/style/layers/raster_layer_properties.cpp index 2cb993bb7103..9caf8e41df41 100644 --- a/src/mln/style/layers/raster_layer_properties.cpp +++ b/src/mln/style/layers/raster_layer_properties.cpp @@ -37,6 +37,10 @@ expression::Dependency RasterLayerProperties::getEvaluatedDependencies() const n return evaluated.getDependencies(); } +void RasterLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/raster_layer_properties.hpp b/src/mln/style/layers/raster_layer_properties.hpp index bf159cfadfbb..096a11a230b5 100644 --- a/src/mln/style/layers/raster_layer_properties.hpp +++ b/src/mln/style/layers/raster_layer_properties.hpp @@ -73,6 +73,8 @@ class RasterLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const RasterLayer::Impl& layerImpl() const noexcept; // Data members. RasterPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/layers/symbol_layer_impl.hpp b/src/mln/style/layers/symbol_layer_impl.hpp index 9058f56f7cf8..36128f11f861 100644 --- a/src/mln/style/layers/symbol_layer_impl.hpp +++ b/src/mln/style/layers/symbol_layer_impl.hpp @@ -133,6 +133,11 @@ class SymbolLayer::Impl : public Layer::Impl { return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); } + void collectLayoutGlobalStateRefs(std::set& refs) const override { + layout.collectGlobalStateRefs(refs); + Layer::Impl::collectLayoutGlobalStateRefs(refs); + } + SymbolLayoutProperties::Unevaluated layout; SymbolPaintProperties::Transitionable paint; diff --git a/src/mln/style/layers/symbol_layer_properties.cpp b/src/mln/style/layers/symbol_layer_properties.cpp index d3e8c5a4bcaf..9437cb9a4363 100644 --- a/src/mln/style/layers/symbol_layer_properties.cpp +++ b/src/mln/style/layers/symbol_layer_properties.cpp @@ -37,6 +37,10 @@ expression::Dependency SymbolLayerProperties::getEvaluatedDependencies() const n return evaluated.getDependencies(); } +void SymbolLayerProperties::collectEvaluatedGlobalStateRefs(std::set& refs) const { + evaluated.collectGlobalStateRefs(refs); +} + } // namespace style } // namespace mln diff --git a/src/mln/style/layers/symbol_layer_properties.hpp b/src/mln/style/layers/symbol_layer_properties.hpp index bebf735029a3..eb4e95bc0066 100644 --- a/src/mln/style/layers/symbol_layer_properties.hpp +++ b/src/mln/style/layers/symbol_layer_properties.hpp @@ -367,6 +367,8 @@ class SymbolLayerProperties final : public LayerProperties { expression::Dependency getEvaluatedDependencies() const noexcept override; + void collectEvaluatedGlobalStateRefs(std::set&) const override; + const SymbolLayer::Impl& layerImpl() const noexcept; // Data members. SymbolPaintProperties::PossiblyEvaluated evaluated; diff --git a/src/mln/style/properties.hpp b/src/mln/style/properties.hpp index e18d7af499e6..7f9632416af9 100644 --- a/src/mln/style/properties.hpp +++ b/src/mln/style/properties.hpp @@ -279,6 +279,31 @@ class Properties { return result; } + /// Collect the names of the global-state properties referenced by + /// the expressions retained in the evaluated properties. + void collectGlobalStateRefs(std::set& refs) const { + util::ignore({(collectGlobalStateRefs(this->template get(), refs), 0)...}); + } + + private: + template + void collectGlobalStateRefs(const P&, std::set&) const noexcept {} + template + void collectGlobalStateRefs(const PossiblyEvaluatedPropertyValue

& v, std::set& refs) const { + if (const auto* propertyRefs = v.getGlobalStateRefs()) { + refs.insert(propertyRefs->begin(), propertyRefs->end()); + } + } + template + void collectGlobalStateRefs(const PossiblyEvaluatedPropertyValue>& v, + std::set& refs) const { + if (const auto* propertyRefs = v.getGlobalStateRefs()) { + refs.insert(propertyRefs->begin(), propertyRefs->end()); + } + } + + public: + unsigned long constantsMask() const noexcept { return ConstantsMask::getMask(*this); } }; @@ -313,7 +338,9 @@ class Properties { const bool needEvaluate = parameters.layerChanged || parameters.hasCrossfade || property.hasTransition() || (parameters.zoomChanged && (getDependencies(property) & Dependency::Zoom)) || (parameters.globalStateChanged && - (getDependencies(property) & Dependency::GlobalState)); + (getDependencies(property) & Dependency::GlobalState) && + expression::globalStateRefsIntersect(getGlobalStateRefs(property), + parameters.changedGlobalStateKeys.get())); return needEvaluate ? property.evaluate(Evaluator(parameters, P::defaultValue()), parameters.now) : oldResult; } @@ -362,6 +389,34 @@ class Properties { return v.getValue().getDependencies(); } + // gather referenced global-state property names, mirroring getDependencies + + template + const std::set* getGlobalStateRefs(const PropertyValue

& v) const noexcept { + return v.getGlobalStateRefs(); + } + + template + const std::set* getGlobalStateRefs(const Transitioning

& v) const noexcept { + return v.getValue().getGlobalStateRefs(); + } + + template + void collectGlobalStateRefs(const P& v, std::set& refs) const { + if (const auto* propertyRefs = getGlobalStateRefs(v)) { + refs.insert(propertyRefs->begin(), propertyRefs->end()); + } + } + + public: + /// Collect the names of the global-state properties referenced by all + /// contained expressions. + void collectGlobalStateRefs(std::set& refs) const { + util::ignore({(collectGlobalStateRefs(this->template get(), refs), 0)...}); + } + + protected: + template bool updateGPUExpression(Unevaluated::GPUExpressions& exprs, TimePoint now) const { constexpr auto index = TypeIndex::value; diff --git a/src/mln/style/property_expression.cpp b/src/mln/style/property_expression.cpp index a2a953b5c4e0..4671d5539247 100644 --- a/src/mln/style/property_expression.cpp +++ b/src/mln/style/property_expression.cpp @@ -15,10 +15,20 @@ bool checkGPUCapable(const Expression& expression, const ZoomCurvePtr& zoomCurve return (expression.dependencies == Dependency::Zoom) && !zoomCurve.is() && (expression.getType().is() || expression.getType().is()); } + +std::shared_ptr> collectGlobalStateRefsShared(const Expression& expression) { + if (!expression.has(Dependency::GlobalState)) { + return nullptr; + } + auto refs = std::make_shared>(); + collectGlobalStateRefs(expression, *refs); + return refs; +} } // namespace PropertyExpressionBase::PropertyExpressionBase(std::unique_ptr expression_) : expression(std::move(expression_)), + globalStateRefs_(collectGlobalStateRefsShared(*expression)), zoomCurve(expression->has(Dependency::Zoom) ? expression::findZoomCurveChecked(*expression) : nullptr), useIntegerZoom_(false), isZoomConstant_(!expression->has(Dependency::Zoom)), @@ -33,6 +43,7 @@ PropertyExpressionBase::PropertyExpressionBase(std::unique_ptr refs; + collectGlobalStateRefs(*expr, refs); + EXPECT_EQ((std::set{"a", "b"}), refs); + + // Expressions without global-state references collect nothing. + auto plain = parseExpression(R"(["to-number", ["get", "width"]])", {type::Number}); + ASSERT_TRUE(plain); + refs.clear(); + collectGlobalStateRefs(*plain, refs); + EXPECT_TRUE(refs.empty()); +} + +TEST(GlobalStateExpression, PropertyExpressionGlobalStateRefs) { + auto expr = parseExpression(R"(["to-number", ["global-state", "width"]])", {type::Number}); + ASSERT_TRUE(expr); + PropertyExpression propertyExpression(std::move(expr)); + ASSERT_TRUE(propertyExpression.getGlobalStateRefs()); + EXPECT_EQ((std::set{"width"}), *propertyExpression.getGlobalStateRefs()); + + auto plain = parseExpression(R"(["to-number", ["get", "width"]])", {type::Number}); + ASSERT_TRUE(plain); + PropertyExpression plainExpression(std::move(plain)); + EXPECT_EQ(nullptr, plainExpression.getGlobalStateRefs()); +} + +TEST(GlobalStateExpression, GlobalStateRefsIntersect) { + const std::set refs{"a", "b"}; + const std::set changedHit{"b", "c"}; + const std::set changedMiss{"c", "d"}; + + EXPECT_TRUE(globalStateRefsIntersect(&refs, &changedHit)); + EXPECT_FALSE(globalStateRefsIntersect(&refs, &changedMiss)); + // Unknown refs or unknown changed keys are conservatively a match. + EXPECT_TRUE(globalStateRefsIntersect(nullptr, &changedHit)); + EXPECT_TRUE(globalStateRefsIntersect(&refs, nullptr)); +} + TEST(GlobalStateExpression, Serialize) { auto expr = parseExpression(R"(["global-state", "showLabels"])"); ASSERT_TRUE(expr); From a1d9a179dfeac94a53d3b9c3394cecdb73294aee Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Sun, 23 Aug 2026 19:48:34 +0900 Subject: [PATCH 03/32] refactor(core): remove redundant comments from global-state implementation --- include/mln/style/color_ramp_property_value.hpp | 2 -- include/mln/style/expression/expression.hpp | 3 +-- .../style/expression/format_section_override.hpp | 5 ++--- include/mln/style/layer_properties.hpp | 4 ++-- include/mln/style/property_expression.hpp | 4 +--- include/mln/style/property_value.hpp | 2 -- .../renderer/layers/render_color_relief_layer.hpp | 2 -- src/mln/renderer/layers/render_heatmap_layer.hpp | 2 -- src/mln/renderer/layers/render_line_layer.hpp | 2 -- .../renderer/possibly_evaluated_property_value.hpp | 4 ---- src/mln/renderer/property_evaluation_parameters.hpp | 5 ++--- src/mln/renderer/render_orchestrator.cpp | 7 ------- src/mln/style/layer.cpp | 3 +-- src/mln/style/layer_impl.hpp | 13 +++++-------- src/mln/style/properties.hpp | 4 ---- src/mln/style/style_impl.cpp | 3 +-- 16 files changed, 15 insertions(+), 50 deletions(-) diff --git a/include/mln/style/color_ramp_property_value.hpp b/include/mln/style/color_ramp_property_value.hpp index fd63f5554c07..22564eaac4dd 100644 --- a/include/mln/style/color_ramp_property_value.hpp +++ b/include/mln/style/color_ramp_property_value.hpp @@ -61,8 +61,6 @@ class ColorRampPropertyValue { using Dependency = style::expression::Dependency; Dependency getDependencies() const noexcept { return value ? value->dependencies : Dependency::None; } - /// The names of the global-state properties referenced by the expression, - /// or null if none are referenced. const std::set* getGlobalStateRefs() const noexcept { return globalStateRefs_.get(); } }; diff --git a/include/mln/style/expression/expression.hpp b/include/mln/style/expression/expression.hpp index 897811566106..0ead0f6e8fb0 100644 --- a/include/mln/style/expression/expression.hpp +++ b/include/mln/style/expression/expression.hpp @@ -391,8 +391,7 @@ class Expression { void collectGlobalStateRefs(const Expression&, std::set& refs); /// Whether any of the global-state keys referenced by an expression is among -/// the changed keys. A null `refs` means the referenced keys are unknown and -/// a null `changedKeys` means the changed keys are unknown; either is +/// the changed keys. A null set on either side means "unknown" and is /// conservatively treated as a match. inline bool globalStateRefsIntersect(const std::set* refs, const std::set* changedKeys) { if (refs == nullptr || changedKeys == nullptr) { diff --git a/include/mln/style/expression/format_section_override.hpp b/include/mln/style/expression/format_section_override.hpp index b4a7fbcca683..e7f8e6ab7374 100644 --- a/include/mln/style/expression/format_section_override.hpp +++ b/include/mln/style/expression/format_section_override.hpp @@ -28,9 +28,8 @@ class FormatSectionOverride final : public Expression { return defaultValue.match( [&context](const style::PropertyExpression& e) { - // Fall back to the global state captured by the wrapped - // property expression, which would otherwise be bypassed by - // evaluating the raw inner expression directly. + // Evaluating the raw inner expression would bypass the + // captured-global-state fallback of the property expression. if (context.globalState == nullptr && e.getCapturedGlobalState() != nullptr) { EvaluationContext contextWithState = context; contextWithState.globalState = e.getCapturedGlobalState().get(); diff --git a/include/mln/style/layer_properties.hpp b/include/mln/style/layer_properties.hpp index f64773606311..3ccea9369a21 100644 --- a/include/mln/style/layer_properties.hpp +++ b/include/mln/style/layer_properties.hpp @@ -31,8 +31,8 @@ class LayerProperties { /// evaluation, such as data-driven paint properties). virtual expression::Dependency getEvaluatedDependencies() const noexcept { return expression::Dependency::None; } - /// Collect the names of the global-state properties referenced by the - /// expressions retained in the evaluated properties. + /// Collect the global-state properties referenced by the expressions + /// retained in the evaluated properties, mirroring getEvaluatedDependencies(). virtual void collectEvaluatedGlobalStateRefs(std::set&) const {} protected: diff --git a/include/mln/style/property_expression.hpp b/include/mln/style/property_expression.hpp index 26b3c550448f..61e3f8a69503 100644 --- a/include/mln/style/property_expression.hpp +++ b/include/mln/style/property_expression.hpp @@ -58,9 +58,7 @@ class PropertyExpressionBase { return capturedGlobalState_; } - /// The names of the global-state properties referenced by the expression, - /// collected once at construction. Null if the expression does not - /// reference the global state. + /// Null if the expression does not reference the global state. const std::set* getGlobalStateRefs() const noexcept { return globalStateRefs_.get(); } /// Can be used for aggregating property expressions from multiple properties(layers) into single match / case diff --git a/include/mln/style/property_value.hpp b/include/mln/style/property_value.hpp index a4fe565e6b06..1ffb98dc3149 100644 --- a/include/mln/style/property_value.hpp +++ b/include/mln/style/property_value.hpp @@ -73,8 +73,6 @@ class PropertyValue { [](const PropertyExpression& ex) { return ex.getDependencies(); }); } - /// The names of the global-state properties referenced by a contained - /// expression, or null if none are referenced. const std::set* getGlobalStateRefs() const noexcept { return value.match([](const Undefined&) -> const std::set* { return nullptr; }, [](const T&) -> const std::set* { return nullptr; }, diff --git a/src/mln/renderer/layers/render_color_relief_layer.hpp b/src/mln/renderer/layers/render_color_relief_layer.hpp index 95058d4abb21..cd5f9fee01ad 100644 --- a/src/mln/renderer/layers/render_color_relief_layer.hpp +++ b/src/mln/renderer/layers/render_color_relief_layer.hpp @@ -52,8 +52,6 @@ class RenderColorReliefLayer final : public RenderLayer { // Color ramp data uint32_t colorRampSize = 256; bool colorRampChanged = true; - // The style's global state as of the last evaluation, used to resolve - // "global-state" expressions in the color ramp. std::shared_ptr colorRampGlobalState; // FIX 1: Changed type and name to correctly hold float elevation data diff --git a/src/mln/renderer/layers/render_heatmap_layer.hpp b/src/mln/renderer/layers/render_heatmap_layer.hpp index 53d13ec1bdea..7c7112133082 100644 --- a/src/mln/renderer/layers/render_heatmap_layer.hpp +++ b/src/mln/renderer/layers/render_heatmap_layer.hpp @@ -60,8 +60,6 @@ class RenderHeatmapLayer final : public RenderLayer { // Paint properties style::HeatmapPaintProperties::Unevaluated unevaluated; std::shared_ptr colorRamp; - // The style's global state as of the last evaluation, used to resolve - // "global-state" expressions in the color ramp. std::shared_ptr colorRampGlobalState; std::unique_ptr renderTexture; SegmentVector segments; diff --git a/src/mln/renderer/layers/render_line_layer.hpp b/src/mln/renderer/layers/render_line_layer.hpp index 6df975e5ae8d..bf587782b451 100644 --- a/src/mln/renderer/layers/render_line_layer.hpp +++ b/src/mln/renderer/layers/render_line_layer.hpp @@ -58,8 +58,6 @@ class RenderLineLayer final : public RenderLayer { void updateColorRamp(); std::shared_ptr colorRamp; - // The style's global state as of the last evaluation, used to resolve - // "global-state" expressions in the color ramp. std::shared_ptr colorRampGlobalState; gfx::Texture2DPtr colorRampTexture2D; diff --git a/src/mln/renderer/possibly_evaluated_property_value.hpp b/src/mln/renderer/possibly_evaluated_property_value.hpp index 4a1ee0927d51..bbffdc798fca 100644 --- a/src/mln/renderer/possibly_evaluated_property_value.hpp +++ b/src/mln/renderer/possibly_evaluated_property_value.hpp @@ -71,8 +71,6 @@ class PossiblyEvaluatedPropertyValue { [](const style::PropertyExpression& expression) { return expression.getDependencies(); }); } - /// The names of the global-state properties referenced by a retained - /// expression, or null if none are referenced. const std::set* getGlobalStateRefs() const noexcept { return value.match( [](const T&) -> const std::set* { return nullptr; }, @@ -133,8 +131,6 @@ class PossiblyEvaluatedPropertyValue> { [](const style::PropertyExpression& expression) { return expression.getDependencies(); }); } - /// The names of the global-state properties referenced by a retained - /// expression, or null if none are referenced. const std::set* getGlobalStateRefs() const noexcept { return value.match( [](const Faded&) -> const std::set* { return nullptr; }, diff --git a/src/mln/renderer/property_evaluation_parameters.hpp b/src/mln/renderer/property_evaluation_parameters.hpp index 4d7ff6fc46f9..47d9bc6f72f5 100644 --- a/src/mln/renderer/property_evaluation_parameters.hpp +++ b/src/mln/renderer/property_evaluation_parameters.hpp @@ -51,9 +51,8 @@ class PropertyEvaluationParameters { /// The style's current global state, used to evaluate `global-state` expressions. std::shared_ptr globalState; - /// The names of the global-state properties that changed since the last - /// evaluation. Only meaningful when `globalStateChanged` is set; null - /// means the changed keys are unknown (treat all as changed). + /// The global-state properties that changed since the last evaluation; + /// null means the changed keys are unknown (treat all as changed). std::shared_ptr> changedGlobalStateKeys; bool zoomChanged = true; diff --git a/src/mln/renderer/render_orchestrator.cpp b/src/mln/renderer/render_orchestrator.cpp index 673c87ba4a31..602f6ba764cf 100644 --- a/src/mln/renderer/render_orchestrator.cpp +++ b/src/mln/renderer/render_orchestrator.cpp @@ -203,9 +203,6 @@ std::unique_ptr RenderOrchestrator::createRenderTree( isMapModeContinuous ? util::DEFAULT_TRANSITION_DURATION : Duration::zero()); const bool globalStateChanged = globalState != updateParameters->globalState; - // The keys whose values changed, so that only the layers and properties - // referencing those keys are re-evaluated or relayouted. Null when the - // previous state is unknown (treat all keys as changed). std::shared_ptr> changedGlobalStateKeys; if (globalStateChanged && globalState && updateParameters->globalState) { changedGlobalStateKeys = std::make_shared>( @@ -690,8 +687,6 @@ std::vector RenderOrchestrator::queryRenderedFeatures( const std::unordered_map& layers) const { MLN_TRACE_FUNC(); - // Inject the current global state so that "global-state" expressions in - // the query filter evaluate against the state that was rendered. RenderedQueryOptions options = options_; options.globalState = globalState; @@ -770,8 +765,6 @@ std::vector RenderOrchestrator::querySourceFeatures(const std::string& const RenderSource* source = getRenderSource(sourceID); if (!source) return {}; - // Inject the current global state so that "global-state" expressions in - // the query filter evaluate against the current state. SourceQueryOptions options = options_; options.globalState = globalState; diff --git a/src/mln/style/layer.cpp b/src/mln/style/layer.cpp index 156d3a7accab..e45a85e1c47a 100644 --- a/src/mln/style/layer.cpp +++ b/src/mln/style/layer.cpp @@ -260,8 +260,7 @@ std::optional Layer::setVisibility(const conversion::Converti } if (isArray(value)) { - // Visibility given as an expression. Per the style specification such - // expressions are data-constant and may only use "global-state". + // Per the style specification, visibility expressions may only use "global-state". expression::ParsingContext ctx(expression::type::String); expression::ParseResult parsed = ctx.parseExpression(value); if (!parsed) { diff --git a/src/mln/style/layer_impl.hpp b/src/mln/style/layer_impl.hpp index 1e70a470ecaa..9005635ccdab 100644 --- a/src/mln/style/layer_impl.hpp +++ b/src/mln/style/layer_impl.hpp @@ -63,20 +63,17 @@ class Layer::Impl { : expression::Dependency::None; } - /// Collect the names of the global-state properties referenced by - /// expressions that affect tile layout: the filter and the layout - /// properties. Derived classes with layout properties combine those with - /// the filter references from this base implementation. + /// Collect the global-state properties referenced by expressions that + /// affect tile layout, mirroring getLayoutDependencies(). virtual void collectLayoutGlobalStateRefs(std::set& refs) const { if (filter.expression && *filter.expression) { expression::collectGlobalStateRefs(**filter.expression, refs); } } - // Optional expression backing the "visibility" layout value. Per the - // style specification, such expressions may only depend on the global - // state; the evaluated result is kept in `visibility` and re-evaluated - // by the style whenever the global state changes. + // Backs an expression-valued "visibility" layout value; the evaluated + // result is kept in `visibility` and re-evaluated by the style whenever + // the global state changes. std::shared_ptr visibilityExpression; std::string id; diff --git a/src/mln/style/properties.hpp b/src/mln/style/properties.hpp index 7f9632416af9..2b6d56060b74 100644 --- a/src/mln/style/properties.hpp +++ b/src/mln/style/properties.hpp @@ -279,8 +279,6 @@ class Properties { return result; } - /// Collect the names of the global-state properties referenced by - /// the expressions retained in the evaluated properties. void collectGlobalStateRefs(std::set& refs) const { util::ignore({(collectGlobalStateRefs(this->template get(), refs), 0)...}); } @@ -409,8 +407,6 @@ class Properties { } public: - /// Collect the names of the global-state properties referenced by all - /// contained expressions. void collectGlobalStateRefs(std::set& refs) const { util::ignore({(collectGlobalStateRefs(this->template get(), refs), 0)...}); } diff --git a/src/mln/style/style_impl.cpp b/src/mln/style/style_impl.cpp index 16f6076e3583..3d2143181cab 100644 --- a/src/mln/style/style_impl.cpp +++ b/src/mln/style/style_impl.cpp @@ -308,8 +308,7 @@ Layer* Style::Impl::addLayer(std::unique_ptr layer, const std::optionalsetObserver(this); Layer* result = layers.add(std::move(layer), before); - // Resolve any "global-state" backed visibility expression against the - // current global state (during parsing it could not be applied yet). + // The global state is not available while the layer itself is parsed. result->reevaluateVisibility(*globalState); observer->onUpdate(); From 0d1c9cc74e02e8197c2eef6ed002721ede88375c Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 13:43:26 +0900 Subject: [PATCH 04/32] feat(android): add global-state runtime API and expression builder --- CHANGELOG.md | 1 + .../src/cpp/native_map_view.cpp | 17 ++++++++++ .../src/cpp/native_map_view.hpp | 4 +++ .../org/maplibre/android/maps/NativeMap.java | 6 ++++ .../maplibre/android/maps/NativeMapView.java | 25 ++++++++++++++ .../java/org/maplibre/android/maps/Style.java | 34 +++++++++++++++++++ .../android/style/expressions/Expression.java | 26 ++++++++++++++ .../style/expressions/ExpressionTest.kt | 8 +++++ .../testapp/style/RuntimeStyleTests.java | 27 +++++++++++++++ 9 files changed, 148 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0bd432c266..07788e031cb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - *...Add new stuff here...* - [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState`. The expression is supported in filters, layout and paint properties (including color ramps such as `heatmap-color` and `line-gradient`), the `visibility` layout property, and feature query filters. State changes track the referenced property names, so only the layers and sources that reference a changed property are re-evaluated or relayouted ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). +- [android] Add `Style#setGlobalStateProperty` / `Style#getGlobalState` and the `Expression.globalState` builder for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [core] Locally rasterized CJK glyphs now render at 2x texture resolution, preserving fine strokes. Mirrors [maplibre-gl-js#3006](https://github.com/maplibre/maplibre-gl-js/pull/3006). - [core] Added new map observer events: onPreCompileShader, onPostCompileShader, onShaderCompileFailed, onGlyphsLoaded, onGlyphsError, onGlyphsRequested, onTileAction, onSpriteLoaded, onSpriteError, onSpriteRequested ([#2694](https://github.com/maplibre/maplibre-native/pull/2694)). - [core] Add WebP image decoding support to default platform (Linux, Windows) diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index 9c7c30a1971e..930c7e81af8b 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -1068,6 +1068,21 @@ void NativeMapView::removeFeatureState(JNIEnv& env, map->triggerRepaint(); } +void NativeMapView::setGlobalStateProperty(JNIEnv& env, + const jni::String& name, + const jni::Object& value) { + if (!name) { + return; + } + + map->getStyle().setGlobalStateProperty(jni::Make(env, name), + value ? gson::JsonElement::convert(env, value) : mln::Value()); +} + +jni::Local> NativeMapView::getGlobalState(JNIEnv& env) { + return gson::JsonObject::New(env, map->getStyle().getGlobalState()); +} + jni::Local> NativeMapView::getLight(JNIEnv& env) { mln::style::Light* light = map->getStyle().getLight(); if (light) { @@ -1484,6 +1499,8 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::setFeatureState, "nativeSetFeatureState"), METHOD(&NativeMapView::getFeatureState, "nativeGetFeatureState"), METHOD(&NativeMapView::removeFeatureState, "nativeRemoveFeatureState"), + METHOD(&NativeMapView::setGlobalStateProperty, "nativeSetGlobalStateProperty"), + METHOD(&NativeMapView::getGlobalState, "nativeGetGlobalState"), METHOD(&NativeMapView::getLight, "nativeGetLight"), METHOD(&NativeMapView::getLayers, "nativeGetLayers"), METHOD(&NativeMapView::getLayer, "nativeGetLayer"), diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp index 26a4abafeb2e..73822e0a046b 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp @@ -276,6 +276,10 @@ class NativeMapView : public MapObserver { const jni::String& featureId, const jni::String& stateKey); + void setGlobalStateProperty(JNIEnv&, const jni::String& name, const jni::Object& value); + + jni::Local> getGlobalState(JNIEnv&); + jni::Local> getLight(JNIEnv&); jni::Local>> getLayers(JNIEnv&); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java index 24ae9491e8e6..ae8f56695aaa 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.maplibre.geojson.Feature; import org.maplibre.geojson.Geometry; @@ -206,6 +207,11 @@ void removeFeatureState(@NonNull String sourceId, @Nullable String featureId, @Nullable String stateKey); + void setGlobalStateProperty(@NonNull String name, @Nullable JsonElement value); + + @NonNull + JsonObject getGlobalState(); + // // Projection API // diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java index 55d3228e8863..e414d2ddffbc 100755 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.maplibre.geojson.Feature; import org.maplibre.geojson.Geometry; @@ -1157,6 +1158,23 @@ public void removeFeatureState(@NonNull String sourceId, nativeRemoveFeatureState(sourceId, sourceLayerId, featureId, stateKey); } + @Override + public void setGlobalStateProperty(@NonNull String name, @Nullable JsonElement value) { + if (checkState("setGlobalStateProperty")) { + return; + } + nativeSetGlobalStateProperty(name, value); + } + + @Override + @NonNull + public JsonObject getGlobalState() { + if (checkState("getGlobalState")) { + return new JsonObject(); + } + return nativeGetGlobalState(); + } + @Override public void setApiBaseUrl(String baseUrl) { if (checkState("setApiBaseUrl")) { @@ -1770,6 +1788,13 @@ private native void nativeRemoveFeatureState(String sourceId, String featureId, String stateKey); + @Keep + private native void nativeSetGlobalStateProperty(String name, JsonElement value); + + @NonNull + @Keep + private native JsonObject nativeGetGlobalState(); + @NonNull @Keep private native Light nativeGetLight(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Style.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Style.java index 936a562fa5aa..8ed842f8b5ca 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Style.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Style.java @@ -10,6 +10,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + import org.maplibre.android.MapLibre; import org.maplibre.android.constants.MapLibreConstants; import org.maplibre.android.log.Logger; @@ -683,6 +686,37 @@ public TransitionOptions getTransition() { return nativeMap.getTransitionOptions(); } + // + // Global state + // + + /** + * Set a global state property, used by the + * global-state expression. + *

+ * Setting a {@code null} value (or {@link com.google.gson.JsonNull}) resets the property to the default + * defined in the style's root {@code state} property, or to null if there is none. + * + * @param name the name of the state property + * @param value the new value of the state property + */ + public void setGlobalStateProperty(@NonNull String name, @Nullable JsonElement value) { + validateState("setGlobalStateProperty"); + nativeMap.setGlobalStateProperty(name, value); + } + + /** + * Get a snapshot of the map's current global state, used by the + * global-state expression. + * + * @return the current global state + */ + @NonNull + public JsonObject getGlobalState() { + validateState("getGlobalState"); + return nativeMap.getGlobalState(); + } + // // Light // diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/expressions/Expression.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/expressions/Expression.java index 3f1a64618adc..aba7c8430f56 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/expressions/Expression.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/expressions/Expression.java @@ -1418,6 +1418,32 @@ public static Expression geometryType() { return new Expression("geometry-type"); } + /** + * Gets a property value from the map's global state. + * Returns null if the requested property is not present in the global state. + *

+ * The global state can be set with {@link org.maplibre.android.maps.Style#setGlobalStateProperty(String, + * com.google.gson.JsonElement)} and defaults can be defined with the style's root {@code state} property. + *

+ * Example usage: + *

+ *
+   * {@code
+   * CircleLayer circleLayer = new CircleLayer("layer-id", "source-id");
+   * circleLayer.setFilter(
+   *     eq(get("charger-type"), globalState("chargerType"))
+   * );
+   * }
+   * 
+ * + * @param property the name of the global state property + * @return expression + * @see Style specification + */ + public static Expression globalState(@NonNull String property) { + return new Expression("global-state", literal(property)); + } + /** * Gets the feature's id, if it has one. *

diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/style/expressions/ExpressionTest.kt b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/style/expressions/ExpressionTest.kt index c2799eaeb417..01026d3d08c4 100644 --- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/style/expressions/ExpressionTest.kt +++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/style/expressions/ExpressionTest.kt @@ -400,6 +400,14 @@ class ExpressionTest : BaseTest() { Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual)) } + @Test + @Throws(Exception::class) + fun testGlobalState() { + val expected = arrayOf("global-state", "chargerType") + val actual = Expression.globalState("chargerType").toArray() + Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual)) + } + @Test @Throws(Exception::class) fun testId() { diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java index 216c5cac863a..718482d7ddc5 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java @@ -8,6 +8,9 @@ import androidx.test.espresso.ViewAction; import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + import org.maplibre.android.style.layers.CannotAddLayerException; import org.maplibre.android.style.layers.BackgroundLayer; import org.maplibre.android.style.layers.CircleLayer; @@ -76,6 +79,30 @@ public void testGetAddRemoveLayer() { onView(withId(R.id.mapView)).perform(new AddRemoveLayerAction()); } + @Test + public void testGlobalState() { + validateTestSetup(); + onView(withId(R.id.mapView)).perform(new BaseViewAction() { + @Override + public void perform(UiController uiController, View view) { + Style style = maplibreMap.getStyle(); + assertNotNull(style); + + style.setGlobalStateProperty("showLabels", new JsonPrimitive(true)); + assertEquals(new JsonPrimitive(true), style.getGlobalState().get("showLabels")); + + style.setGlobalStateProperty("showLabels", new JsonPrimitive(false)); + assertEquals(new JsonPrimitive(false), style.getGlobalState().get("showLabels")); + + // A null value resets the property to the style's default, which is + // null since the test style defines no "state" property. + style.setGlobalStateProperty("showLabels", null); + JsonElement reset = style.getGlobalState().get("showLabels"); + assertTrue(reset == null || reset.isJsonNull()); + } + }); + } + @Test public void testAddLayerAbove() { validateTestSetup(); From 2a3353ad630dd5743aa4a3098d7ed2aa8575db4d Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 14:10:58 +0900 Subject: [PATCH 05/32] feat(ios): add global-state runtime API and NSExpression support --- CHANGELOG.md | 1 + platform/darwin/src/MLNStyle.h | 23 ++++++++++ platform/darwin/src/MLNStyle.mm | 17 ++++++++ platform/darwin/src/MLNStyleValue.mm | 42 +++++++++++++++++++ platform/darwin/src/MLNStyleValue_Private.h | 2 + .../darwin/src/NSExpression+MLNAdditions.mm | 12 ++++++ platform/darwin/test/MLNExpressionTests.mm | 10 +++++ platform/darwin/test/MLNStyleTests.mm | 16 +++++++ 8 files changed, 123 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07788e031cb4..10075ac2a58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - *...Add new stuff here...* - [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState`. The expression is supported in filters, layout and paint properties (including color ramps such as `heatmap-color` and `line-gradient`), the `visibility` layout property, and feature query filters. State changes track the referenced property names, so only the layers and sources that reference a changed property are re-evaluated or relayouted ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [android] Add `Style#setGlobalStateProperty` / `Style#getGlobalState` and the `Expression.globalState` builder for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). +- [ios] Add `MLNStyle.globalState` / `-[MLNStyle setGlobalStateValue:forProperty:]` and `NSExpression` support (`mgl_globalState:`) for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [core] Locally rasterized CJK glyphs now render at 2x texture resolution, preserving fine strokes. Mirrors [maplibre-gl-js#3006](https://github.com/maplibre/maplibre-gl-js/pull/3006). - [core] Added new map observer events: onPreCompileShader, onPostCompileShader, onShaderCompileFailed, onGlyphsLoaded, onGlyphsError, onGlyphsRequested, onTileAction, onSpriteLoaded, onSpriteError, onSpriteRequested ([#2694](https://github.com/maplibre/maplibre-native/pull/2694)). - [core] Add WebP image decoding support to default platform (Linux, Windows) diff --git a/platform/darwin/src/MLNStyle.h b/platform/darwin/src/MLNStyle.h index 54757147aa20..6d288aaa6789 100644 --- a/platform/darwin/src/MLNStyle.h +++ b/platform/darwin/src/MLNStyle.h @@ -104,6 +104,29 @@ MLN_EXPORT */ @property (nonatomic, assign) BOOL performsPlacementTransitions; +// MARK: Global State + +/** + A snapshot of the map's current global state, used by the `global-state` + style expression. + + Initial values are the defaults defined in the style's root `state` property. + */ +@property (nonatomic, readonly, copy) NSDictionary *globalState; + +/** + Sets a global state property, used by the `global-state` style expression. + + Setting a value of `nil` or `NSNull` resets the property to the default + defined in the style's root `state` property, or to null if there is none. + + @param value The new value of the state property. May be a Foundation + instance corresponding to a JSON value: `NSString`, `NSNumber`, `NSArray`, + `NSDictionary`, or `NSNull`. + @param propertyName The name of the state property. + */ +- (void)setGlobalStateValue:(nullable id)value forProperty:(NSString *)propertyName; + /** Returns a source with the given identifier in the current style. diff --git a/platform/darwin/src/MLNStyle.mm b/platform/darwin/src/MLNStyle.mm index 224e413fa64b..2dcc13c620b4 100644 --- a/platform/darwin/src/MLNStyle.mm +++ b/platform/darwin/src/MLNStyle.mm @@ -27,6 +27,7 @@ #import "MLNAttributionInfo_Private.h" #import "MLNLoggingConfiguration_Private.h" +#import "MLNStyleValue_Private.h" #include #include @@ -533,6 +534,22 @@ - (MLNImage *)imageForName:(NSString *)name { return styleImage ? [[MLNImage alloc] initWithMLNStyleImage:*styleImage] : nil; } +// MARK: Global state + +- (NSDictionary *)globalState { + const mln::GlobalStateMap state = self.rawStyle->getGlobalState(); + NSMutableDictionary *globalState = [NSMutableDictionary dictionaryWithCapacity:state.size()]; + for (const auto &property : state) { + globalState[@(property.first.c_str())] = MLNJSONObjectFromMBGLValue(property.second); + } + return [globalState copy]; +} + +- (void)setGlobalStateValue:(id)value forProperty:(NSString *)propertyName { + MLNLogDebug(@"Setting global state property: %@", propertyName); + self.rawStyle->setGlobalStateProperty(propertyName.UTF8String, MLNValueFromJSONObject(value)); +} + // MARK: Style transitions - (void)setTransition:(MLNTransition)transition { diff --git a/platform/darwin/src/MLNStyleValue.mm b/platform/darwin/src/MLNStyleValue.mm index 7c5297d196c5..446de0d807c4 100644 --- a/platform/darwin/src/MLNStyleValue.mm +++ b/platform/darwin/src/MLNStyleValue.mm @@ -42,6 +42,48 @@ id MLNJSONObjectFromMBGLExpression(const mln::style::expression::Expression &mbg return MLNJSONObjectFromMBGLValue(mbglExpression.serialize()); } +mln::Value MLNValueFromJSONObject(id object) { + if (!object || object == [NSNull null]) { + return {}; + } + if ([object isKindOfClass:[NSString class]]) { + return {std::string([(NSString *)object UTF8String])}; + } + if ([object isKindOfClass:[NSNumber class]]) { + NSNumber *number = (NSNumber *)object; + if ((strcmp([number objCType], @encode(char)) == 0) || + (strcmp([number objCType], @encode(BOOL)) == 0)) { + return {(bool)number.boolValue}; + } + if ((strcmp([number objCType], @encode(double)) == 0) || + (strcmp([number objCType], @encode(float)) == 0)) { + return {(double)number.doubleValue}; + } + if ([number compare:@(0)] == NSOrderedAscending) { + return {(int64_t)number.longLongValue}; + } + return {(uint64_t)number.unsignedLongLongValue}; + } + if ([object isKindOfClass:[NSArray class]]) { + mapbox::base::ValueArray array; + array.reserve([(NSArray *)object count]); + for (id element in (NSArray *)object) { + array.push_back(MLNValueFromJSONObject(element)); + } + return array; + } + if ([object isKindOfClass:[NSDictionary class]]) { + mapbox::base::ValueObject dictionary; + NSDictionary *dictionaryObject = (NSDictionary *)object; + for (NSString *key in dictionaryObject) { + dictionary.emplace(std::string(key.UTF8String), MLNValueFromJSONObject(dictionaryObject[key])); + } + return dictionary; + } + [NSException raise:NSInvalidArgumentException format:@"Can’t convert %@ to mln::Value", object]; + return {}; +} + std::unique_ptr MLNClusterPropertyFromNSExpression( NSExpression *expression) { if (!expression) { diff --git a/platform/darwin/src/MLNStyleValue_Private.h b/platform/darwin/src/MLNStyleValue_Private.h index d2a14998ebcf..1979a09d3fb9 100644 --- a/platform/darwin/src/MLNStyleValue_Private.h +++ b/platform/darwin/src/MLNStyleValue_Private.h @@ -38,6 +38,8 @@ class Expression; id MLNJSONObjectFromMBGLValue(const mln::Value &value); +mln::Value MLNValueFromJSONObject(id object); + NS_INLINE MLNTransition MLNTransitionFromOptions(const mln::style::TransitionOptions &options) { MLNTransition transition; transition.duration = diff --git a/platform/darwin/src/NSExpression+MLNAdditions.mm b/platform/darwin/src/NSExpression+MLNAdditions.mm index 99b37fdb1f14..f0582146dc20 100644 --- a/platform/darwin/src/NSExpression+MLNAdditions.mm +++ b/platform/darwin/src/NSExpression+MLNAdditions.mm @@ -81,6 +81,7 @@ + (void)installFunctions { INSTALL_METHOD(mgl_tan:); INSTALL_METHOD(mgl_log2:); INSTALL_METHOD(mgl_distanceFrom:); + INSTALL_METHOD(mgl_globalState:); INSTALL_METHOD(mgl_attributed:); // Install functions that resemble control structures, taking arbitrary @@ -175,6 +176,15 @@ - (NSNumber *)mgl_distanceFrom:(id)object { return nil; } +/** + Returns the value of the given property in the map's global state. + */ +- (id)mgl_globalState:(NSString *)property { + [NSException raise:NSInvalidArgumentException + format:@"Global state expressions lack underlying Objective-C implementations."]; + return nil; +} + /** A placeholder for a method that evaluates an interpolation expression. */ @@ -717,6 +727,7 @@ + (instancetype)expressionWithMLNJSONObject:(id)object { @"ceil" : @"ceiling:", @"^" : @"raise:toPower:", @"distance" : @"mgl_distanceFrom:", + @"global-state" : @"mgl_globalState:", @"upcase" : @"uppercase:", @"downcase" : @"lowercase:", @"let" : @"MLN_LET", @@ -1068,6 +1079,7 @@ - (id)mgl_jsonExpressionObject { @"mgl_tan:" : @"tan", @"mgl_log2:" : @"log2", @"mgl_distanceFrom:" : @"distance", + @"mgl_globalState:" : @"global-state", // Vararg aftermarket expressions need to be declared with an explicit and implicit first // argument. @"MLN_LET" : @"let", diff --git a/platform/darwin/test/MLNExpressionTests.mm b/platform/darwin/test/MLNExpressionTests.mm index 214ce85c201e..2f844b6315e2 100644 --- a/platform/darwin/test/MLNExpressionTests.mm +++ b/platform/darwin/test/MLNExpressionTests.mm @@ -922,6 +922,16 @@ - (void)testShapeDistanceExpressionObject { } } +- (void)testGlobalStateExpressionObject { + { + NSArray *jsonExpression = @[ @"global-state", @"chargerType" ]; + NSExpression *expression = [NSExpression expressionWithMLNJSONObject:jsonExpression]; + XCTAssertEqualObjects(expression.mgl_jsonExpressionObject, jsonExpression); + XCTAssertThrowsSpecificNamed([expression expressionValueWithObject:nil context:nil], + NSException, NSInvalidArgumentException); + } +} + - (void)testStringFormattingExpressionObject { NSArray *arguments = @[ MLNConstantExpression(@"MacDonald") ]; { diff --git a/platform/darwin/test/MLNStyleTests.mm b/platform/darwin/test/MLNStyleTests.mm index 6733222f8b95..b69327e41c36 100644 --- a/platform/darwin/test/MLNStyleTests.mm +++ b/platform/darwin/test/MLNStyleTests.mm @@ -540,6 +540,22 @@ - (void)testLanguageMatching { // MARK: Transition tests +- (void)testGlobalState { + XCTAssertEqualObjects(self.style.globalState, @{}, + @"A style without a root state property should have an empty global state."); + + [self.style setGlobalStateValue:@YES forProperty:@"showLabels"]; + XCTAssertEqualObjects(self.style.globalState[@"showLabels"], @YES); + + [self.style setGlobalStateValue:@[ @"CCS", @"CHAdeMO" ] forProperty:@"chargerType"]; + XCTAssertEqualObjects(self.style.globalState[@"chargerType"], (@[ @"CCS", @"CHAdeMO" ])); + + // A nil value resets the property to the style's default, which is null + // since this style defines no root state property. + [self.style setGlobalStateValue:nil forProperty:@"showLabels"]; + XCTAssertEqualObjects(self.style.globalState[@"showLabels"], [NSNull null]); +} + - (void)testTransition { MLNTransition transitionTest = MLNTransitionMake(5, 4); From 89e3090fe2952342124154b766b90bd5e3cb9d0f Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 14:22:30 +0900 Subject: [PATCH 06/32] docs: remake changelog texts and document mgl_globalState --- CHANGELOG.md | 2 +- .../MapLibre.docc/Predicates_and_Expressions.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10075ac2a58f..58d5215ee82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### ✨ New features - *...Add new stuff here...* -- [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState`. The expression is supported in filters, layout and paint properties (including color ramps such as `heatmap-color` and `line-gradient`), the `visibility` layout property, and feature query filters. State changes track the referenced property names, so only the layers and sources that reference a changed property are re-evaluated or relayouted ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). +- [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property in filters, layout properties, and paint properties, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState` ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [android] Add `Style#setGlobalStateProperty` / `Style#getGlobalState` and the `Expression.globalState` builder for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [ios] Add `MLNStyle.globalState` / `-[MLNStyle setGlobalStateValue:forProperty:]` and `NSExpression` support (`mgl_globalState:`) for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [core] Locally rasterized CJK glyphs now render at 2x texture resolution, preserving fine strokes. Mirrors [maplibre-gl-js#3006](https://github.com/maplibre/maplibre-gl-js/pull/3006). diff --git a/platform/ios/MapLibre.docc/Predicates_and_Expressions.md b/platform/ios/MapLibre.docc/Predicates_and_Expressions.md index bb89fd0b6c0e..9f33bb56cb59 100644 --- a/platform/ios/MapLibre.docc/Predicates_and_Expressions.md +++ b/platform/ios/MapLibre.docc/Predicates_and_Expressions.md @@ -434,6 +434,21 @@ This function corresponds to the [`distance`](https://maplibre.org/maplibre-style-spec/expressions/#distance) operator in the MapLibre Style Spec. +### mgl_globalState: + +**Selector:** `mgl_globalState:` + +**Format string syntax:** `mgl_globalState('chargerType')` + +Returns the value of the given property in the map's global state, or `nil` if +the property is not present. The global state can be set with +``MLNStyle/setGlobalStateValue:forProperty:`` and defaults can be defined with +the style's root `state` property. + +This function corresponds to the +[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) +operator in the MapLibre Style Spec. + ### mgl_coalesce: **Selector:** `mgl_coalesce:` From fa43402ea66048596f04dc60d381c0545ecd8778 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 14:53:12 +0900 Subject: [PATCH 07/32] feat(android): add global-state API to MapSnapshotter --- CHANGELOG.md | 2 +- .../src/cpp/snapshotter/map_snapshotter.cpp | 18 ++++++++ .../src/cpp/snapshotter/map_snapshotter.hpp | 4 ++ .../android/snapshotter/MapSnapshotter.kt | 40 +++++++++++++++++ .../android/snapshotter/MapSnapshotterTest.kt | 43 +++++++++++++++++++ 5 files changed, 106 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d5215ee82f..8a2fd63d75ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - *...Add new stuff here...* - [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property in filters, layout properties, and paint properties, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState` ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). -- [android] Add `Style#setGlobalStateProperty` / `Style#getGlobalState` and the `Expression.globalState` builder for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). +- [android] Add `Style#setGlobalStateProperty` / `Style#getGlobalState` (also available on `MapSnapshotter`) and the `Expression.globalState` builder for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [ios] Add `MLNStyle.globalState` / `-[MLNStyle setGlobalStateValue:forProperty:]` and `NSExpression` support (`mgl_globalState:`) for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [core] Locally rasterized CJK glyphs now render at 2x texture resolution, preserving fine strokes. Mirrors [maplibre-gl-js#3006](https://github.com/maplibre/maplibre-gl-js/pull/3006). - [core] Added new map observer events: onPreCompileShader, onPostCompileShader, onShaderCompileFailed, onGlyphsLoaded, onGlyphsError, onGlyphsRequested, onTileAction, onSpriteLoaded, onSpriteError, onSpriteRequested ([#2694](https://github.com/maplibre/maplibre-native/pull/2694)). diff --git a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp index 435e2a5d6904..0735c4d35664 100644 --- a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp @@ -319,6 +319,21 @@ jni::Local> MapSnapshotter::getSource(JNIEnv& env, const jni return jni::NewLocal(env, Source::peerForCoreSource(env, *coreSource)); } +void MapSnapshotter::setGlobalStateProperty(JNIEnv& env, + const jni::String& name, + const jni::Object& value) { + if (!name) { + return; + } + + snapshotter->getStyle().setGlobalStateProperty(jni::Make(env, name), + value ? gson::JsonElement::convert(env, value) : mln::Value()); +} + +jni::Local> MapSnapshotter::getGlobalState(JNIEnv& env) { + return gson::JsonObject::New(env, snapshotter->getStyle().getGlobalState()); +} + // Static methods // void MapSnapshotter::registerNative(jni::JNIEnv& env) { @@ -358,6 +373,9 @@ void MapSnapshotter::registerNative(jni::JNIEnv& env) { METHOD(&MapSnapshotter::addImages, "nativeAddImages"), METHOD(&MapSnapshotter::getLayer, "nativeGetLayer"), METHOD(&MapSnapshotter::getSource, "nativeGetSource"), + METHOD(&MapSnapshotter::setGlobalStateProperty, + "nativeSetGlobalStateProperty"), + METHOD(&MapSnapshotter::getGlobalState, "nativeGetGlobalState"), METHOD(&MapSnapshotter::setStyleJson, "setStyleJson"), METHOD(&MapSnapshotter::setSize, "setSize"), METHOD(&MapSnapshotter::setCameraPosition, "setCameraPosition"), diff --git a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp index ccbfef0886c9..b7e78737360b 100644 --- a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp @@ -9,6 +9,7 @@ #include "../file_source.hpp" #include "../geometry/lat_lng_bounds.hpp" +#include "../gson/json_object.hpp" #include "../map/camera_position.hpp" #include "../map/image.hpp" #include "../style/layers/layer.hpp" @@ -67,6 +68,9 @@ class MapSnapshotter final : public mln::MapSnapshotterObserver { jni::Local> getLayer(JNIEnv&, const jni::String&); jni::Local> getSource(JNIEnv&, const jni::String&); + void setGlobalStateProperty(JNIEnv&, const jni::String& name, const jni::Object& value); + jni::Local> getGlobalState(JNIEnv&); + // MapSnapshotterObserver overrides void onDidFailLoadingStyle(const std::string&) override; void onDidFinishLoadingStyle() override; diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt index 41b3805db0d3..5a1b8f4bbd60 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt @@ -14,6 +14,8 @@ import androidx.annotation.Keep import androidx.annotation.UiThread import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toBitmap +import com.google.gson.JsonElement +import com.google.gson.JsonObject import org.maplibre.android.R import org.maplibre.android.attribution.AttributionLayout import org.maplibre.android.attribution.AttributionMeasure @@ -740,6 +742,38 @@ open class MapSnapshotter(context: Context, options: Options) { return if (fullyLoaded) nativeGetSource(sourceId) else null } + /** + * Set a global state property on the style that is used by the snapshotter, used by the + * [global-state](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression. + * + * Setting a null value (or [com.google.gson.JsonNull]) resets the property to the default + * defined in the style's root `state` property, or to null if there is none. + * + * Only takes effect once the style is fully loaded; when the style is set by URL, call this + * from [Observer.onDidFinishLoadingStyle]. + * + * @param name the name of the state property + * @param value the new value of the state property + */ + fun setGlobalStateProperty(name: String, value: JsonElement?) { + checkThread() + if (fullyLoaded) { + nativeSetGlobalStateProperty(name, value) + } + } + + /** + * Get a snapshot of the current global state of the style that is used by the snapshotter, + * used by the + * [global-state](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression. + * + * @return the current global state, or an empty object if the style is not fully loaded yet + */ + fun getGlobalState(): JsonObject { + checkThread() + return if (fullyLoaded) nativeGetGlobalState() else JsonObject() + } + /** * Called by JNI peer when snapshot style image is missing. */ @@ -806,6 +840,12 @@ open class MapSnapshotter(context: Context, options: Options) { @Keep private external fun nativeGetSource(sourceId: String): Source + @Keep + private external fun nativeSetGlobalStateProperty(name: String, value: JsonElement?) + + @Keep + private external fun nativeGetGlobalState(): JsonObject + @Keep @Throws(Throwable::class) protected open external fun finalize() diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt index 31d1287d8bb4..2d9087852d95 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt @@ -2,6 +2,7 @@ package org.maplibre.android.snapshotter import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import androidx.test.rule.ActivityTestRule +import com.google.gson.JsonPrimitive import org.maplibre.android.camera.CameraPosition import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.Style @@ -65,4 +66,46 @@ class MapSnapshotterTest { throw TimeoutException() } } + + @Test + fun mapSnapshotterGlobalState() { + var mapSnapshotter: MapSnapshotter? + rule.activity.runOnUiThread { + val styleJson = """ + { + "version": 8, + "state": {"showLabels": {"default": true}}, + "sources": {}, + "layers": [] + } + """.trimIndent() + val options = MapSnapshotter.Options(64, 64) + .withPixelRatio(1.0f) + .withStyleBuilder(Style.Builder().fromJson(styleJson)) + mapSnapshotter = MapSnapshotter(rule.activity, options) + mapSnapshotter!!.start( + { + // The style is fully loaded once the snapshot is ready; + // the default from the style's root "state" property applies. + Assert.assertEquals( + JsonPrimitive(true), + mapSnapshotter!!.getGlobalState().get("showLabels") + ) + + mapSnapshotter!!.setGlobalStateProperty("showLabels", JsonPrimitive(false)) + Assert.assertEquals( + JsonPrimitive(false), + mapSnapshotter!!.getGlobalState().get("showLabels") + ) + countDownLatch.countDown() + }, + { + Assert.fail(it) + } + ) + } + if (!countDownLatch.await(30, TimeUnit.SECONDS)) { + throw TimeoutException() + } + } } From e0c281224f2208dad34e414cab05f9240ad9db85 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 15:34:50 +0900 Subject: [PATCH 08/32] feat(node): add global-state runtime API --- CHANGELOG.md | 1 + platform/node/index.d.ts | 18 +++++- platform/node/src/node_map.cpp | 62 +++++++++++++++++++ platform/node/src/node_map.hpp | 2 + platform/node/test/js/global_state.test.js | 70 ++++++++++++++++++++++ platform/node/test/js/map.test.js | 2 + 6 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 platform/node/test/js/global_state.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a2fd63d75ac..f979e06b72cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property in filters, layout properties, and paint properties, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState` ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [android] Add `Style#setGlobalStateProperty` / `Style#getGlobalState` (also available on `MapSnapshotter`) and the `Expression.globalState` builder for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [ios] Add `MLNStyle.globalState` / `-[MLNStyle setGlobalStateValue:forProperty:]` and `NSExpression` support (`mgl_globalState:`) for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). +- [node] Add `map.setGlobalStateProperty` / `map.getGlobalState` for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [core] Locally rasterized CJK glyphs now render at 2x texture resolution, preserving fine strokes. Mirrors [maplibre-gl-js#3006](https://github.com/maplibre/maplibre-gl-js/pull/3006). - [core] Added new map observer events: onPreCompileShader, onPostCompileShader, onShaderCompileFailed, onGlyphsLoaded, onGlyphsError, onGlyphsRequested, onTileAction, onSpriteLoaded, onSpriteError, onSpriteRequested ([#2694](https://github.com/maplibre/maplibre-native/pull/2694)). - [core] Add WebP image decoding support to default platform (Linux, Windows) diff --git a/platform/node/index.d.ts b/platform/node/index.d.ts index 33573695811c..348eee862db8 100644 --- a/platform/node/index.d.ts +++ b/platform/node/index.d.ts @@ -169,15 +169,29 @@ declare module '@maplibre/maplibre-gl-native' { setLayerZoomRange: (layerId: string, minZoom: number, maxZoom: number) => void; /** - * Set the value for a layer's property + * Set the value for a layer's property. + * The value may also be an expression, such as + * `["case", ["global-state", "show"], "visible", "none"]` for `visibility`. */ - setLayoutProperty: (layerId: string, name: string, value: string) => void; + setLayoutProperty: (layerId: string, name: string, value: string | number | boolean | unknown[]) => void; /** * Set filter for specified style layer */ setFilter: (layerId: string, filter: [] | null | undefined) => void; + /** + * Set a property of the map's global state, used by the `global-state` expression. + * A `null` value resets the property to the default defined in the style's + * root `state` property, or to null if there is none. + */ + setGlobalStateProperty: (name: string, value: unknown) => void; + + /** + * Get a snapshot of the map's current global state, used by the `global-state` expression. + */ + getGlobalState: () => Record; + /** * Set size of the tile */ diff --git a/platform/node/src/node_map.cpp b/platform/node/src/node_map.cpp index 95d619b843b7..b8eaf2f3967b 100644 --- a/platform/node/src/node_map.cpp +++ b/platform/node/src/node_map.cpp @@ -117,6 +117,8 @@ ParseError)JS") Nan::SetPrototypeMethod(tpl, "setFeatureState", SetFeatureState); Nan::SetPrototypeMethod(tpl, "getFeatureState", GetFeatureState); Nan::SetPrototypeMethod(tpl, "removeFeatureState", RemoveFeatureState); + Nan::SetPrototypeMethod(tpl, "setGlobalStateProperty", SetGlobalStateProperty); + Nan::SetPrototypeMethod(tpl, "getGlobalState", GetGlobalState); Nan::SetPrototypeMethod(tpl, "dumpDebugLogs", DumpDebugLogs); Nan::SetPrototypeMethod(tpl, "queryRenderedFeatures", QueryRenderedFeatures); @@ -1391,6 +1393,66 @@ void NodeMap::RemoveFeatureState(const Nan::FunctionCallbackInfo& inf info.GetReturnValue().SetUndefined(); } +namespace { + +// Deep conversion of a JS value to an mln::Value (unlike conversion::toValue, +// which only handles scalar values). +mln::Value toGlobalStateValue(const mln::style::conversion::Convertible& value) { + using namespace mln::style::conversion; + + if (isArray(value)) { + mapbox::base::ValueArray array; + const std::size_t length = arrayLength(value); + array.reserve(length); + for (std::size_t i = 0; i < length; ++i) { + array.push_back(toGlobalStateValue(arrayMember(value, i))); + } + return array; + } + if (isObject(value)) { + mapbox::base::ValueObject object; + eachMember(value, + [&](const std::string& key, const Convertible& member) -> std::optional { + object.emplace(key, toGlobalStateValue(member)); + return std::nullopt; + }); + return object; + } + return toValue(value).value_or(mln::Value()); +} + +} // namespace + +void NodeMap::SetGlobalStateProperty(const Nan::FunctionCallbackInfo& info) { + using namespace mln::style::conversion; + + auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); + if (!nodeMap->map) return Nan::ThrowError(releasedMessage()); + + if (info.Length() < 2) { + return Nan::ThrowTypeError("Two arguments required"); + } + if (!info[0]->IsString()) { + return Nan::ThrowTypeError("First argument must be a string"); + } + + try { + nodeMap->map->getStyle().setGlobalStateProperty(*Nan::Utf8String(info[0]), + toGlobalStateValue(Convertible(info[1]))); + } catch (const std::exception& ex) { + return Nan::ThrowError(ex.what()); + } + + info.GetReturnValue().SetUndefined(); +} + +void NodeMap::GetGlobalState(const Nan::FunctionCallbackInfo& info) { + auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); + if (!nodeMap->map) return Nan::ThrowError(releasedMessage()); + + info.GetReturnValue().Set(toJS(nodeMap->map->getStyle().getGlobalState())); +} + void NodeMap::DumpDebugLogs(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); if (!nodeMap->map) return Nan::ThrowError(releasedMessage()); diff --git a/platform/node/src/node_map.hpp b/platform/node/src/node_map.hpp index 1429bc33f484..423f6a8a098b 100644 --- a/platform/node/src/node_map.hpp +++ b/platform/node/src/node_map.hpp @@ -70,6 +70,8 @@ class NodeMap : public Nan::ObjectWrap { static void SetFeatureState(const Nan::FunctionCallbackInfo&); static void GetFeatureState(const Nan::FunctionCallbackInfo&); static void RemoveFeatureState(const Nan::FunctionCallbackInfo&); + static void SetGlobalStateProperty(const Nan::FunctionCallbackInfo&); + static void GetGlobalState(const Nan::FunctionCallbackInfo&); static v8::Local ParseError(const char* msg); diff --git a/platform/node/test/js/global_state.test.js b/platform/node/test/js/global_state.test.js new file mode 100644 index 000000000000..3d2c90fdeabb --- /dev/null +++ b/platform/node/test/js/global_state.test.js @@ -0,0 +1,70 @@ +'use strict'; + +var test = require('tape'); +var mbgl = require('../../index'); + +var style = { + version: 8, + state: { + showLabels: {default: true}, + categories: {default: ['restaurant', 'hotel']} + }, + sources: {}, + layers: [] +}; + +test('Map#globalState', function(t) { + t.test('applies defaults from the style root state property', function(t) { + var map = new mbgl.Map({request: function() {}}); + map.load(style); + t.deepEqual(map.getGlobalState(), { + showLabels: true, + categories: ['restaurant', 'hotel'] + }); + map.release(); + t.end(); + }); + + t.test('sets and gets global state properties', function(t) { + var map = new mbgl.Map({request: function() {}}); + map.load(style); + + map.setGlobalStateProperty('showLabels', false); + t.equal(map.getGlobalState().showLabels, false); + + map.setGlobalStateProperty('custom', {nested: [1, 'two', true]}); + t.deepEqual(map.getGlobalState().custom, {nested: [1, 'two', true]}); + + map.release(); + t.end(); + }); + + t.test('resets a property to the style default on null', function(t) { + var map = new mbgl.Map({request: function() {}}); + map.load(style); + + map.setGlobalStateProperty('showLabels', false); + map.setGlobalStateProperty('showLabels', null); + t.equal(map.getGlobalState().showLabels, true); + + map.release(); + t.end(); + }); + + t.test('requires a string property name', function(t) { + var map = new mbgl.Map({request: function() {}}); + map.load(style); + + t.throws(function() { + map.setGlobalStateProperty(1, true); + }, /First argument must be a string/); + t.throws(function() { + map.setGlobalStateProperty('name'); + }, /Two arguments required/); + + map.release(); + t.end(); + }); + + t.end(); +}); diff --git a/platform/node/test/js/map.test.js b/platform/node/test/js/map.test.js index 3d1f291246ae..4cf455f1813e 100644 --- a/platform/node/test/js/map.test.js +++ b/platform/node/test/js/map.test.js @@ -128,6 +128,8 @@ test('Map', function(t) { 'setFeatureState', 'getFeatureState', 'removeFeatureState', + 'setGlobalStateProperty', + 'getGlobalState', 'dumpDebugLogs', 'queryRenderedFeatures' ]); From d81819440e668a51117635e06ee5be3b8813c814 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 15:42:09 +0900 Subject: [PATCH 09/32] fix(core): evaluate runtime-set visibility expressions against current global state --- src/mln/style/style_impl.cpp | 1 + test/style/style.test.cpp | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/mln/style/style_impl.cpp b/src/mln/style/style_impl.cpp index 3d2143181cab..521b0d0644c3 100644 --- a/src/mln/style/style_impl.cpp +++ b/src/mln/style/style_impl.cpp @@ -501,6 +501,7 @@ void Style::Impl::onSpriteRequested(const std::optional& sprite) } void Style::Impl::onLayerChanged(Layer& layer) { + layer.reevaluateVisibility(*globalState); layers.update(layer); observer->onUpdate(); } diff --git a/test/style/style.test.cpp b/test/style/style.test.cpp index 6f8944863ed6..2d63fbc84b51 100644 --- a/test/style/style.test.cpp +++ b/test/style/style.test.cpp @@ -183,6 +183,45 @@ TEST(Style, GlobalStateVisibility) { EXPECT_EQ(VisibilityType::None, layer->getVisibility()); } +TEST(Style, VisibilityExpressionSetAtRuntime) { + util::RunLoop loop; + + auto fileSource = std::make_shared(); + Style::Impl style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; + + style.loadJSON(R"STYLE({ + "version": 8, + "state": {"showBackground": {"default": false}}, + "sources": {}, + "layers": [{"id": "background", "type": "background"}] + })STYLE"); + + Layer* layer = style.getLayer("background"); + ASSERT_TRUE(layer); + EXPECT_EQ(VisibilityType::Visible, layer->getVisibility()); + + // A visibility expression set after the style has loaded must be + // evaluated against the current global state right away. + rapidjson::GenericDocument, rapidjson::CrtAllocator> document; + document.Parse<0>(R"(["case", ["to-boolean", ["global-state", "showBackground"]], "visible", "none"])"); + ASSERT_FALSE(document.HasParseError()); + const JSValue* json = &document; + auto error = layer->setProperty("visibility", conversion::Convertible(json)); + EXPECT_FALSE(error); + EXPECT_EQ(VisibilityType::None, layer->getVisibility()); + + // Runtime global state (not only style defaults) applies as well. + style.setGlobalStateProperty("showBackground", true); + ASSERT_TRUE(layer); + EXPECT_EQ(VisibilityType::Visible, layer->getVisibility()); + + document.Parse<0>(R"(["case", ["to-boolean", ["global-state", "showBackground"]], "none", "visible"])"); + ASSERT_FALSE(document.HasParseError()); + error = layer->setProperty("visibility", conversion::Convertible(json)); + EXPECT_FALSE(error); + EXPECT_EQ(VisibilityType::None, layer->getVisibility()); +} + TEST(Style, VisibilityExpressionRejectsOtherDependencies) { util::RunLoop loop; From 8b4fb76a862692f4f584b4e597b82cdab010d0b9 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 15:53:38 +0900 Subject: [PATCH 10/32] fix(core): keep global-state dependency through format section overrides --- .../renderer/layers/render_symbol_layer.cpp | 2 +- src/mln/style/layers/symbol_layer_impl.hpp | 30 ++++++++++--- test/style/style_layer.test.cpp | 44 ++++++++++++++++++- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/mln/renderer/layers/render_symbol_layer.cpp b/src/mln/renderer/layers/render_symbol_layer.cpp index cbfea31bedb8..1b8ebf6dc926 100644 --- a/src/mln/renderer/layers/render_symbol_layer.cpp +++ b/src/mln/renderer/layers/render_symbol_layer.cpp @@ -163,7 +163,7 @@ void RenderSymbolLayer::evaluate(const PropertyEvaluationParameters& parameters) const auto& layout = impl_cast(baseImpl).layout; if (hasFormatSectionOverrides) { - SymbolLayerPaintPropertyOverrides::setOverrides(layout, evaluated); + SymbolLayerPaintPropertyOverrides::setOverrides(layout, evaluated, unevaluated, parameters.globalState); } auto hasIconOpacity = evaluated.get().constantOr(Color::black()).a > 0 || diff --git a/src/mln/style/layers/symbol_layer_impl.hpp b/src/mln/style/layers/symbol_layer_impl.hpp index 36128f11f861..e291a5c3daa8 100644 --- a/src/mln/style/layers/symbol_layer_impl.hpp +++ b/src/mln/style/layers/symbol_layer_impl.hpp @@ -17,20 +17,38 @@ struct FormatSectionOverrides; template struct FormatSectionOverrides> { - template - static void setOverride(const T& overrides, U& overridable) { + template + static void setOverride(const T& overrides, + U& overridable, + const V& unevaluated, + const std::shared_ptr& globalState) { if (hasOverride(overrides.template get())) { + auto& value = overridable.template get(); + // Wrapping a folded constant would drop the GlobalState dependency + // from the wrapper and exempt the bucket from relayout tracking. + if (value.isConstant()) { + const auto& unevaluatedValue = unevaluated.template get().getValue(); + if (unevaluatedValue.isExpression() && + (unevaluatedValue.getDependencies() & expression::Dependency::GlobalState)) { + auto retained = unevaluatedValue.asExpression(); + retained.captureGlobalState(globalState); + value = PossiblyEvaluatedPropertyValue(std::move(retained)); + } + } auto override = std::make_unique>( - Property::expressionType(), std::move(overridable.template get()), Property::name()); + Property::expressionType(), std::move(value), Property::name()); PropertyExpression expr(std::move(override)); overridable.template get() = PossiblyEvaluatedPropertyValue( std::move(expr)); } } - template - static void setOverrides(const T& overrides, U& overridable) { - util::ignore({(setOverride(overrides, overridable), 0)...}); + template + static void setOverrides(const T& overrides, + U& overridable, + const V& unevaluated, + const std::shared_ptr& globalState) { + util::ignore({(setOverride(overrides, overridable, unevaluated, globalState), 0)...}); } template diff --git a/test/style/style_layer.test.cpp b/test/style/style_layer.test.cpp index a7e59b9348b5..09606dd5a358 100644 --- a/test/style/style_layer.test.cpp +++ b/test/style/style_layer.test.cpp @@ -2,10 +2,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -442,7 +444,7 @@ TEST(Layer, SymbolLayerOverrides) { layout.get() = PossiblyEvaluatedPropertyValue(std::move(formatted)); paint.get() = PossiblyEvaluatedPropertyValue{Color::red()}; EXPECT_TRUE(paint.get().isConstant()); - MockOverrides::setOverrides(layout, paint); + MockOverrides::setOverrides(layout, paint, MockPaintProperties::Unevaluated(), nullptr); EXPECT_FALSE(paint.get().isConstant()); MockPaintProperties::PossiblyEvaluated updated; @@ -450,4 +452,44 @@ TEST(Layer, SymbolLayerOverrides) { MockOverrides::updateOverrides(paint, updated); EXPECT_FALSE(updated.get().isConstant()); } + + // A text-color folded to a constant from a global-state-only expression + // keeps the dependency, its keys, and the state through the override + // wrapper. + { + MockLayoutProperties::PossiblyEvaluated layout; + MockPaintProperties::PossiblyEvaluated paint; + + auto formatted = Formatted(""); + formatted.sections.emplace_back("section text"s, std::nullopt, std::nullopt, Color::green()); + layout.get() = PossiblyEvaluatedPropertyValue(std::move(formatted)); + paint.get() = PossiblyEvaluatedPropertyValue{Color::red()}; + + ParsingContext ctx; + std::vector> args; + args.push_back(literal("labelColor")); + ParseResult stateExpression = createCompoundExpression("global-state", std::move(args), ctx); + ASSERT_TRUE(stateExpression); + MockPaintProperties::Unevaluated unevaluated; + unevaluated.get() = Transitioning>( + PropertyValue(PropertyExpression(toColor(std::move(*stateExpression))))); + + auto state = std::make_shared(GlobalStateMap{{"labelColor", std::string("red")}}); + MockOverrides::setOverrides(layout, paint, unevaluated, state); + + const auto& wrapped = paint.get(); + EXPECT_FALSE(wrapped.isConstant()); + EXPECT_TRUE(wrapped.getDependencies() & Dependency::GlobalState); + ASSERT_TRUE(wrapped.getGlobalStateRefs()); + EXPECT_EQ((std::set{"labelColor"}), *wrapped.getGlobalStateRefs()); + + // A section without an override evaluates from the captured state. + const StubGeometryTileFeature feature(PropertyMap{}); + const Color evaluated = wrapped.match( + [&](const PropertyExpression& fn) { + return fn.evaluate(EvaluationContext(0.0f, &feature), Color::black()); + }, + [](const Color& constant) { return constant; }); + EXPECT_EQ(*Color::parse("red"), evaluated); + } } From 928365882dd3669cbd5bb0237acc9d980f63e610 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 16:12:53 +0900 Subject: [PATCH 11/32] fix(android): query the style's load state instead of tracking it in MapSnapshotter --- include/mln/style/style.hpp | 3 ++ .../src/cpp/snapshotter/map_snapshotter.cpp | 5 ++ .../src/cpp/snapshotter/map_snapshotter.hpp | 2 + .../android/snapshotter/MapSnapshotter.kt | 15 ++++-- .../android/snapshotter/MapSnapshotterTest.kt | 47 +++++++++++++++++++ src/mln/style/style.cpp | 4 ++ 6 files changed, 73 insertions(+), 3 deletions(-) diff --git a/include/mln/style/style.hpp b/include/mln/style/style.hpp index 4beafe13e902..76206b2a24ae 100644 --- a/include/mln/style/style.hpp +++ b/include/mln/style/style.hpp @@ -33,6 +33,9 @@ class Style { std::string getJSON() const; std::string getURL() const; + /// False while a style passed to loadJSON/loadURL is still being fetched or parsed. + bool isLoaded() const; + // Defaults std::string getName() const; CameraOptions getDefaultCamera() const; diff --git a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp index 0735c4d35664..1c2666381ec3 100644 --- a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp @@ -319,6 +319,10 @@ jni::Local> MapSnapshotter::getSource(JNIEnv& env, const jni return jni::NewLocal(env, Source::peerForCoreSource(env, *coreSource)); } +jni::jboolean MapSnapshotter::isFullyLoaded(JNIEnv&) { + return snapshotter->getStyle().isLoaded(); +} + void MapSnapshotter::setGlobalStateProperty(JNIEnv& env, const jni::String& name, const jni::Object& value) { @@ -373,6 +377,7 @@ void MapSnapshotter::registerNative(jni::JNIEnv& env) { METHOD(&MapSnapshotter::addImages, "nativeAddImages"), METHOD(&MapSnapshotter::getLayer, "nativeGetLayer"), METHOD(&MapSnapshotter::getSource, "nativeGetSource"), + METHOD(&MapSnapshotter::isFullyLoaded, "nativeIsFullyLoaded"), METHOD(&MapSnapshotter::setGlobalStateProperty, "nativeSetGlobalStateProperty"), METHOD(&MapSnapshotter::getGlobalState, "nativeGetGlobalState"), diff --git a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp index b7e78737360b..596462cbeda6 100644 --- a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp @@ -68,6 +68,8 @@ class MapSnapshotter final : public mln::MapSnapshotterObserver { jni::Local> getLayer(JNIEnv&, const jni::String&); jni::Local> getSource(JNIEnv&, const jni::String&); + jni::jboolean isFullyLoaded(JNIEnv&); + void setGlobalStateProperty(JNIEnv&, const jni::String& name, const jni::Object& value); jni::Local> getGlobalState(JNIEnv&); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt index 5a1b8f4bbd60..432efc269a1d 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt @@ -44,7 +44,13 @@ open class MapSnapshotter(context: Context, options: Options) { @Keep private val nativePtr: Long = 0 private val context: Context - private var fullyLoaded = false + // Not cached: core resets the load state whenever a new style is set. + private val fullyLoaded: Boolean + get() = nativeIsFullyLoaded() + + // Sources, layers, and images from Options.builder are applied to the + // first loaded style only. + private var builderApplied = false private val options: Options private var callback: SnapshotReadyCallback? = null private var errorHandler: ErrorHandler? = null @@ -438,6 +444,9 @@ open class MapSnapshotter(context: Context, options: Options) { @Keep external fun setStyleJson(styleJson: String?) + @Keep + private external fun nativeIsFullyLoaded(): Boolean + /** * Adds the layer to the map. The layer must be newly created and not added to the snapshotter before * @@ -692,8 +701,8 @@ open class MapSnapshotter(context: Context, options: Options) { */ @Keep protected fun onDidFinishLoadingStyle() { - if (!fullyLoaded) { - fullyLoaded = true + if (!builderApplied) { + builderApplied = true val builder = options.builder if (builder != null) { for (source in builder.sources) { diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt index 2d9087852d95..be0485f5b9b3 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt @@ -67,6 +67,53 @@ class MapSnapshotterTest { } } + @Test + fun mapSnapshotterStyleReloadResetsGlobalState() { + var mapSnapshotter: MapSnapshotter? + rule.activity.runOnUiThread { + val styleJson = """ + { + "version": 8, + "state": {"showLabels": {"default": true}}, + "sources": {}, + "layers": [] + } + """.trimIndent() + val options = MapSnapshotter.Options(64, 64) + .withPixelRatio(1.0f) + .withStyleBuilder(Style.Builder().fromJson(styleJson)) + mapSnapshotter = MapSnapshotter(rule.activity, options) + mapSnapshotter!!.start( + { + Assert.assertEquals( + JsonPrimitive(true), + mapSnapshotter!!.getGlobalState().get("showLabels") + ) + + mapSnapshotter!!.setObserver(object : MapSnapshotter.Observer { + override fun onDidFinishLoadingStyle() { + // The new style defines no state. + Assert.assertEquals(0, mapSnapshotter!!.getGlobalState().entrySet().size) + countDownLatch.countDown() + } + + override fun onStyleImageMissing(imageName: String) {} + }) + mapSnapshotter!!.setStyleUrl("asset://fill_color_style.json") + // While the new style is loading, the previous style's + // state must not be visible. + Assert.assertEquals(0, mapSnapshotter!!.getGlobalState().entrySet().size) + }, + { + Assert.fail(it) + } + ) + } + if (!countDownLatch.await(30, TimeUnit.SECONDS)) { + throw TimeoutException() + } + } + @Test fun mapSnapshotterGlobalState() { var mapSnapshotter: MapSnapshotter? diff --git a/src/mln/style/style.cpp b/src/mln/style/style.cpp index 8cb7544d2741..8bee110ff931 100644 --- a/src/mln/style/style.cpp +++ b/src/mln/style/style.cpp @@ -39,6 +39,10 @@ std::string Style::getURL() const { return impl->getURL(); } +bool Style::isLoaded() const { + return impl->isLoaded(); +} + std::string Style::getName() const { return impl->getName(); } From a7a0546cc63d0274ce2a84622416e57227dc7585 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 16:24:31 +0900 Subject: [PATCH 12/32] feat(android): allow setting layer visibility as an expression --- .../location/LocationPropertyFactory.java | 15 +++++++++++ .../maplibre/android/style/layers/Layer.java | 3 +++ .../android/style/layers/PropertyFactory.java | 15 +++++++++++ .../style/layers/property_factory.java.ejs | 15 +++++++++++ .../testapp/style/RuntimeStyleTests.java | 27 +++++++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationPropertyFactory.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationPropertyFactory.java index 653f2d224f1f..7a33e0532f7c 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationPropertyFactory.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationPropertyFactory.java @@ -4,6 +4,8 @@ import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + import org.maplibre.android.style.expressions.Expression; import org.maplibre.android.style.types.Formatted; import static org.maplibre.android.utils.ColorUtils.colorToRgbaString; @@ -29,6 +31,19 @@ public static PropertyValue visibility(@Property.VISIBILITY String value return new LayoutPropertyValue<>("visibility", value); } + /** + * Set the property visibility as an expression. Per the style specification, + * visibility expressions may only use the + * global-state expression + * and must evaluate to {@code "visible"} or {@code "none"}. + * + * @param expression the visibility expression + * @return property wrapper around visibility + */ + public static PropertyValue visibility(@NonNull Expression expression) { + return new LayoutPropertyValue<>("visibility", expression); + } + /** * The amount of the perspective compensation, between 0 and 1. A value of 1 produces a location indicator of constant width across the screen. A value of 0 makes it scale naturally according to the viewing projection. * diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/Layer.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/Layer.java index 6d6bccc4ed4b..c0ba3b4240a0 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/Layer.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/Layer.java @@ -71,6 +71,9 @@ public String getId() { return nativeGetId(); } + /** + * When the visibility was set as an expression, this returns the current evaluated value. + */ @NonNull public PropertyValue getVisibility() { checkThread(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/PropertyFactory.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/PropertyFactory.java index 1c1525f75686..f356e5a60a3e 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/PropertyFactory.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/PropertyFactory.java @@ -4,6 +4,8 @@ import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + import org.maplibre.android.style.expressions.Expression; import org.maplibre.android.style.types.Formatted; import static org.maplibre.android.utils.ColorUtils.colorToRgbaString; @@ -25,6 +27,19 @@ public static PropertyValue visibility(@Property.VISIBILITY String value return new LayoutPropertyValue<>("visibility", value); } + /** + * Set the property visibility as an expression. Per the style specification, + * visibility expressions may only use the + * global-state expression + * and must evaluate to {@code "visible"} or {@code "none"}. + * + * @param expression the visibility expression + * @return property wrapper around visibility + */ + public static PropertyValue visibility(@NonNull Expression expression) { + return new LayoutPropertyValue<>("visibility", expression); + } + /** * Whether or not the fill should be antialiased. * diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/property_factory.java.ejs b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/property_factory.java.ejs index 6730ff5834e5..b1ba7ab98980 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/property_factory.java.ejs +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/property_factory.java.ejs @@ -13,6 +13,8 @@ package org.maplibre.android.style.layers; import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + import org.maplibre.android.style.expressions.Expression; import org.maplibre.android.style.types.Formatted; import static org.maplibre.android.utils.ColorUtils.colorToRgbaString; @@ -44,6 +46,19 @@ public class PropertyFactory { return new LayoutPropertyValue<>("visibility", value); } + /** + * Set the property visibility as an expression. Per the style specification, + * visibility expressions may only use the + * global-state expression + * and must evaluate to {@code "visible"} or {@code "none"}. + * + * @param expression the visibility expression + * @return property wrapper around visibility + */ + public static PropertyValue visibility(@NonNull Expression expression) { + return new LayoutPropertyValue<>("visibility", expression); + } + <% for (const property of paintProperties) { -%> <% if (property.type == 'color') { -%> /** diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java index 718482d7ddc5..7041d4610027 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java @@ -11,6 +11,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; +import org.maplibre.android.style.expressions.Expression; import org.maplibre.android.style.layers.CannotAddLayerException; import org.maplibre.android.style.layers.BackgroundLayer; import org.maplibre.android.style.layers.CircleLayer; @@ -79,6 +80,32 @@ public void testGetAddRemoveLayer() { onView(withId(R.id.mapView)).perform(new AddRemoveLayerAction()); } + @Test + public void testVisibilityExpression() { + validateTestSetup(); + onView(withId(R.id.mapView)).perform(new BaseViewAction() { + @Override + public void perform(UiController uiController, View view) { + Style style = maplibreMap.getStyle(); + assertNotNull(style); + Layer layer = style.getLayers().get(0); + + layer.setProperties(PropertyFactory.visibility( + Expression.switchCase( + Expression.toBool(Expression.globalState("showLayer")), + Expression.literal(Property.VISIBLE), + Expression.literal(Property.NONE)))); + assertEquals(Property.NONE, layer.getVisibility().getValue()); + + style.setGlobalStateProperty("showLayer", new JsonPrimitive(true)); + assertEquals(Property.VISIBLE, layer.getVisibility().getValue()); + + style.setGlobalStateProperty("showLayer", new JsonPrimitive(false)); + assertEquals(Property.NONE, layer.getVisibility().getValue()); + } + }); + } + @Test public void testGlobalState() { validateTestSetup(); From da94ac5197fafaf54b469ba26b1505c594068366 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 16:43:21 +0900 Subject: [PATCH 13/32] feat(ios): allow setting layer visibility as an expression --- include/mln/style/layer.hpp | 4 +++ platform/darwin/src/MLNStyleLayer.h | 13 +++++++++ platform/darwin/src/MLNStyleLayer.mm | 29 +++++++++++++++++++ platform/darwin/test/MLNStyleTests.mm | 41 +++++++++++++++++++++++++++ src/mln/style/layer.cpp | 4 +++ 5 files changed, 91 insertions(+) diff --git a/include/mln/style/layer.hpp b/include/mln/style/layer.hpp index d47aaef06ea9..d60e51b56ac2 100644 --- a/include/mln/style/layer.hpp +++ b/include/mln/style/layer.hpp @@ -138,6 +138,10 @@ class Layer { /// value using "global-state"), if any, against the given global state. void reevaluateVisibility(const GlobalStateMap&); + /// The serialized visibility expression, or a null value when the + /// visibility is a constant. + Value getVisibilityExpression() const; + // Zoom range float getMinZoom() const; float getMaxZoom() const; diff --git a/platform/darwin/src/MLNStyleLayer.h b/platform/darwin/src/MLNStyleLayer.h index 48e23cb2618e..822f625d0501 100644 --- a/platform/darwin/src/MLNStyleLayer.h +++ b/platform/darwin/src/MLNStyleLayer.h @@ -51,6 +51,19 @@ MLN_EXPORT */ @property (nonatomic, assign, getter=isVisible) BOOL visible; +/** + The layer's visibility, specified as an expression that evaluates to `visible` + or `none`. Per the style specification, visibility expressions may only use + the `global-state` expression (`mgl_globalState:`); passing any other data + dependency raises an `NSInvalidArgumentException`. + + The expression is re-evaluated whenever the global state changes; the current + evaluated value is available through the `visible` property. Setting `visible` + removes the expression, and setting this property to `nil` resets the layer to + visible. + */ +@property (nonatomic, nullable) NSExpression *visibilityExpression; + /** The maximum zoom level at which the layer gets parsed and appears. This value is a floating-point number. diff --git a/platform/darwin/src/MLNStyleLayer.mm b/platform/darwin/src/MLNStyleLayer.mm index 3bbce2b5e7ad..7ec451d9ab27 100644 --- a/platform/darwin/src/MLNStyleLayer.mm +++ b/platform/darwin/src/MLNStyleLayer.mm @@ -1,5 +1,8 @@ #import "MLNStyleLayer_Private.h" +#import "MLNConversion.h" +#import "MLNStyleValue_Private.h" #import "MLNStyle_Private.h" +#import "NSExpression+MLNPrivateAdditions.h" #include #include @@ -74,6 +77,32 @@ - (BOOL)isVisible { return (v == mln::style::VisibilityType::Visible); } +- (void)setVisibilityExpression:(NSExpression *)expression { + MLNAssertStyleLayerIsValid(); + + if (!expression) { + self.rawLayer->setVisibility(mln::style::VisibilityType::Visible); + return; + } + + const auto error = self.rawLayer->setProperty( + "visibility", mln::style::conversion::makeConvertible(expression.mgl_jsonExpressionObject)); + if (error) { + [NSException raise:NSInvalidArgumentException + format:@"Invalid visibility expression: %@", @(error->message.c_str())]; + } +} + +- (NSExpression *)visibilityExpression { + MLNAssertStyleLayerIsValid(); + + const mln::Value serialized = self.rawLayer->getVisibilityExpression(); + if (serialized.is()) { + return nil; + } + return [NSExpression expressionWithMLNJSONObject:MLNJSONObjectFromMBGLValue(serialized)]; +} + - (void)setMaximumZoomLevel:(float)maximumZoomLevel { MLNAssertStyleLayerIsValid(); diff --git a/platform/darwin/test/MLNStyleTests.mm b/platform/darwin/test/MLNStyleTests.mm index b69327e41c36..b8f779c1467a 100644 --- a/platform/darwin/test/MLNStyleTests.mm +++ b/platform/darwin/test/MLNStyleTests.mm @@ -556,6 +556,47 @@ - (void)testGlobalState { XCTAssertEqualObjects(self.style.globalState[@"showLabels"], [NSNull null]); } +- (void)testVisibilityExpression { + MLNBackgroundStyleLayer *layer = [[MLNBackgroundStyleLayer alloc] initWithIdentifier:@"bg"]; + [self.style addLayer:layer]; + XCTAssertNil(layer.visibilityExpression); + XCTAssertTrue(layer.visible); + + // The expression is evaluated against the current global state right away. + [self.style setGlobalStateValue:@NO forProperty:@"show"]; + NSArray *json = @[ @"case", @[ @"to-boolean", @[ @"global-state", @"show" ] ], @"visible", @"none" ]; + layer.visibilityExpression = [NSExpression expressionWithMLNJSONObject:json]; + XCTAssertFalse(layer.visible); + XCTAssertNotNil(layer.visibilityExpression); + + [self.style setGlobalStateValue:@YES forProperty:@"show"]; + XCTAssertTrue(layer.visible); + + // Round trip of a plain global-state expression; parsing coerces the + // untyped state value to the expected string type. + NSArray *stateJson = @[ @"global-state", @"vis" ]; + layer.visibilityExpression = [NSExpression expressionWithMLNJSONObject:stateJson]; + NSArray *coercedStateJson = @[ @"to-string", stateJson ]; + XCTAssertEqualObjects(layer.visibilityExpression.mgl_jsonExpressionObject, coercedStateJson); + + // Setting a constant visibility removes the expression. + layer.visible = NO; + XCTAssertNil(layer.visibilityExpression); + XCTAssertFalse(layer.visible); + + // nil resets the layer to visible. + layer.visibilityExpression = [NSExpression expressionWithMLNJSONObject:json]; + layer.visibilityExpression = nil; + XCTAssertNil(layer.visibilityExpression); + XCTAssertTrue(layer.visible); + + // Only global-state may be used. + NSArray *zoomJson = @[ @"case", @[ @">", @[ @"zoom" ], @10 ], @"visible", @"none" ]; + XCTAssertThrowsSpecificNamed( + layer.visibilityExpression = [NSExpression expressionWithMLNJSONObject:zoomJson], + NSException, NSInvalidArgumentException); +} + - (void)testTransition { MLNTransition transitionTest = MLNTransitionMake(5, 4); diff --git a/src/mln/style/layer.cpp b/src/mln/style/layer.cpp index e45a85e1c47a..d2ca37a80007 100644 --- a/src/mln/style/layer.cpp +++ b/src/mln/style/layer.cpp @@ -114,6 +114,10 @@ VisibilityType evaluateVisibilityExpression(const expression::Expression& expres } // namespace +Value Layer::getVisibilityExpression() const { + return baseImpl->visibilityExpression ? baseImpl->visibilityExpression->serialize() : Value(); +} + void Layer::reevaluateVisibility(const GlobalStateMap& globalState) { if (!baseImpl->visibilityExpression) return; const VisibilityType value = evaluateVisibilityExpression(*baseImpl->visibilityExpression, &globalState); From 599978655cb52de4239409820008a451e7c60818 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:41:33 +0000 Subject: [PATCH 14/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- include/mln/style/expression/expression.hpp | 12 +-- .../src/cpp/snapshotter/map_snapshotter.cpp | 82 +++++++++---------- platform/darwin/src/MLNStyleLayer.mm | 2 +- platform/darwin/src/MLNStyleValue.mm | 3 +- platform/darwin/test/MLNStyleTests.mm | 12 +-- platform/node/src/node_map.cpp | 11 +-- src/mln/geometry/feature_index.cpp | 7 +- .../layers/render_color_relief_layer.cpp | 4 +- src/mln/renderer/layers/render_line_layer.cpp | 4 +- .../layers/location_indicator_layer_impl.hpp | 2 +- src/mln/style/properties.hpp | 2 - src/mln/tile/custom_geometry_tile.cpp | 7 +- src/mln/tile/geojson_tile.cpp | 7 +- src/mln/tile/geometry_tile.cpp | 7 +- test/style/expression/global_state.test.cpp | 10 +-- 15 files changed, 84 insertions(+), 88 deletions(-) diff --git a/include/mln/style/expression/expression.hpp b/include/mln/style/expression/expression.hpp index 0ead0f6e8fb0..bbbf94978631 100644 --- a/include/mln/style/expression/expression.hpp +++ b/include/mln/style/expression/expression.hpp @@ -222,12 +222,12 @@ enum class Kind : int32_t { enum class Dependency : uint32_t { None = 0, - Feature = 1 << 0, // Data reference - Image = 1 << 1, // Image reference (equivalent to not "runtime constant") - Zoom = 1 << 2, // Zoom level - Location = 1 << 3, // Not used yet, "distance-from-center" not supported - Bind = 1 << 4, // Create variable binding ("let") - Var = 1 << 5, // Use variable binding + Feature = 1 << 0, // Data reference + Image = 1 << 1, // Image reference (equivalent to not "runtime constant") + Zoom = 1 << 2, // Zoom level + Location = 1 << 3, // Not used yet, "distance-from-center" not supported + Bind = 1 << 4, // Create variable binding ("let") + Var = 1 << 5, // Use variable binding Override = 1 << 6, // Property override Elevation = 1 << 7, // Elevation from DEM GlobalState = 1 << 8, // Global state ("global-state") diff --git a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp index 1c2666381ec3..0828c91c2ca2 100644 --- a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp @@ -347,47 +347,47 @@ void MapSnapshotter::registerNative(jni::JNIEnv& env) { #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) // Register the peer - jni::RegisterNativePeer(env, - javaClass, - "nativePtr", - jni::MakePeer&, - const jni::Object&, - jni::jfloat, - jni::jint, - jni::jint, - const jni::String&, - const jni::String&, - const jni::Object&, - const jni::Object&, - jni::jboolean, - jni::jboolean, - const jni::String&, - jni::jfloat, - jni::jfloat, - jni::jfloat, - jni::jfloat>, - "nativeInitialize", - "finalize", - METHOD(&MapSnapshotter::setStyleUrl, "setStyleUrl"), - METHOD(&MapSnapshotter::addLayerAt, "nativeAddLayerAt"), - METHOD(&MapSnapshotter::addLayerBelow, "nativeAddLayerBelow"), - METHOD(&MapSnapshotter::addLayerAbove, "nativeAddLayerAbove"), - METHOD(&MapSnapshotter::addSource, "nativeAddSource"), - METHOD(&MapSnapshotter::addImages, "nativeAddImages"), - METHOD(&MapSnapshotter::getLayer, "nativeGetLayer"), - METHOD(&MapSnapshotter::getSource, "nativeGetSource"), - METHOD(&MapSnapshotter::isFullyLoaded, "nativeIsFullyLoaded"), - METHOD(&MapSnapshotter::setGlobalStateProperty, - "nativeSetGlobalStateProperty"), - METHOD(&MapSnapshotter::getGlobalState, "nativeGetGlobalState"), - METHOD(&MapSnapshotter::setStyleJson, "setStyleJson"), - METHOD(&MapSnapshotter::setSize, "setSize"), - METHOD(&MapSnapshotter::setCameraPosition, "setCameraPosition"), - METHOD(&MapSnapshotter::setRegion, "setRegion"), - METHOD(&MapSnapshotter::setPadding, "setPadding"), - METHOD(&MapSnapshotter::start, "nativeStart"), - METHOD(&MapSnapshotter::cancel, "nativeCancel")); + jni::RegisterNativePeer( + env, + javaClass, + "nativePtr", + jni::MakePeer&, + const jni::Object&, + jni::jfloat, + jni::jint, + jni::jint, + const jni::String&, + const jni::String&, + const jni::Object&, + const jni::Object&, + jni::jboolean, + jni::jboolean, + const jni::String&, + jni::jfloat, + jni::jfloat, + jni::jfloat, + jni::jfloat>, + "nativeInitialize", + "finalize", + METHOD(&MapSnapshotter::setStyleUrl, "setStyleUrl"), + METHOD(&MapSnapshotter::addLayerAt, "nativeAddLayerAt"), + METHOD(&MapSnapshotter::addLayerBelow, "nativeAddLayerBelow"), + METHOD(&MapSnapshotter::addLayerAbove, "nativeAddLayerAbove"), + METHOD(&MapSnapshotter::addSource, "nativeAddSource"), + METHOD(&MapSnapshotter::addImages, "nativeAddImages"), + METHOD(&MapSnapshotter::getLayer, "nativeGetLayer"), + METHOD(&MapSnapshotter::getSource, "nativeGetSource"), + METHOD(&MapSnapshotter::isFullyLoaded, "nativeIsFullyLoaded"), + METHOD(&MapSnapshotter::setGlobalStateProperty, "nativeSetGlobalStateProperty"), + METHOD(&MapSnapshotter::getGlobalState, "nativeGetGlobalState"), + METHOD(&MapSnapshotter::setStyleJson, "setStyleJson"), + METHOD(&MapSnapshotter::setSize, "setSize"), + METHOD(&MapSnapshotter::setCameraPosition, "setCameraPosition"), + METHOD(&MapSnapshotter::setRegion, "setRegion"), + METHOD(&MapSnapshotter::setPadding, "setPadding"), + METHOD(&MapSnapshotter::start, "nativeStart"), + METHOD(&MapSnapshotter::cancel, "nativeCancel")); } } // namespace android diff --git a/platform/darwin/src/MLNStyleLayer.mm b/platform/darwin/src/MLNStyleLayer.mm index 7ec451d9ab27..9fd2a1f76b42 100644 --- a/platform/darwin/src/MLNStyleLayer.mm +++ b/platform/darwin/src/MLNStyleLayer.mm @@ -1,5 +1,5 @@ -#import "MLNStyleLayer_Private.h" #import "MLNConversion.h" +#import "MLNStyleLayer_Private.h" #import "MLNStyleValue_Private.h" #import "MLNStyle_Private.h" #import "NSExpression+MLNPrivateAdditions.h" diff --git a/platform/darwin/src/MLNStyleValue.mm b/platform/darwin/src/MLNStyleValue.mm index 446de0d807c4..17fc416bef27 100644 --- a/platform/darwin/src/MLNStyleValue.mm +++ b/platform/darwin/src/MLNStyleValue.mm @@ -76,7 +76,8 @@ id MLNJSONObjectFromMBGLExpression(const mln::style::expression::Expression &mbg mapbox::base::ValueObject dictionary; NSDictionary *dictionaryObject = (NSDictionary *)object; for (NSString *key in dictionaryObject) { - dictionary.emplace(std::string(key.UTF8String), MLNValueFromJSONObject(dictionaryObject[key])); + dictionary.emplace(std::string(key.UTF8String), + MLNValueFromJSONObject(dictionaryObject[key])); } return dictionary; } diff --git a/platform/darwin/test/MLNStyleTests.mm b/platform/darwin/test/MLNStyleTests.mm index b8f779c1467a..da9ff7306f64 100644 --- a/platform/darwin/test/MLNStyleTests.mm +++ b/platform/darwin/test/MLNStyleTests.mm @@ -541,8 +541,9 @@ - (void)testLanguageMatching { // MARK: Transition tests - (void)testGlobalState { - XCTAssertEqualObjects(self.style.globalState, @{}, - @"A style without a root state property should have an empty global state."); + XCTAssertEqualObjects( + self.style.globalState, @{}, + @"A style without a root state property should have an empty global state."); [self.style setGlobalStateValue:@YES forProperty:@"showLabels"]; XCTAssertEqualObjects(self.style.globalState[@"showLabels"], @YES); @@ -564,7 +565,8 @@ - (void)testVisibilityExpression { // The expression is evaluated against the current global state right away. [self.style setGlobalStateValue:@NO forProperty:@"show"]; - NSArray *json = @[ @"case", @[ @"to-boolean", @[ @"global-state", @"show" ] ], @"visible", @"none" ]; + NSArray *json = + @[ @"case", @[ @"to-boolean", @[ @"global-state", @"show" ] ], @"visible", @"none" ]; layer.visibilityExpression = [NSExpression expressionWithMLNJSONObject:json]; XCTAssertFalse(layer.visible); XCTAssertNotNil(layer.visibilityExpression); @@ -593,8 +595,8 @@ - (void)testVisibilityExpression { // Only global-state may be used. NSArray *zoomJson = @[ @"case", @[ @">", @[ @"zoom" ], @10 ], @"visible", @"none" ]; XCTAssertThrowsSpecificNamed( - layer.visibilityExpression = [NSExpression expressionWithMLNJSONObject:zoomJson], - NSException, NSInvalidArgumentException); + layer.visibilityExpression = [NSExpression expressionWithMLNJSONObject:zoomJson], NSException, + NSInvalidArgumentException); } - (void)testTransition { diff --git a/platform/node/src/node_map.cpp b/platform/node/src/node_map.cpp index b8eaf2f3967b..3da4968445a9 100644 --- a/platform/node/src/node_map.cpp +++ b/platform/node/src/node_map.cpp @@ -1411,11 +1411,12 @@ mln::Value toGlobalStateValue(const mln::style::conversion::Convertible& value) } if (isObject(value)) { mapbox::base::ValueObject object; - eachMember(value, - [&](const std::string& key, const Convertible& member) -> std::optional { - object.emplace(key, toGlobalStateValue(member)); - return std::nullopt; - }); + eachMember( + value, + [&](const std::string& key, const Convertible& member) -> std::optional { + object.emplace(key, toGlobalStateValue(member)); + return std::nullopt; + }); return object; } return toValue(value).value_or(mln::Value()); diff --git a/src/mln/geometry/feature_index.cpp b/src/mln/geometry/feature_index.cpp index f3b1fbbc513e..fc32c8a2d7ae 100644 --- a/src/mln/geometry/feature_index.cpp +++ b/src/mln/geometry/feature_index.cpp @@ -290,10 +290,9 @@ void FeatureIndex::addFeature(std::unordered_map(tileID.z), geometryTileFeature.get()} - .withGlobalState(options.globalState.get()))) { + if (options.filter && !(*options.filter)(style::expression::EvaluationContext{static_cast(tileID.z), + geometryTileFeature.get()} + .withGlobalState(options.globalState.get()))) { continue; } diff --git a/src/mln/renderer/layers/render_color_relief_layer.cpp b/src/mln/renderer/layers/render_color_relief_layer.cpp index 476c947d63a6..78d8564c0178 100644 --- a/src/mln/renderer/layers/render_color_relief_layer.cpp +++ b/src/mln/renderer/layers/render_color_relief_layer.cpp @@ -64,8 +64,8 @@ void RenderColorReliefLayer::evaluate(const PropertyEvaluationParameters& parame if (colorRampGlobalState != parameters.globalState) { const auto& rampValue = unevaluated.get().getValue(); const bool rampAffected = (rampValue.getDependencies() & style::expression::Dependency::GlobalState) && - style::expression::globalStateRefsIntersect( - rampValue.getGlobalStateRefs(), parameters.changedGlobalStateKeys.get()); + style::expression::globalStateRefsIntersect(rampValue.getGlobalStateRefs(), + parameters.changedGlobalStateKeys.get()); colorRampGlobalState = parameters.globalState; if (rampAffected) { updateColorRamp(); diff --git a/src/mln/renderer/layers/render_line_layer.cpp b/src/mln/renderer/layers/render_line_layer.cpp index 2e10739036e0..4891628e24b2 100644 --- a/src/mln/renderer/layers/render_line_layer.cpp +++ b/src/mln/renderer/layers/render_line_layer.cpp @@ -70,8 +70,8 @@ void RenderLineLayer::evaluate(const PropertyEvaluationParameters& parameters) { if (colorRampGlobalState != parameters.globalState) { const auto& rampValue = unevaluated.get().getValue(); const bool rampAffected = (rampValue.getDependencies() & style::expression::Dependency::GlobalState) && - style::expression::globalStateRefsIntersect( - rampValue.getGlobalStateRefs(), parameters.changedGlobalStateKeys.get()); + style::expression::globalStateRefsIntersect(rampValue.getGlobalStateRefs(), + parameters.changedGlobalStateKeys.get()); colorRampGlobalState = parameters.globalState; if (rampAffected) { updateColorRamp(); diff --git a/src/mln/style/layers/location_indicator_layer_impl.hpp b/src/mln/style/layers/location_indicator_layer_impl.hpp index d89968de02d8..786309a759f7 100644 --- a/src/mln/style/layers/location_indicator_layer_impl.hpp +++ b/src/mln/style/layers/location_indicator_layer_impl.hpp @@ -29,7 +29,7 @@ class LocationIndicatorLayer::Impl : public Layer::Impl { return layout.getDependencies() | Layer::Impl::getLayoutDependencies(); } - void collectLayoutGlobalStateRefs(std::set& refs) const override { + void collectLayoutGlobalStateRefs(std::set &refs) const override { layout.collectGlobalStateRefs(refs); Layer::Impl::collectLayoutGlobalStateRefs(refs); } diff --git a/src/mln/style/properties.hpp b/src/mln/style/properties.hpp index 2b6d56060b74..ea16aca26759 100644 --- a/src/mln/style/properties.hpp +++ b/src/mln/style/properties.hpp @@ -301,7 +301,6 @@ class Properties { } public: - unsigned long constantsMask() const noexcept { return ConstantsMask::getMask(*this); } }; @@ -412,7 +411,6 @@ class Properties { } protected: - template bool updateGPUExpression(Unevaluated::GPUExpressions& exprs, TimePoint now) const { constexpr auto index = TypeIndex::value; diff --git a/src/mln/tile/custom_geometry_tile.cpp b/src/mln/tile/custom_geometry_tile.cpp index 5ad61f68dc3f..c602293cbf8e 100644 --- a/src/mln/tile/custom_geometry_tile.cpp +++ b/src/mln/tile/custom_geometry_tile.cpp @@ -79,10 +79,9 @@ void CustomGeometryTile::querySourceFeatures(std::vector& result, const auto feature = layer->getFeature(i); // Apply filter, if any - if (queryOptions.filter && - !(*queryOptions.filter)( - style::expression::EvaluationContext{static_cast(id.overscaledZ), feature.get()} - .withGlobalState(queryOptions.globalState.get()))) { + if (queryOptions.filter && !(*queryOptions.filter)(style::expression::EvaluationContext{ + static_cast(id.overscaledZ), feature.get()} + .withGlobalState(queryOptions.globalState.get()))) { continue; } diff --git a/src/mln/tile/geojson_tile.cpp b/src/mln/tile/geojson_tile.cpp index 050794074b9a..7dd5e8f55b5e 100644 --- a/src/mln/tile/geojson_tile.cpp +++ b/src/mln/tile/geojson_tile.cpp @@ -47,10 +47,9 @@ void GeoJSONTile::querySourceFeatures(std::vector& result, const Source auto feature = layer->getFeature(i); // Apply filter, if any - if (options.filter && - !(*options.filter)( - style::expression::EvaluationContext{static_cast(this->id.overscaledZ), feature.get()} - .withGlobalState(options.globalState.get()))) { + if (options.filter && !(*options.filter)(style::expression::EvaluationContext{ + static_cast(this->id.overscaledZ), feature.get()} + .withGlobalState(options.globalState.get()))) { continue; } diff --git a/src/mln/tile/geometry_tile.cpp b/src/mln/tile/geometry_tile.cpp index fbe5c1b7f66f..82a5c393c466 100644 --- a/src/mln/tile/geometry_tile.cpp +++ b/src/mln/tile/geometry_tile.cpp @@ -567,10 +567,9 @@ void GeometryTile::querySourceFeatures(std::vector& result, const Sourc auto feature = layer->getFeature(i); // Apply filter, if any - if (options.filter && - !(*options.filter)( - style::expression::EvaluationContext{static_cast(this->id.overscaledZ), feature.get()} - .withGlobalState(options.globalState.get()))) { + if (options.filter && !(*options.filter)(style::expression::EvaluationContext{ + static_cast(this->id.overscaledZ), feature.get()} + .withGlobalState(options.globalState.get()))) { continue; } diff --git a/test/style/expression/global_state.test.cpp b/test/style/expression/global_state.test.cpp index 97685cf087c4..f851bac0b5d3 100644 --- a/test/style/expression/global_state.test.cpp +++ b/test/style/expression/global_state.test.cpp @@ -110,8 +110,7 @@ TEST(GlobalStateExpression, EvaluateArray) { } TEST(GlobalStateExpression, EvaluateInComposition) { - auto expr = parseExpression(R"(["case", ["to-boolean", ["global-state", "enabled"]], 10.0, 5.0])", - {type::Number}); + auto expr = parseExpression(R"(["case", ["to-boolean", ["global-state", "enabled"]], 10.0, 5.0])", {type::Number}); ASSERT_TRUE(expr); EXPECT_TRUE(expr->dependencies & Dependency::GlobalState); @@ -150,8 +149,7 @@ TEST(GlobalStateExpression, PropertyExpressionCapturesGlobalState) { auto state = std::make_shared(GlobalStateMap{{"width", 3.0}}); // State provided via the evaluation context. - EXPECT_EQ(3.0f, - propertyExpression.evaluate(EvaluationContext(0.0f).withGlobalState(state.get()), 0.0f)); + EXPECT_EQ(3.0f, propertyExpression.evaluate(EvaluationContext(0.0f).withGlobalState(state.get()), 0.0f)); // Captured state is used when the context has none. propertyExpression.captureGlobalState(state); @@ -236,8 +234,8 @@ TEST(GlobalStateExpression, FormatSectionOverrideUsesCapturedState) { } TEST(GlobalStateExpression, CollectGlobalStateRefs) { - auto expr = parseExpression(R"(["case", ["to-boolean", ["global-state", "a"]], ["to-number", ["global-state", "b"]], 0])", - {type::Number}); + auto expr = parseExpression( + R"(["case", ["to-boolean", ["global-state", "a"]], ["to-number", ["global-state", "b"]], 0])", {type::Number}); ASSERT_TRUE(expr); std::set refs; From e42efbedb96dddfea157c68eebf7c278002cb55d Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 18:05:42 +0900 Subject: [PATCH 15/32] feat(android): add Layer#getVisibilityExpression --- .../src/cpp/style/layers/layer.cpp | 16 ++++++++++++++- .../src/cpp/style/layers/layer.hpp | 1 + .../maplibre/android/style/layers/Layer.java | 20 +++++++++++++++++++ .../testapp/style/RuntimeStyleTests.java | 13 ++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/platform/android/MapLibreAndroid/src/cpp/style/layers/layer.cpp b/platform/android/MapLibreAndroid/src/cpp/style/layers/layer.cpp index 16f84d5fb740..99f46a9a8ab7 100644 --- a/platform/android/MapLibreAndroid/src/cpp/style/layers/layer.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/style/layers/layer.cpp @@ -198,6 +198,19 @@ jni::Local> Layer::getVisibility(jni::JNIEnv& env) { return std::move(*convert>>(env, layer->getVisibility())); } +jni::Local> Layer::getVisibilityExpression(jni::JNIEnv& env) { + auto layer = layerPtr.get(); + if (!layer) { + return jni::Local>(env, nullptr); + } + + const mln::Value serialized = layer->getVisibilityExpression(); + if (serialized.is()) { + return jni::Local>(env, nullptr); + } + return gson::JsonElement::New(env, serialized); +} + void Layer::registerNative(jni::JNIEnv& env) { // Lookup the class static auto& javaClass = jni::Class::Singleton(env); @@ -222,7 +235,8 @@ void Layer::registerNative(jni::JNIEnv& env) { METHOD(&Layer::getMaxZoom, "nativeGetMaxZoom"), METHOD(&Layer::setMinZoom, "nativeSetMinZoom"), METHOD(&Layer::setMaxZoom, "nativeSetMaxZoom"), - METHOD(&Layer::getVisibility, "nativeGetVisibility")); + METHOD(&Layer::getVisibility, "nativeGetVisibility"), + METHOD(&Layer::getVisibilityExpression, "nativeGetVisibilityExpression")); } } // namespace android diff --git a/platform/android/MapLibreAndroid/src/cpp/style/layers/layer.hpp b/platform/android/MapLibreAndroid/src/cpp/style/layers/layer.hpp index 980b10e0b643..ca0f734439e9 100644 --- a/platform/android/MapLibreAndroid/src/cpp/style/layers/layer.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/style/layers/layer.hpp @@ -62,6 +62,7 @@ class Layer { // Property getters jni::Local> getVisibility(jni::JNIEnv&); + jni::Local> getVisibilityExpression(jni::JNIEnv&); protected: /* diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/Layer.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/Layer.java index c0ba3b4240a0..1fd79b18fb1a 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/Layer.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/style/layers/Layer.java @@ -80,6 +80,22 @@ public PropertyValue getVisibility() { return new PaintPropertyValue<>("visibility", (String) nativeGetVisibility()); } + /** + * Get the visibility of this layer as an expression. + * + * @return the visibility expression, or null when the visibility is a constant value + */ + @Nullable + public Expression getVisibilityExpression() { + checkThread(); + JsonElement jsonElement = nativeGetVisibilityExpression(); + if (jsonElement != null) { + return Expression.Converter.convert(jsonElement); + } else { + return null; + } + } + public float getMinZoom() { checkThread(); return nativeGetMinZoom(); @@ -112,6 +128,10 @@ public void setMaxZoom(float zoom) { @Keep protected native Object nativeGetVisibility(); + @Nullable + @Keep + private native JsonElement nativeGetVisibilityExpression(); + @Keep protected native void nativeSetLayoutProperty(String name, Object value); diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java index 7041d4610027..f9bfb363ff31 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/style/RuntimeStyleTests.java @@ -102,6 +102,19 @@ public void perform(UiController uiController, View view) { style.setGlobalStateProperty("showLayer", new JsonPrimitive(false)); assertEquals(Property.NONE, layer.getVisibility().getValue()); + + // The expression can be read back; parsing wraps the untyped state + // value with a string type assertion. + layer.setProperties(PropertyFactory.visibility(Expression.globalState("vis"))); + Expression expression = layer.getVisibilityExpression(); + assertNotNull(expression); + assertEquals( + java.util.Arrays.deepToString(new Object[] {"string", new Object[] {"global-state", "vis"}}), + java.util.Arrays.deepToString(expression.toArray())); + + // A constant visibility has no expression. + layer.setProperties(PropertyFactory.visibility(Property.VISIBLE)); + assertNull(layer.getVisibilityExpression()); } }); } From cb3351e9f1b7bba81313a7f3dbfbfc6e1fc5ec03 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 24 Aug 2026 23:43:15 +0900 Subject: [PATCH 16/32] fix(core): address global-state CI build failures --- platform/darwin/src/MLNConversion.h | 2 ++ src/mln/style/layer.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/platform/darwin/src/MLNConversion.h b/platform/darwin/src/MLNConversion.h index 3d0bb71c87c5..0e8faed6cec8 100644 --- a/platform/darwin/src/MLNConversion.h +++ b/platform/darwin/src/MLNConversion.h @@ -1,3 +1,5 @@ +#import + #include NS_ASSUME_NONNULL_BEGIN diff --git a/src/mln/style/layer.cpp b/src/mln/style/layer.cpp index d2ca37a80007..7c7789b656ba 100644 --- a/src/mln/style/layer.cpp +++ b/src/mln/style/layer.cpp @@ -108,7 +108,7 @@ VisibilityType evaluateVisibilityExpression(const expression::Expression& expres } } Log::Warning(Event::ParseStyle, - "visibility expression did not evaluate to \"visible\" or \"none\"; defaulting to \"visible\""); + R"(visibility expression did not evaluate to "visible" or "none"; defaulting to "visible")"); return VisibilityType::Visible; } From b388d5dacc816eb89557b7ddf26a26412b511f72 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Tue, 25 Aug 2026 08:49:27 +0900 Subject: [PATCH 17/32] update: v8.json from @maplibre/maplibre-gl-style-spec --- scripts/style-spec-reference/v8.json | 742 ++++++++++++++++++++------- 1 file changed, 569 insertions(+), 173 deletions(-) diff --git a/scripts/style-spec-reference/v8.json b/scripts/style-spec-reference/v8.json index c94ce02c72e8..36954550ac94 100644 --- a/scripts/style-spec-reference/v8.json +++ b/scripts/style-spec-reference/v8.json @@ -28,6 +28,7 @@ "center": { "type": "array", "value": "number", + "length": 2, "doc": "Default map center in longitude and latitude. The style center will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", "example": [ -73.9749, @@ -97,15 +98,6 @@ } } }, - "light": { - "type": "light", - "doc": "The global light source.", - "example": { - "anchor": "viewport", - "color": "white", - "intensity": 0.4 - } - }, "state": { "type": "state", "default": {}, @@ -126,6 +118,15 @@ } } }, + "light": { + "type": "light", + "doc": "The global light source.", + "example": { + "anchor": "viewport", + "color": "white", + "intensity": 0.4 + } + }, "sky": { "type": "sky", "doc": "The map's sky configuration. **Note:** this definition is still experimental and is under development in maplibre-gl-js.", @@ -212,15 +213,14 @@ "ios": "0.1.0" }, "omit to use local fonts": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/3302", + "js": "5.11.0", "android": "https://github.com/maplibre/maplibre-native/issues/165", "ios": "https://github.com/maplibre/maplibre-native/issues/165" } } }, "font-faces": { - "type": "array", - "value": "fontFaces", + "type": "fontFaces", "doc": "The `font-faces` property can be used to specify what font files to use for rendering text. Font faces contain information needed to render complex texts such as [Devanagari](https://en.wikipedia.org/wiki/Devanagari), [Khmer](https://en.wikipedia.org/wiki/Khmer_script) among many others.

Unicode range

The optional `unicode-range` property can be used to only use a particular font file for characters within the specified unicode range(s). Its value should be an array of strings, each indicating a start and end of a unicode range, similar to the [CSS descriptor with the same name](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range). This allows specifying multiple non-consecutive unicode ranges. When not specified, the default value is `U+0-10FFFF`, meaning the font file will be used for all unicode characters.\n\nRefer to the [Unicode Character Code Charts](https://www.unicode.org/charts/) to see ranges for scripts supported by Unicode. To see what unicode code-points are available in a font, use a tool like [FontDrop](https://fontdrop.info/).\n\n

Font Resolution

For every name in a symbol layer’s [`text-font`](./layers.md/#text-font) array, characters are matched if they are covered one of the by the font files in the corresponding entry of the `font-faces` map. Any still-unmatched characters then fall back to the [`glyphs`](./glyphs.md) URL if provided.\n\n

Supported Fonts

What type of fonts are supported is implementation-defined. Unsupported fonts are ignored.", "example": { "Noto Sans Regular": [{ @@ -243,7 +243,7 @@ }, "sdk-support": { "basic functionality": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/50", + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6637", "android": "11.13.0", "ios": "6.18.0" } @@ -383,9 +383,9 @@ "js": "supported" }, "mlt": { - "android": "https://github.com/maplibre/maplibre-native/issues/3721", - "ios": "https://github.com/maplibre/maplibre-native/issues/3721", - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6258" + "android": "12.1.0", + "ios": "6.20.0", + "js": "5.12.0" } } }, @@ -654,7 +654,7 @@ "doc": "Size of the tile buffer on each side. A value of 0 produces no buffer. A value of 512 produces a buffer as wide as the tile itself. Larger values produce fewer rendering artifacts near tile edges and slower performance." }, "filter": { - "type": "*", + "type": "filter", "doc": "An expression for filtering features prior to processing them for rendering." }, "tolerance": { @@ -948,8 +948,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { - "js": "5.16.0", + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -1006,8 +1006,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { - "js": "5.16.0", + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -1064,8 +1064,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { - "js": "5.16.0", + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -1097,6 +1097,11 @@ "js": "0.41.0", "android": "6.0.0", "ios": "4.0.0" + }, + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -1109,20 +1114,6 @@ } }, "layout_fill-extrusion": { - "fill-extrusion-rounded-corner-distance": { - "type": "number", - "default": 0, - "minimum": 0, - "units": "meters", - "doc": "The distance from the corner that will be cut and replaced with rounded corner.", - "sdk-support": { - "basic functionality": { - "android": "13.0.0", - "ios": "6.0.0" - } - }, - "property-type": "constant" - }, "visibility": { "type": "enum", "values": { @@ -1140,6 +1131,11 @@ "js": "0.27.0", "android": "5.1.0", "ios": "3.6.0" + }, + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -1308,8 +1304,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { - "js": "5.16.0", + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -2335,6 +2331,7 @@ "zoom" ] }, + "example": ["center", "left", "right"], "property-type": "data-constant" }, "text-variable-anchor-offset": { @@ -2367,6 +2364,7 @@ "feature" ] }, + "example": ["top", [0, 4], "left", [3, 0], "bottom", [1, 1]], "property-type": "data-driven" }, "text-anchor": { @@ -2774,8 +2772,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { - "js": "5.16.0", + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -2808,8 +2806,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { - "js": "5.16.0", + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -2841,6 +2839,11 @@ "js": "0.43.0", "android": "6.0.0", "ios": "4.0.0" + }, + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -2867,6 +2870,14 @@ "doc": "Whether this layer is displayed.", "sdk-support": { "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3408", + "ios": "https://github.com/maplibre/maplibre-native/issues/3408" + }, + "`global state` expression": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -2879,9 +2890,16 @@ } }, "filter": { - "type": "array", - "value": "*", - "doc": "A filter selects specific features from a layer." + "type": "boolean", + "doc": "A filter selects specific features from a layer.", + "expression": { + "interpolated": false, + "parameters": [ + "zoom", + "feature" + ] + }, + "property-type": "data-driven" }, "filter_operator": { "type": "enum", @@ -3018,12 +3036,12 @@ }, "expression": { "type": "array", - "value": "*", + "value": "expression_name", "minimum": 1, - "doc": "An expression defines a function that can be used for data-driven style properties or feature filters." + "doc": "An expression defines a function that can be used for data-driven style properties or feature filters. The first element of an expression array is a string naming the expression operator, e.g. `\"*\"` or `\"case\"`. Elements that follow (if any) are the _arguments_ to the expression. Each argument is either a literal value (a string, number, boolean, or `null`), or another expression array." }, "expression_name": { - "doc": "", + "doc": "First element in an expression array. May be followed by a number of arguments.", "type": "enum", "values": { "let": { @@ -3031,25 +3049,25 @@ "syntax": { "overloads": [ { - "parameters": ["var_1_name", "var_1_value", "...", "var_n_name", "var_n_value", "expression"], + "parameters": ["var_name_1", "var_value_1", "...", "var_name_n", "var_value_n", "expression"], "output-type": "any" } ], "parameters": [ { - "name": "var_i_name", + "name": "var_name_i", "type": "string literal", - "description": "The name of the i-th variable." + "doc": "The name of the i-th variable." }, { - "name": "var_i_value", + "name": "var_value_i", "type": "any", - "description": "The value of the i-th variable." + "doc": "The value of the i-th variable." }, { "name": "expression", "type": "any", - "description": "The expression within which the named variables can be referenced." + "doc": "The expression within which the named variables can be referenced." } ] }, @@ -3076,7 +3094,7 @@ { "name": "var_name", "type": "string literal", - "description": "The name of the variable bound using `let`." + "doc": "The name of the variable bound using `let`." } ] }, @@ -3148,13 +3166,13 @@ }, { "name": "type", - "type": "\"string\" | \"number\" | \"boolean\"", - "description": "The asserted type of the input array." + "type": ["string", "number", "boolean"], + "doc": "The asserted type of the input array." }, { "name": "length", "type": "number literal", - "description": "The asserted length of the input array." + "doc": "The asserted length of the input array." } ] }, @@ -3181,12 +3199,12 @@ { "name": "index", "type": "number", - "description": "The index into `array`." + "doc": "The index into `array`." }, { "name": "array", "type": "array", - "description": "The array of items to retrieve the specified item from." + "doc": "The array of items to retrieve the specified item from." } ] }, @@ -3217,22 +3235,22 @@ { "name": "item", "type": "T", - "description": "The needle to search for within `array`." + "doc": "The needle to search for within `array`." }, { "name": "array", "type": "array", - "description": "The haystack through which to search for `item`." + "doc": "The haystack through which to search for `item`." }, { "name": "substring", "type": "string", - "description": "The needle to search for within `string`." + "doc": "The needle to search for within `string`." }, { "name": "string", "type": "string", - "description": "The haystack through which to search for `substring`." + "doc": "The haystack through which to search for `substring`." } ] }, @@ -3263,27 +3281,27 @@ { "name": "item", "type": "T", - "description": "The needle to search for within `array`." + "doc": "The needle to search for within `array`." }, { "name": "array", "type": "array", - "description": "The haystack through which to search for `item`." + "doc": "The haystack through which to search for `item`." }, { "name": "substring", "type": "string", - "description": "The needle to search for within `string`." + "doc": "The needle to search for within `string`." }, { "name": "string", "type": "string", - "description": "The haystack through which to search for `substring`." + "doc": "The haystack through which to search for `substring`." }, { "name": "from_index", "type": "number", - "description": "The index from where to begin the search." + "doc": "The index from where to begin the search." } ] }, @@ -3314,22 +3332,22 @@ { "name": "array", "type": "array", - "description": "The original array from which to extract the subarray." + "doc": "The original array from which to extract the subarray." }, { "name": "string", "type": "string", - "description": "The original string from which to extract the substring." + "doc": "The original string from which to extract the substring." }, { "name": "start_index", "type": "number", - "description": "The inclusive index from which `slice` extracts items or characters from the subarray or substring." + "doc": "The inclusive index from which `slice` extracts items or characters from the subarray or substring." }, { "name": "end_index", "type": "number", - "description": "The non-inclusive index up to which `slice` extracts items or characters from the subarray or substring." + "doc": "The non-inclusive index up to which `slice` extracts items or characters from the subarray or substring." } ] }, @@ -3364,7 +3382,7 @@ { "name": "fallback", "type": "any", - "description": "The result when no condition evaluates to true." + "doc": "The result when no condition evaluates to true." } ] }, @@ -3390,23 +3408,23 @@ "parameters": [ { "name": "input", - "type": "string | number", - "description": "Any expression." + "type": ["string", "number"], + "doc": "Any expression." }, { "name": "label_i", - "type": "string literal | number literal | array | array", - "description": "The i-th literal value or array of literal values to match the input against." + "type": ["string literal", "number literal", "array", "array"], + "doc": "The i-th literal value or array of literal values to match the input against." }, { "name": "output_i", "type": "any", - "description": "The result when the i-th label is the first label to match the input." + "doc": "The result when the i-th label is the first label to match the input." }, { "name": "fallback", "type": "any", - "description": "The result when no label matches the input." + "doc": "The result when no label matches the input." } ] }, @@ -3451,7 +3469,7 @@ "syntax": { "overloads": [ { - "parameters": ["input", "output_0", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "parameters": ["input", "output_0", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], "output-type": "any" } ], @@ -3459,22 +3477,22 @@ { "name": "input", "type": "number", - "description": "Any numeric expression." + "doc": "Any numeric expression." }, { "name": "output_0", "type": "any", - "description": "The result when the `input` is less than the first stop." + "doc": "The result when the `input` is less than the first stop." }, { - "name": "stop_i_input", + "name": "stop_input_i", "type": "number literal", - "description": "The value of the i-th stop against which the `input` is compared." + "doc": "The value of the i-th stop against which the `input` is compared." }, { - "name": "stop_i_output", + "name": "stop_output_i", "type": "any", - "description": "The result when the i-th stop is the last stop less than the `input`." + "doc": "The result when the i-th stop is the last stop less than the `input`." } ] }, @@ -3489,34 +3507,34 @@ } }, "interpolate": { - "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, `color`, `array`, or `projection`.\n\nInterpolation types:\n\n- `[\"linear\"]`, or an expression returning one of those types: Interpolates linearly between the pair of stops just less than and just greater than the input.\n\n- `[\"exponential\", base]`: Interpolates exponentially between the stops just less than and just greater than the input. `base` controls the rate at which the output increases: higher values make the output increase more towards the high end of the range. With values close to 1 the output increases linearly.\n\n- `[\"cubic-bezier\", x1, y1, x2, y2]`: Interpolates using the cubic bezier curve defined by the given control points.\n\n - [Animate map camera around a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-camera-around-point/)\n\n - [Change building color based on zoom level](https://maplibre.org/maplibre-gl-js/docs/examples/change-building-color-based-on-zoom-level/)\n\n - [Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/examples/heatmap-layer/)\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", + "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, `color`, `array`, or `projection`.\n\n - [Animate map camera around a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-camera-around-point/)\n\n - [Change building color based on zoom level](https://maplibre.org/maplibre-gl-js/docs/examples/change-building-color-based-on-zoom-level/)\n\n - [Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/examples/heatmap-layer/)\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], - "output-type": "number | array | color | array | projection" + "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], + "output-type": ["number", "array", "color", "array", "projection"] } ], "parameters": [ { "name": "interpolation_type", - "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]", - "description": "The interpolation type." + "type": "interpolation", + "doc": "The interpolation type." }, { "name": "input", "type": "number", - "description": "Any numeric expression." + "doc": "Any numeric expression." }, { - "name": "stop_i_input", + "name": "stop_input_i", "type": "number literal", - "description": "The value of the i-th stop against which the `input` is compared." + "doc": "The value of the i-th stop against which the `input` is compared." }, { - "name": "stop_i_output", - "type": "number | array | color | array | projection", - "description": "The output value corresponding to the i-th stop." + "name": "stop_output_i", + "type": ["number", "array", "color", "array", "projection"], + "doc": "The output value corresponding to the i-th stop." } ] }, @@ -3535,26 +3553,27 @@ "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], - "output-type": "color | array" + "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], + "output-type": ["color", "array"] } ], "parameters": [ { "name": "interpolation_type", - "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]" + "type": "interpolation", + "doc": "The interpolation type." }, { "name": "input", "type": "number" }, { - "name": "stop_i_input", + "name": "stop_input_i", "type": "number literal" }, { - "name": "stop_i_output", - "type": "color | array" + "name": "stop_output_i", + "type": ["color", "array"] } ] }, @@ -3573,26 +3592,27 @@ "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], - "output-type": "color | array" + "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], + "output-type": ["color", "array"] } ], "parameters": [ { "name": "interpolation_type", - "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]" + "type": "interpolation", + "doc": "The interpolation type." }, { "name": "input", "type": "number" }, { - "name": "stop_i_input", + "name": "stop_input_i", "type": "number literal" }, { - "name": "stop_i_output", - "type": "color | array" + "name": "stop_output_i", + "type": ["color", "array"] } ] }, @@ -3797,7 +3817,7 @@ } }, "collator": { - "doc": "Returns a `collator` for use in locale-dependent comparison operations. The `case-sensitive` and `diacritic-sensitive` options default to `false`. The `locale` argument specifies the IETF language tag of the locale to use. If none is provided, the default locale is used. If the requested locale is not available, the `collator` will use a system-defined fallback locale. Use `resolved-locale` to test the results of locale fallback behavior.", + "doc": "Returns a `collator` for use in locale-dependent comparison operations. Use `resolved-locale` to test the results of locale fallback behavior.", "syntax": { "overloads": [ { @@ -3808,7 +3828,25 @@ "parameters": [ { "name": "options", - "type": "{ \"case-sensitive\"?: boolean, \"diacritic-sensitive\"?: boolean, \"locale\"?: string }" + "type": { + "case-sensitive": { + "type": "boolean", + "default": false, + "example": true, + "doc": "If characters of different case-ness are considered different" + }, + "diacritic-sensitive": { + "type": "boolean", + "default": false, + "example": true, + "doc": "If characters with different diacritics are considered different" + }, + "locale": { + "type": "string", + "example": "en", + "doc": "IETF language tag of the locale to use. If none is provided, the default locale is used. If the requested locale is not available, the `collator` will use a system-defined fallback locale." + } + } } ] }, @@ -3823,7 +3861,7 @@ } }, "format": { - "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n- `\"vertical-align\"`: Aligns vertically text section or image in relation to the row it belongs to. Possible values are: \n\t- `\"bottom\"` *default*: align the bottom of this section with the bottom of other sections.\n\"Visual\n\t- `\"center\"`: align the center of this section with the center of other sections.\n\"Visual\n\t- `\"top\"`: align the top of this section with the top of other sections.\n\"Visual\n\t- Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", + "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", "syntax": { "overloads": [ { @@ -3834,11 +3872,44 @@ "parameters": [ { "name": "input_i", - "type": "string | image" + "type": ["string", "image"] }, { "name": "style_overrides_i", - "type": "{ \"text-font\"?: string, \"text-color\"?: color, \"font-scale\"?: number, \"vertical-align\"?: \"bottom\" | \"center\" | \"top\" }" + "type": { + "text-font": { + "type": "string", + "doc": "Overrides the font stack specified by the root layout property.", + "example": "Arial Unicode MS Regular" + }, + "text-color": { + "type": "color", + "doc": "Overrides the color specified by the root paint property.", + "example": "#333" + }, + "font-scale": { + "type": "number", + "doc":"Applies a scaling factor on `text-size` as specified by the root layout property.", + "example": 1.2 + }, + "vertical-align": { + "type": "enum", + "doc": "Aligns a vertical text section or image in relation to the row it belongs to. Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.", + "default": "bottom", + "example": "bottom", + "values": { + "bottom": { + "doc": "align the bottom of this section with the bottom of other sections.\n![Visual representation of bottom alignment](https://github.com/user-attachments/assets/0474a2fd-a4b2-417c-9187-7a13a28695bc)" + }, + "center": { + "doc": "align the center of this section with the center of other sections.\n![Visual representation of center alignment](https://github.com/user-attachments/assets/92237455-be6d-4c5d-b8f6-8127effc1950)" + }, + "top": { + "doc": "align the top of this section with the top of other sections.\n![Visual representation of top alignment](https://github.com/user-attachments/assets/45dccb28-d977-4abb-a006-4ea9792b7c53)" + } + } + } + } } ] }, @@ -3903,8 +3974,35 @@ } } }, + "global-state": { + "doc": "Retrieves a property value from global state that can be set with platform-specific APIs. Defaults can be provided using the [`state`](https://maplibre.org/maplibre-style-spec/root/#state) root property. Returns `null` if no value nor default value is set for the retrieved property.", + "group": "Lookup", + "syntax": { + "overloads": [ + { + "parameters": ["property_name"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "property_name", + "type": "string literal", + "doc": "The name of the global state property to retrieve." + } + ] + }, + "example": ["global-state", "someProperty"], + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" + } + } + }, "number-format": { - "doc": "Converts the input number into a string representation using the providing formatting rules. If set, the `locale` argument specifies the locale to use, as a BCP 47 language tag. If set, the `currency` argument specifies an ISO 4217 code to use for currency-style formatting. If set, the `min-fraction-digits` and `max-fraction-digits` arguments specify the minimum and maximum number of fractional digits to include.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "doc": "Converts the input number into a string representation using the provided format_options.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", "syntax": { "overloads": [ { @@ -3915,11 +4013,39 @@ "parameters": [ { "name": "input", - "type": "number" + "type": "number", + "doc": "number to format" }, { "name": "format_options", - "type": "{ \"locale\"?: string, \"currency\"?: string, \"min-fraction-digits\"?: number, \"max-fraction-digits\"?: number }" + "type": { + "locale": { + "type": "string", + "example": "en", + "doc": "Specifies the locale to use, as a BCP 47 language tag" + }, + "currency": { + "type": "string", + "example": "USD", + "doc": "An ISO 4217 code to use for currency-style formatting" + }, + "unit": { + "type": "string", + "example": "meter", + "doc": "A CLDR or ECMA-402 unit to use for quantity-style formatting" + }, + "min-fraction-digits": { + "type": "number", + "example": 1, + "doc": "Minimum number of fractional digits to include" + }, + "max-fraction-digits": { + "type": "number", + "example": 2, + "doc": "Maximum number of fractional digits to include" + } + }, + "doc": "Format options for the number" } ] }, @@ -3930,6 +4056,11 @@ "js": "0.54.0", "android": "8.4.0", "ios": "supported" + }, + "`unit` property": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/7024", + "android": "https://github.com/maplibre/maplibre-native/issues/4071", + "ios": "https://github.com/maplibre/maplibre-native/issues/4071" } } }, @@ -4098,7 +4229,7 @@ } }, "rgba": { - "doc": "Creates a color value from red, green, blue components, which must range between 0 and 255, and an alpha component which must range between 0 and 1. If any component is out of range, the expression is an error.", + "doc": "Creates a color value from red, green, blue components, which must range between 0 and 255, and an alpha component which must range between zero and one. If any component is out of range, the expression is an error.", "syntax": { "overloads": [ { @@ -4148,12 +4279,12 @@ { "name": "property_name", "type": "string", - "description": "The name of the property to retrieve the value of." + "doc": "The name of the property to retrieve the value of." }, { "name": "object", "type": "object", - "description": "The object to retrieve the value from." + "doc": "The object to retrieve the value from." } ] }, @@ -4180,12 +4311,12 @@ { "name": "property_name", "type": "string", - "description": "The name of the property to test for the presence of." + "doc": "The name of the property to test for the presence of." }, { "name": "object", "type": "object", - "description": "The object in which to test for the presence of the `property_name` property." + "doc": "The object in which to test for the presence of the `property_name` property." } ] }, @@ -4211,7 +4342,7 @@ "parameters": [ { "name": "array_or_string", - "type": "array | string" + "type": ["array", "string"] } ] }, @@ -4271,32 +4402,6 @@ } } }, - "global-state": { - "doc": "Retrieves a property value from the map's global state. Returns null if the requested property is not present in the global state. The global state can be set programmatically and defaults can be defined using the root-level `state` style property.", - "syntax": { - "overloads": [ - { - "parameters": ["property_name"], - "output-type": "any" - } - ], - "parameters": [ - { - "name": "property_name", - "type": "string" - } - ] - }, - "example": ["global-state", "chargerType"], - "group": "Feature data", - "sdk-support": { - "basic functionality": { - "js": "5.6.0", - "android": "https://github.com/maplibre/maplibre-native/issues/3302", - "ios": "https://github.com/maplibre/maplibre-native/issues/3302" - } - } - }, "geometry-type": { "doc": "Returns the feature's simple geometry type: `Point`, `LineString`, or `Polygon`. `MultiPoint`, `MultiLineString`, and `MultiPolygon` are returned as `Point`, `LineString`, and `Polygon`, respectively.", "syntax": { @@ -4506,17 +4611,17 @@ { "name": "input_1", "type": "number", - "description": "The number from which to subtract `input_2`." + "doc": "The number from which to subtract `input_2`." }, { "name": "input_2", "type": "number", - "description": "The number to subtract from `input_1`." + "doc": "The number to subtract from `input_1`." }, { "name": "single_input", "type": "number", - "description": "The number to subtract from 0." + "doc": "The number to subtract from 0." } ] }, @@ -4543,12 +4648,12 @@ { "name": "input_1", "type": "number", - "description": "The dividend." + "doc": "The dividend." }, { "name": "input_2", "type": "number", - "description": "The divisor." + "doc": "The divisor." } ] }, @@ -4575,12 +4680,12 @@ { "name": "input_1", "type": "number", - "description": "The dividend." + "doc": "The dividend." }, { "name": "input_2", "type": "number", - "description": "The divisor." + "doc": "The divisor." } ] }, @@ -4607,12 +4712,12 @@ { "name": "input_1", "type": "number", - "description": "The base." + "doc": "The base." }, { "name": "input_2", "type": "number", - "description": "The exponent." + "doc": "The exponent." } ] }, @@ -4639,7 +4744,7 @@ { "name": "input", "type": "number", - "description": "The radicand." + "doc": "The radicand." } ] }, @@ -5090,7 +5195,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5130,7 +5235,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5174,7 +5279,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5218,7 +5323,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5262,7 +5367,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5306,7 +5411,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5561,6 +5666,66 @@ "ios": "4.2.0" } } + }, + "split": { + "doc": "Returns an array of substrings formed by splitting an input string by a separator string.", + "group": "String", + "syntax": { + "overloads": [ + { + "parameters": ["input", "separator"], + "output-type": "array" + } + ], + "parameters": [ + { + "name": "input", + "type": "string" + }, + { + "name": "separator", + "type": "string" + } + ] + }, + "example": ["split", ["get", "name"], ";"], + "sdk-support": { + "basic functionality": { + "js": "5.20.0", + "android": "https://github.com/maplibre/maplibre-native/issues/4133", + "ios": "https://github.com/maplibre/maplibre-native/issues/4133" + } + } + }, + "join": { + "doc": "Returns a string formed by concatenating the elements of the input array, inserting a separator between each element.", + "group": "String", + "syntax": { + "overloads": [ + { + "parameters": ["input", "separator"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "input", + "type": "array" + }, + { + "name": "separator", + "type": "string" + } + ] + }, + "example": ["join", ["split", ["get", "name"], ";"], "\n"], + "sdk-support": { + "basic functionality": { + "js": "5.20.0", + "android": "https://github.com/maplibre/maplibre-native/issues/4133", + "ios": "https://github.com/maplibre/maplibre-native/issues/4133" + } + } } } }, @@ -6474,7 +6639,7 @@ "line-dasharray": { "type": "array", "value": "number", - "doc": "Specifies the lengths of the alternating dashes and gaps that form the dash pattern. The lengths are later scaled by the line width. To convert a dash length to pixels, multiply the length by the current line width. Note that GeoJSON sources with `lineMetrics: true` specified won't render dashed lines to the expected scale. Also note that zoom-dependent expressions will be evaluated only at integer zoom levels.", + "doc": "Specifies the lengths of the alternating dashes and gaps that form the dash pattern. The lengths are later scaled by the line width. To convert a dash length to pixels, multiply the length by the current line width. GeoJSON sources with `lineMetrics: true` specified won't render dashed lines to the expected scale. Zoom-dependent expressions will be evaluated only at integer zoom levels. The only way to create an array value is using `[\"literal\", [...]]`; arrays cannot be read from or derived from feature properties.", "minimum": 0, "transition": true, "units": "line widths", @@ -6489,15 +6654,20 @@ "android": "2.0.1", "ios": "2.0.0" }, - "data-driven styling": {} + "data-driven styling": { + "js": "5.8.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/744", + "android": "https://github.com/maplibre/maplibre-native/issues/744" + } }, "expression": { "interpolated": false, "parameters": [ - "zoom" + "zoom", + "feature" ] }, - "property-type": "cross-faded" + "property-type": "cross-faded-data-driven" }, "line-pattern": { "type": "resolvedImage", @@ -7579,9 +7749,36 @@ }, "property-type": "data-constant" }, + "resampling": { + "type": "enum", + "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter.\n![Visual comparison of linear resampling versus nearest resampling](assets/resampling.png)", + "values": { + "linear": { + "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" + }, + "nearest": { + "doc": "Nearest neighbor filtering interpolates pixel values using the nearest original source pixel creating a sharp but pixelated look when overscaled" + } + }, + "default": "linear", + "sdk-support": { + "basic functionality": { + "js": "5.20.0", + "android": "https://github.com/maplibre/maplibre-native/issues/4117", + "ios": "https://github.com/maplibre/maplibre-native/issues/4117" + } + }, + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + }, "raster-resampling": { "type": "enum", - "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter", + "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter. It is advised to use the generic `resampling` paint property instead.", "values": { "linear": { "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" @@ -7823,7 +8020,7 @@ } }, "default": "standard", - "doc": "The hillshade algorithm to use, one of `standard`, `basic`, `combined`, `igor`, or `multidirectional`. ![image](assets/hillshade_methods.png)", + "doc": "The hillshade algorithm to use, one of `standard`, `basic`, `combined`, `igor`, or `multidirectional`.\n![Visual comparison of standard, basic, igor, combined, and multidirectional hillshade-method](assets/hillshade_methods.png)", "sdk-support": { "basic functionality": { "js": "5.5.0", @@ -7838,6 +8035,33 @@ ] }, "property-type": "data-constant" + }, + "resampling": { + "type": "enum", + "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter.\n![Visual comparison of linear resampling versus nearest resampling](assets/resampling.png)", + "values": { + "linear": { + "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" + }, + "nearest": { + "doc": "Nearest neighbor filtering interpolates pixel values using the nearest original source pixel creating a sharp but pixelated look when overscaled" + } + }, + "default": "linear", + "sdk-support": { + "basic functionality": { + "js": "5.20.0", + "android": "https://github.com/maplibre/maplibre-native/issues/4117", + "ios": "https://github.com/maplibre/maplibre-native/issues/4117" + } + }, + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" } }, "paint_color-relief": { @@ -7889,6 +8113,33 @@ ] }, "property-type": "color-ramp" + }, + "resampling": { + "type": "enum", + "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter.\n![Visual comparison of linear resampling versus nearest resampling](assets/resampling.png)", + "values": { + "linear": { + "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" + }, + "nearest": { + "doc": "Nearest neighbor filtering interpolates pixel values using the nearest original source pixel creating a sharp but pixelated look when overscaled" + } + }, + "default": "linear", + "sdk-support": { + "basic functionality": { + "js": "5.20.0", + "android": "https://github.com/maplibre/maplibre-native/issues/4117", + "ios": "https://github.com/maplibre/maplibre-native/issues/4117" + } + }, + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" } }, "paint_background": { @@ -8007,5 +8258,150 @@ "type": "string", "doc": "A name of a feature property to use as ID for feature state." } + }, + "interpolation": { + "type": "array", + "value": "interpolation_name", + "minimum": 1, + "doc": "An interpolation defines how to transition between items. The first element of an interpolation array is a string naming the interpolation operator, e.g. `\"linear\"` or `\"exponential\"`. Elements that follow (if any) are the _arguments_ to the interpolation." + }, + "interpolation_name": { + "doc": "First element in an interpolation array. May be followed by a number of arguments.", + "type": "enum", + "values": { + "linear": { + "doc": "Interpolates linearly between the pair of stops just less than and just greater than the input", + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "interpolation" + } + ], + "parameters": [] + }, + "example": ["linear"], + "sdk-support": { + "interpolate": { + "js": "0.42.0", + "android": "6.0.0", + "ios": "4.0.0" + }, + "interpolate-hcl": { + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + }, + "interpolate-lab":{ + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + } + } + }, + "exponential": { + "doc": "Interpolates exponentially between the stops just less than and just greater than the input.", + "syntax": { + "overloads": [ + { + "parameters": [ + "base" + ], + "output-type": "interpolation" + } + ], + "parameters": [ + { + "name": "base", + "type": "number literal", + "doc": "rate at which the output increases in `f(x) = x^r`. Values higher than 1 increase, close to one behaves linearly, and below one decrease." + } + ] + }, + "example": [ + "exponential", + 2 + ], + "sdk-support": { + "interpolate": { + "js": "0.42.0", + "android": "6.0.0", + "ios": "4.0.0" + }, + "interpolate-hcl": { + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + }, + "interpolate-lab":{ + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + } + } + }, + "cubic-bezier": { + "doc": "Interpolates using the cubic bézier curve defined by the given control points.", + "syntax": { + "overloads": [ + { + "parameters": [ + "x1", + "y1", + "x2", + "y2" + ], + "output-type": "interpolation" + } + ], + "parameters": [ + { + "name": "x1", + "type": "number literal", + "doc": "X-coordinate of the first control point. Must be between zero and one for a valid monotonic easing curve. Controls how quickly the curve speeds up at the beginning." + }, + { + "name": "y1", + "type": "number literal", + "doc": "Y-coordinate of the first control point. Typically between zero and one, but may exceed this range for overshoot effects. Influences the starting slope of the curve." + }, + { + "name": "x2", + "type": "number literal", + "doc": "X-coordinate of the second control point. Must be between zero and one for a valid monotonic easing curve. Controls when the curve begins to decelerate toward the end." + }, + { + "name": "y2", + "type": "number literal", + "doc": "Y-coordinate of the second control point. Typically between zero and one, but may exceed this range for overshoot effects. Influences the ending slope of the curve." + } + ] + }, + "example": [ + "cubic-bezier", + 2, + 3, + 2, + 3 + ], + "sdk-support": { + "interpolate": { + "js": "0.42.0", + "android": "6.0.0", + "ios": "4.0.0" + }, + "interpolate-hcl": { + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + }, + "interpolate-lab":{ + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + } + } + } + } } } From 56fe296b251c16601cd0ab5a83846caf45cd394d Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Tue, 25 Aug 2026 08:52:28 +0900 Subject: [PATCH 18/32] chore: port integration test from gl-js --- .../global-state/basic/expected.png | Bin 0 -> 607 bytes .../global-state/basic/style.json | 48 +++++++++++ .../global-state/default/expected.png | Bin 0 -> 517 bytes .../global-state/default/style.json | 45 ++++++++++ .../global-state/set-property/expected.png | Bin 0 -> 603 bytes .../global-state/set-property/style.json | 61 +++++++++++++ .../with-feature-state/basic/expected.png | Bin 0 -> 604 bytes .../with-feature-state/basic/style.json | 73 ++++++++++++++++ .../set-and-remove/expected.png | Bin 0 -> 603 bytes .../set-and-remove/style.json | 80 ++++++++++++++++++ .../global-state/with-get/expected.png | Bin 0 -> 604 bytes .../global-state/with-get/style.json | 63 ++++++++++++++ render-test/parser.cpp | 50 +++++++++++ 13 files changed, 420 insertions(+) create mode 100644 metrics/integration/render-tests/global-state/basic/expected.png create mode 100644 metrics/integration/render-tests/global-state/basic/style.json create mode 100644 metrics/integration/render-tests/global-state/default/expected.png create mode 100644 metrics/integration/render-tests/global-state/default/style.json create mode 100644 metrics/integration/render-tests/global-state/set-property/expected.png create mode 100644 metrics/integration/render-tests/global-state/set-property/style.json create mode 100644 metrics/integration/render-tests/global-state/with-feature-state/basic/expected.png create mode 100644 metrics/integration/render-tests/global-state/with-feature-state/basic/style.json create mode 100644 metrics/integration/render-tests/global-state/with-feature-state/set-and-remove/expected.png create mode 100644 metrics/integration/render-tests/global-state/with-feature-state/set-and-remove/style.json create mode 100644 metrics/integration/render-tests/global-state/with-get/expected.png create mode 100644 metrics/integration/render-tests/global-state/with-get/style.json diff --git a/metrics/integration/render-tests/global-state/basic/expected.png b/metrics/integration/render-tests/global-state/basic/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..88cc00c530cd21b0158687ec7a3a233e68743851 GIT binary patch literal 607 zcmV-l0-*hgP)vvCWH4Y**i(1)-SL0lwF5{RXR#d@!W z5X<-gA{f*nuz3VQt5{eFNwGq(iWVXWF4Oqs1s3i}N-3q3Qc5YMlv2JR zVFf8SaULh|5ijs8#DUKs+gko`%@Bq_BjPySRaw!#&5<5HC$2wg^#pN#0VxsRFU4JnUll=2t` z%bVf82AvTpH*m>{Qr^)hvTw zE*Q}nchYo@8+Y7vP8!kq!ga>IGMzWZy)m6vMs&vQnZ`WC2O~m!z}z(UjOdJeYyv4C zXq57SX*@QfGa|$R9$Ha|>l%f)Zgmes92n7QvxB+i-3;+gqY&?K)AHun(O^x3!SJcq z@8M^Q`!mE(7Bm>1^!ja_vbgOK|7ozM(O`Jn>wm`;tZQ>O#EvBmhDW{rTb$MAJ-!R^ zk49k$DR*%LGlzSQt07*RK*}xL!x@L0<93K!78F*Hauerq0w3`L&q5sd3{r0398Tj1 tUgK$qeWjFAN-3q3Qc5YMlv2v5{{d~ukwT-7%IE+9002ovPDHLkV1fq$7?=P6 literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/global-state/basic/style.json b/metrics/integration/render-tests/global-state/basic/style.json new file mode 100644 index 000000000000..e77d93974f99 --- /dev/null +++ b/metrics/integration/render-tests/global-state/basic/style.json @@ -0,0 +1,48 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "state": { + "color": { "default": "magenta" } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "id": 0, + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-10, 0] }, + "properties": {} + }, + { + "id": 1, + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [10, 0] }, + "properties": {} + } + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 10, + "circle-color": [ + "global-state", + "color" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/global-state/default/expected.png b/metrics/integration/render-tests/global-state/default/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..3abac7f9279aacdf441001ab8d77fc2a166a678e GIT binary patch literal 517 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Or9=|Ar*{or0Om)Ffi@|@!lTX z=ruJ_g5kmQKVt5zia8m@7Y`nEI9PC@yFl1cBy**LJ3G58yW;UZ41zqR9B+hJ<`xwQ zWCd_6FA;DIIG1$yK;wJs|3Bx}%y;KdY!PsR5$Q{o*)q>?kbhU$m26WX6)SPvU{_(M z@*M4z%jTr&EL^u!_{?J;)3yz*foDxO^qK0e$~?-w6#5utIiw~a(D?J`(fEBz@qV3VsUeW;vLU-Xgyl0nUeQV?wrLPneDE3HZ9oDamM#gYyOs< z+K*<1ru=J*-%{EC-t|rPmSv&O#Qq)IRhIts@3zE>6v>&@I~LuwTKDp|JHEtDHXA$6#Qw>!w14qCp7*f2vQ_T8Gs#h@a)%QtjqhAvV60Zm zJbjJU=f1F+W1M^;mC~(~;D*N_w-8xq^5zj`=(bOg8FIN|O@rdC_B?X?48U zI?qbI_eIS^tH*IO&q+VNH{+Z(Q0X_HWW7hthJx}2=DoH+zT9l5XUuDjj7F|wbFFw6n^U>Te!S@B1KRvr~F~PSx@yC+)-#=_V`d9Oj zdbd=Y`mZ|a*CbXfdB6W?WbVDk)`I^tM5ayL z*)2W8@=kG#=*LSUM%xSc(`EAdZY$+Q21K|%Q#<#V#qJU7yiSV`mMM7;Wc(g;#64cw jQ45PIaHO4D@t@K6;6x`?0mq5J)WYED>gTe~DWM4fSfCX8 literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/global-state/set-property/style.json b/metrics/integration/render-tests/global-state/set-property/style.json new file mode 100644 index 000000000000..a926cb446274 --- /dev/null +++ b/metrics/integration/render-tests/global-state/set-property/style.json @@ -0,0 +1,61 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64, + "operations": [ + [ + "wait" + ], + [ + "setGlobalStateProperty", + "color", + "cyan" + ], + [ + "wait" + ] + ] + } + }, + "state": { + "color": { "default": "magenta" } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "id": 0, + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-10, 0] }, + "properties": {} + }, + { + "id": 1, + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [10, 0] }, + "properties": {} + } + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 10, + "circle-color": [ + "global-state", + "color" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/global-state/with-feature-state/basic/expected.png b/metrics/integration/render-tests/global-state/with-feature-state/basic/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..43b8af6ea3c8b1ab7abc62490ef623b92a2851f8 GIT binary patch literal 604 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Or9=|Ar*{or0OmKIieum+k+ds zLeeD|E_}Z0J0sSSW7&qyh1}h|I}$crXx||07^C7V)UmYnXw~{wfzv-2lq9YwZKz~% zTpba?;knUiLzkEmhu-BMPg%v@*&AO!^YP)_}Wp#BG)BaP=3 zi2UHjJB$FuhRKWwn)YJzX` zl=#Q-HK~Qmt7`e$)Ws(5?3UMk&R+X}b>fc{m*r<`&*uIJu{a^lWw^$7f8udr=y@!N? z-aTSVV3VsUeW;vLU-Xgyl0nUeQV?wrLPneDE3HZ9oDamM#gYyOs< z+K*<1ru=J*-%{EC-t|rPmSv&O#Qq)IRhIts@3zE>6v>&@I~LuwTKDp|JHEtDHXA$6#Qw>!w14qCp7*f2vQ_T8Gs#h@a)%QtjqhAvV60Zm zJbjJU=f1F+W1M^;mC~(~;D*N_w-8xq^5zj`=(bOg8FIN|O@rdC_B?X?48U zI?qbI_eIS^tH*IO&q+VNH{+Z(Q0X_HWW7hthJx}2=DoH+zT9l5XUuDjj7F|wbFFw6n^U>Te!S@B1KRvr~F~PSx@yC+)-#=_V`d9Oj zdbd=Y`mZ|a*CbXfdB6W?WbVDk)`I^tM5ayL z*)2W8@=kG#=*LSUM%xSc(`EAdZY$+Q21K|%Q#<#V#qJU7yiSV`mMM7;Wc(g;#64cw jQ45PIaHO4D@t@K6;6x`?0mq5J)WYED>gTe~DWM4fSfCX8 literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/global-state/with-feature-state/set-and-remove/style.json b/metrics/integration/render-tests/global-state/with-feature-state/set-and-remove/style.json new file mode 100644 index 000000000000..14e07c0672b7 --- /dev/null +++ b/metrics/integration/render-tests/global-state/with-feature-state/set-and-remove/style.json @@ -0,0 +1,80 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64, + "operations": [ + [ + "setGlobalStateProperty", + "color", + "cyan" + ], + [ + "setFeatureState", + { + "source": "geojson", + "id": 0 + }, + { + "color": "magenta" + } + ], + [ + "removeFeatureState", + { + "source": "geojson", + "id": 0 + } + ], + [ + "wait" + ] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "id": 0, + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [-10, 0] }, + "properties": {} + }, + { + "id": 1, + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [10, 0] }, + "properties": {} + } + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 10, + "circle-color": [ + "coalesce", + [ + "feature-state", + "color" + ], + [ + "global-state", + "color" + ], + "yellow" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/global-state/with-get/expected.png b/metrics/integration/render-tests/global-state/with-get/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..43b8af6ea3c8b1ab7abc62490ef623b92a2851f8 GIT binary patch literal 604 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Or9=|Ar*{or0OmKIieum+k+ds zLeeD|E_}Z0J0sSSW7&qyh1}h|I}$crXx||07^C7V)UmYnXw~{wfzv-2lq9YwZKz~% zTpba?;knUiLzkEmhu-BMPg%v@*&AO!^YP)_}Wp#BG)BaP=3 zi2UHjJB$FuhRKWwn)YJzX` zl=#Q-HK~Qmt7`e$)Ws(5?3UMk&R+X}b>fc{m*r<`&*uIJu{a^lWw^$7f8udr=y@!N? z-aTSVsetProperty(propertyName, propertyValue); return true; }); + } else if (operationArray[0].GetString() == setGlobalStatePropertyOp) { + // setGlobalStateProperty + assert(operationArray.Size() >= 3u); + assert(operationArray[1].IsString()); + + std::string propertyName{operationArray[1].GetString(), operationArray[1].GetStringLength()}; + auto value = parseGlobalStateValue(operationArray[2]); + result.emplace_back([propertyName, value = std::move(value)](TestContext& ctx) { + ctx.getMap().getStyle().setGlobalStateProperty(propertyName, value); + return true; + }); } else if (operationArray[0].GetString() == fileSizeProbeOp) { // probeFileSize assert(operationArray.Size() >= 4u); From 0bfffc54a8363e12a0d8023bc3c6bd88680b93d6 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Tue, 25 Aug 2026 08:54:30 +0900 Subject: [PATCH 19/32] fix(core): use property defaults when expression evaluation fails 2. --- src/mln/renderer/data_driven_property_evaluator.hpp | 6 ++++-- src/mln/renderer/property_evaluator.hpp | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/mln/renderer/data_driven_property_evaluator.hpp b/src/mln/renderer/data_driven_property_evaluator.hpp index a42c621c1265..5f9db6dc0fda 100644 --- a/src/mln/renderer/data_driven_property_evaluator.hpp +++ b/src/mln/renderer/data_driven_property_evaluator.hpp @@ -38,7 +38,8 @@ class DataDrivenPropertyEvaluator { return ResultType(returnExpression); } return ResultType(expression.evaluate(style::expression::EvaluationContext(std::floor(parameters.z)) - .withGlobalState(parameters.globalState.get()))); + .withGlobalState(parameters.globalState.get()), + defaultValue)); } else { if (needsFeature) { auto returnExpression = expression; @@ -46,7 +47,8 @@ class DataDrivenPropertyEvaluator { return ResultType(returnExpression); } return ResultType(expression.evaluate( - style::expression::EvaluationContext(parameters.z).withGlobalState(parameters.globalState.get()))); + style::expression::EvaluationContext(parameters.z).withGlobalState(parameters.globalState.get()), + defaultValue)); } } diff --git a/src/mln/renderer/property_evaluator.hpp b/src/mln/renderer/property_evaluator.hpp index 1182f10bbbbf..38249ccd3a95 100644 --- a/src/mln/renderer/property_evaluator.hpp +++ b/src/mln/renderer/property_evaluator.hpp @@ -19,7 +19,8 @@ class PropertyEvaluator { T operator()(const T& constant) const { return constant; } T operator()(const style::PropertyExpression& fn) const { return fn.evaluate( - style::expression::EvaluationContext(parameters.z).withGlobalState(parameters.globalState.get())); + style::expression::EvaluationContext(parameters.z).withGlobalState(parameters.globalState.get()), + defaultValue); } private: From 45aedbef5a92129b5c920c5c20f03c94624a4813 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Tue, 25 Aug 2026 19:24:32 +0900 Subject: [PATCH 20/32] Revert "update: v8.json from @maplibre/maplibre-gl-style-spec" This reverts commit 04556ef49c80ffcd34cddf8e78bb9a12951ceb39. --- scripts/style-spec-reference/v8.json | 742 +++++++-------------------- 1 file changed, 173 insertions(+), 569 deletions(-) diff --git a/scripts/style-spec-reference/v8.json b/scripts/style-spec-reference/v8.json index 36954550ac94..c94ce02c72e8 100644 --- a/scripts/style-spec-reference/v8.json +++ b/scripts/style-spec-reference/v8.json @@ -28,7 +28,6 @@ "center": { "type": "array", "value": "number", - "length": 2, "doc": "Default map center in longitude and latitude. The style center will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", "example": [ -73.9749, @@ -98,6 +97,15 @@ } } }, + "light": { + "type": "light", + "doc": "The global light source.", + "example": { + "anchor": "viewport", + "color": "white", + "intensity": 0.4 + } + }, "state": { "type": "state", "default": {}, @@ -118,15 +126,6 @@ } } }, - "light": { - "type": "light", - "doc": "The global light source.", - "example": { - "anchor": "viewport", - "color": "white", - "intensity": 0.4 - } - }, "sky": { "type": "sky", "doc": "The map's sky configuration. **Note:** this definition is still experimental and is under development in maplibre-gl-js.", @@ -213,14 +212,15 @@ "ios": "0.1.0" }, "omit to use local fonts": { - "js": "5.11.0", + "js": "https://github.com/maplibre/maplibre-gl-js/issues/3302", "android": "https://github.com/maplibre/maplibre-native/issues/165", "ios": "https://github.com/maplibre/maplibre-native/issues/165" } } }, "font-faces": { - "type": "fontFaces", + "type": "array", + "value": "fontFaces", "doc": "The `font-faces` property can be used to specify what font files to use for rendering text. Font faces contain information needed to render complex texts such as [Devanagari](https://en.wikipedia.org/wiki/Devanagari), [Khmer](https://en.wikipedia.org/wiki/Khmer_script) among many others.

Unicode range

The optional `unicode-range` property can be used to only use a particular font file for characters within the specified unicode range(s). Its value should be an array of strings, each indicating a start and end of a unicode range, similar to the [CSS descriptor with the same name](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range). This allows specifying multiple non-consecutive unicode ranges. When not specified, the default value is `U+0-10FFFF`, meaning the font file will be used for all unicode characters.\n\nRefer to the [Unicode Character Code Charts](https://www.unicode.org/charts/) to see ranges for scripts supported by Unicode. To see what unicode code-points are available in a font, use a tool like [FontDrop](https://fontdrop.info/).\n\n

Font Resolution

For every name in a symbol layer’s [`text-font`](./layers.md/#text-font) array, characters are matched if they are covered one of the by the font files in the corresponding entry of the `font-faces` map. Any still-unmatched characters then fall back to the [`glyphs`](./glyphs.md) URL if provided.\n\n

Supported Fonts

What type of fonts are supported is implementation-defined. Unsupported fonts are ignored.", "example": { "Noto Sans Regular": [{ @@ -243,7 +243,7 @@ }, "sdk-support": { "basic functionality": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6637", + "js": "https://github.com/maplibre/maplibre-gl-js/issues/50", "android": "11.13.0", "ios": "6.18.0" } @@ -383,9 +383,9 @@ "js": "supported" }, "mlt": { - "android": "12.1.0", - "ios": "6.20.0", - "js": "5.12.0" + "android": "https://github.com/maplibre/maplibre-native/issues/3721", + "ios": "https://github.com/maplibre/maplibre-native/issues/3721", + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6258" } } }, @@ -654,7 +654,7 @@ "doc": "Size of the tile buffer on each side. A value of 0 produces no buffer. A value of 512 produces a buffer as wide as the tile itself. Larger values produce fewer rendering artifacts near tile edges and slower performance." }, "filter": { - "type": "filter", + "type": "*", "doc": "An expression for filtering features prior to processing them for rendering." }, "tolerance": { @@ -948,8 +948,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -1006,8 +1006,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -1064,8 +1064,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -1097,11 +1097,6 @@ "js": "0.41.0", "android": "6.0.0", "ios": "4.0.0" - }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", - "android": "https://github.com/maplibre/maplibre-native/issues/3302", - "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -1114,6 +1109,20 @@ } }, "layout_fill-extrusion": { + "fill-extrusion-rounded-corner-distance": { + "type": "number", + "default": 0, + "minimum": 0, + "units": "meters", + "doc": "The distance from the corner that will be cut and replaced with rounded corner.", + "sdk-support": { + "basic functionality": { + "android": "13.0.0", + "ios": "6.0.0" + } + }, + "property-type": "constant" + }, "visibility": { "type": "enum", "values": { @@ -1131,11 +1140,6 @@ "js": "0.27.0", "android": "5.1.0", "ios": "3.6.0" - }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", - "android": "https://github.com/maplibre/maplibre-native/issues/3302", - "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -1304,8 +1308,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -2331,7 +2335,6 @@ "zoom" ] }, - "example": ["center", "left", "right"], "property-type": "data-constant" }, "text-variable-anchor-offset": { @@ -2364,7 +2367,6 @@ "feature" ] }, - "example": ["top", [0, 4], "left", [3, 0], "bottom", [1, 1]], "property-type": "data-driven" }, "text-anchor": { @@ -2772,8 +2774,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -2806,8 +2808,8 @@ "android": "2.0.1", "ios": "2.0.0" }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", + "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } @@ -2839,11 +2841,6 @@ "js": "0.43.0", "android": "6.0.0", "ios": "4.0.0" - }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", - "android": "https://github.com/maplibre/maplibre-native/issues/3302", - "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -2870,14 +2867,6 @@ "doc": "Whether this layer is displayed.", "sdk-support": { "basic functionality": { - "js": "5.6.0", - "android": "https://github.com/maplibre/maplibre-native/issues/3408", - "ios": "https://github.com/maplibre/maplibre-native/issues/3408" - }, - "`global state` expression": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6495", - "android": "https://github.com/maplibre/maplibre-native/issues/3302", - "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -2890,16 +2879,9 @@ } }, "filter": { - "type": "boolean", - "doc": "A filter selects specific features from a layer.", - "expression": { - "interpolated": false, - "parameters": [ - "zoom", - "feature" - ] - }, - "property-type": "data-driven" + "type": "array", + "value": "*", + "doc": "A filter selects specific features from a layer." }, "filter_operator": { "type": "enum", @@ -3036,12 +3018,12 @@ }, "expression": { "type": "array", - "value": "expression_name", + "value": "*", "minimum": 1, - "doc": "An expression defines a function that can be used for data-driven style properties or feature filters. The first element of an expression array is a string naming the expression operator, e.g. `\"*\"` or `\"case\"`. Elements that follow (if any) are the _arguments_ to the expression. Each argument is either a literal value (a string, number, boolean, or `null`), or another expression array." + "doc": "An expression defines a function that can be used for data-driven style properties or feature filters." }, "expression_name": { - "doc": "First element in an expression array. May be followed by a number of arguments.", + "doc": "", "type": "enum", "values": { "let": { @@ -3049,25 +3031,25 @@ "syntax": { "overloads": [ { - "parameters": ["var_name_1", "var_value_1", "...", "var_name_n", "var_value_n", "expression"], + "parameters": ["var_1_name", "var_1_value", "...", "var_n_name", "var_n_value", "expression"], "output-type": "any" } ], "parameters": [ { - "name": "var_name_i", + "name": "var_i_name", "type": "string literal", - "doc": "The name of the i-th variable." + "description": "The name of the i-th variable." }, { - "name": "var_value_i", + "name": "var_i_value", "type": "any", - "doc": "The value of the i-th variable." + "description": "The value of the i-th variable." }, { "name": "expression", "type": "any", - "doc": "The expression within which the named variables can be referenced." + "description": "The expression within which the named variables can be referenced." } ] }, @@ -3094,7 +3076,7 @@ { "name": "var_name", "type": "string literal", - "doc": "The name of the variable bound using `let`." + "description": "The name of the variable bound using `let`." } ] }, @@ -3166,13 +3148,13 @@ }, { "name": "type", - "type": ["string", "number", "boolean"], - "doc": "The asserted type of the input array." + "type": "\"string\" | \"number\" | \"boolean\"", + "description": "The asserted type of the input array." }, { "name": "length", "type": "number literal", - "doc": "The asserted length of the input array." + "description": "The asserted length of the input array." } ] }, @@ -3199,12 +3181,12 @@ { "name": "index", "type": "number", - "doc": "The index into `array`." + "description": "The index into `array`." }, { "name": "array", "type": "array", - "doc": "The array of items to retrieve the specified item from." + "description": "The array of items to retrieve the specified item from." } ] }, @@ -3235,22 +3217,22 @@ { "name": "item", "type": "T", - "doc": "The needle to search for within `array`." + "description": "The needle to search for within `array`." }, { "name": "array", "type": "array", - "doc": "The haystack through which to search for `item`." + "description": "The haystack through which to search for `item`." }, { "name": "substring", "type": "string", - "doc": "The needle to search for within `string`." + "description": "The needle to search for within `string`." }, { "name": "string", "type": "string", - "doc": "The haystack through which to search for `substring`." + "description": "The haystack through which to search for `substring`." } ] }, @@ -3281,27 +3263,27 @@ { "name": "item", "type": "T", - "doc": "The needle to search for within `array`." + "description": "The needle to search for within `array`." }, { "name": "array", "type": "array", - "doc": "The haystack through which to search for `item`." + "description": "The haystack through which to search for `item`." }, { "name": "substring", "type": "string", - "doc": "The needle to search for within `string`." + "description": "The needle to search for within `string`." }, { "name": "string", "type": "string", - "doc": "The haystack through which to search for `substring`." + "description": "The haystack through which to search for `substring`." }, { "name": "from_index", "type": "number", - "doc": "The index from where to begin the search." + "description": "The index from where to begin the search." } ] }, @@ -3332,22 +3314,22 @@ { "name": "array", "type": "array", - "doc": "The original array from which to extract the subarray." + "description": "The original array from which to extract the subarray." }, { "name": "string", "type": "string", - "doc": "The original string from which to extract the substring." + "description": "The original string from which to extract the substring." }, { "name": "start_index", "type": "number", - "doc": "The inclusive index from which `slice` extracts items or characters from the subarray or substring." + "description": "The inclusive index from which `slice` extracts items or characters from the subarray or substring." }, { "name": "end_index", "type": "number", - "doc": "The non-inclusive index up to which `slice` extracts items or characters from the subarray or substring." + "description": "The non-inclusive index up to which `slice` extracts items or characters from the subarray or substring." } ] }, @@ -3382,7 +3364,7 @@ { "name": "fallback", "type": "any", - "doc": "The result when no condition evaluates to true." + "description": "The result when no condition evaluates to true." } ] }, @@ -3408,23 +3390,23 @@ "parameters": [ { "name": "input", - "type": ["string", "number"], - "doc": "Any expression." + "type": "string | number", + "description": "Any expression." }, { "name": "label_i", - "type": ["string literal", "number literal", "array", "array"], - "doc": "The i-th literal value or array of literal values to match the input against." + "type": "string literal | number literal | array | array", + "description": "The i-th literal value or array of literal values to match the input against." }, { "name": "output_i", "type": "any", - "doc": "The result when the i-th label is the first label to match the input." + "description": "The result when the i-th label is the first label to match the input." }, { "name": "fallback", "type": "any", - "doc": "The result when no label matches the input." + "description": "The result when no label matches the input." } ] }, @@ -3469,7 +3451,7 @@ "syntax": { "overloads": [ { - "parameters": ["input", "output_0", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], + "parameters": ["input", "output_0", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], "output-type": "any" } ], @@ -3477,22 +3459,22 @@ { "name": "input", "type": "number", - "doc": "Any numeric expression." + "description": "Any numeric expression." }, { "name": "output_0", "type": "any", - "doc": "The result when the `input` is less than the first stop." + "description": "The result when the `input` is less than the first stop." }, { - "name": "stop_input_i", + "name": "stop_i_input", "type": "number literal", - "doc": "The value of the i-th stop against which the `input` is compared." + "description": "The value of the i-th stop against which the `input` is compared." }, { - "name": "stop_output_i", + "name": "stop_i_output", "type": "any", - "doc": "The result when the i-th stop is the last stop less than the `input`." + "description": "The result when the i-th stop is the last stop less than the `input`." } ] }, @@ -3507,34 +3489,34 @@ } }, "interpolate": { - "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, `color`, `array`, or `projection`.\n\n - [Animate map camera around a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-camera-around-point/)\n\n - [Change building color based on zoom level](https://maplibre.org/maplibre-gl-js/docs/examples/change-building-color-based-on-zoom-level/)\n\n - [Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/examples/heatmap-layer/)\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", + "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, `color`, `array`, or `projection`.\n\nInterpolation types:\n\n- `[\"linear\"]`, or an expression returning one of those types: Interpolates linearly between the pair of stops just less than and just greater than the input.\n\n- `[\"exponential\", base]`: Interpolates exponentially between the stops just less than and just greater than the input. `base` controls the rate at which the output increases: higher values make the output increase more towards the high end of the range. With values close to 1 the output increases linearly.\n\n- `[\"cubic-bezier\", x1, y1, x2, y2]`: Interpolates using the cubic bezier curve defined by the given control points.\n\n - [Animate map camera around a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-camera-around-point/)\n\n - [Change building color based on zoom level](https://maplibre.org/maplibre-gl-js/docs/examples/change-building-color-based-on-zoom-level/)\n\n - [Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/examples/heatmap-layer/)\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], - "output-type": ["number", "array", "color", "array", "projection"] + "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "output-type": "number | array | color | array | projection" } ], "parameters": [ { "name": "interpolation_type", - "type": "interpolation", - "doc": "The interpolation type." + "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]", + "description": "The interpolation type." }, { "name": "input", "type": "number", - "doc": "Any numeric expression." + "description": "Any numeric expression." }, { - "name": "stop_input_i", + "name": "stop_i_input", "type": "number literal", - "doc": "The value of the i-th stop against which the `input` is compared." + "description": "The value of the i-th stop against which the `input` is compared." }, { - "name": "stop_output_i", - "type": ["number", "array", "color", "array", "projection"], - "doc": "The output value corresponding to the i-th stop." + "name": "stop_i_output", + "type": "number | array | color | array | projection", + "description": "The output value corresponding to the i-th stop." } ] }, @@ -3553,27 +3535,26 @@ "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], - "output-type": ["color", "array"] + "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "output-type": "color | array" } ], "parameters": [ { "name": "interpolation_type", - "type": "interpolation", - "doc": "The interpolation type." + "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]" }, { "name": "input", "type": "number" }, { - "name": "stop_input_i", + "name": "stop_i_input", "type": "number literal" }, { - "name": "stop_output_i", - "type": ["color", "array"] + "name": "stop_i_output", + "type": "color | array" } ] }, @@ -3592,27 +3573,26 @@ "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], - "output-type": ["color", "array"] + "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "output-type": "color | array" } ], "parameters": [ { "name": "interpolation_type", - "type": "interpolation", - "doc": "The interpolation type." + "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]" }, { "name": "input", "type": "number" }, { - "name": "stop_input_i", + "name": "stop_i_input", "type": "number literal" }, { - "name": "stop_output_i", - "type": ["color", "array"] + "name": "stop_i_output", + "type": "color | array" } ] }, @@ -3817,7 +3797,7 @@ } }, "collator": { - "doc": "Returns a `collator` for use in locale-dependent comparison operations. Use `resolved-locale` to test the results of locale fallback behavior.", + "doc": "Returns a `collator` for use in locale-dependent comparison operations. The `case-sensitive` and `diacritic-sensitive` options default to `false`. The `locale` argument specifies the IETF language tag of the locale to use. If none is provided, the default locale is used. If the requested locale is not available, the `collator` will use a system-defined fallback locale. Use `resolved-locale` to test the results of locale fallback behavior.", "syntax": { "overloads": [ { @@ -3828,25 +3808,7 @@ "parameters": [ { "name": "options", - "type": { - "case-sensitive": { - "type": "boolean", - "default": false, - "example": true, - "doc": "If characters of different case-ness are considered different" - }, - "diacritic-sensitive": { - "type": "boolean", - "default": false, - "example": true, - "doc": "If characters with different diacritics are considered different" - }, - "locale": { - "type": "string", - "example": "en", - "doc": "IETF language tag of the locale to use. If none is provided, the default locale is used. If the requested locale is not available, the `collator` will use a system-defined fallback locale." - } - } + "type": "{ \"case-sensitive\"?: boolean, \"diacritic-sensitive\"?: boolean, \"locale\"?: string }" } ] }, @@ -3861,7 +3823,7 @@ } }, "format": { - "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", + "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n- `\"vertical-align\"`: Aligns vertically text section or image in relation to the row it belongs to. Possible values are: \n\t- `\"bottom\"` *default*: align the bottom of this section with the bottom of other sections.\n\"Visual\n\t- `\"center\"`: align the center of this section with the center of other sections.\n\"Visual\n\t- `\"top\"`: align the top of this section with the top of other sections.\n\"Visual\n\t- Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", "syntax": { "overloads": [ { @@ -3872,44 +3834,11 @@ "parameters": [ { "name": "input_i", - "type": ["string", "image"] + "type": "string | image" }, { "name": "style_overrides_i", - "type": { - "text-font": { - "type": "string", - "doc": "Overrides the font stack specified by the root layout property.", - "example": "Arial Unicode MS Regular" - }, - "text-color": { - "type": "color", - "doc": "Overrides the color specified by the root paint property.", - "example": "#333" - }, - "font-scale": { - "type": "number", - "doc":"Applies a scaling factor on `text-size` as specified by the root layout property.", - "example": 1.2 - }, - "vertical-align": { - "type": "enum", - "doc": "Aligns a vertical text section or image in relation to the row it belongs to. Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.", - "default": "bottom", - "example": "bottom", - "values": { - "bottom": { - "doc": "align the bottom of this section with the bottom of other sections.\n![Visual representation of bottom alignment](https://github.com/user-attachments/assets/0474a2fd-a4b2-417c-9187-7a13a28695bc)" - }, - "center": { - "doc": "align the center of this section with the center of other sections.\n![Visual representation of center alignment](https://github.com/user-attachments/assets/92237455-be6d-4c5d-b8f6-8127effc1950)" - }, - "top": { - "doc": "align the top of this section with the top of other sections.\n![Visual representation of top alignment](https://github.com/user-attachments/assets/45dccb28-d977-4abb-a006-4ea9792b7c53)" - } - } - } - } + "type": "{ \"text-font\"?: string, \"text-color\"?: color, \"font-scale\"?: number, \"vertical-align\"?: \"bottom\" | \"center\" | \"top\" }" } ] }, @@ -3974,35 +3903,8 @@ } } }, - "global-state": { - "doc": "Retrieves a property value from global state that can be set with platform-specific APIs. Defaults can be provided using the [`state`](https://maplibre.org/maplibre-style-spec/root/#state) root property. Returns `null` if no value nor default value is set for the retrieved property.", - "group": "Lookup", - "syntax": { - "overloads": [ - { - "parameters": ["property_name"], - "output-type": "any" - } - ], - "parameters": [ - { - "name": "property_name", - "type": "string literal", - "doc": "The name of the global state property to retrieve." - } - ] - }, - "example": ["global-state", "someProperty"], - "sdk-support": { - "basic functionality": { - "js": "5.6.0", - "android": "https://github.com/maplibre/maplibre-native/issues/3302", - "ios": "https://github.com/maplibre/maplibre-native/issues/3302" - } - } - }, "number-format": { - "doc": "Converts the input number into a string representation using the provided format_options.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "doc": "Converts the input number into a string representation using the providing formatting rules. If set, the `locale` argument specifies the locale to use, as a BCP 47 language tag. If set, the `currency` argument specifies an ISO 4217 code to use for currency-style formatting. If set, the `min-fraction-digits` and `max-fraction-digits` arguments specify the minimum and maximum number of fractional digits to include.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", "syntax": { "overloads": [ { @@ -4013,39 +3915,11 @@ "parameters": [ { "name": "input", - "type": "number", - "doc": "number to format" + "type": "number" }, { "name": "format_options", - "type": { - "locale": { - "type": "string", - "example": "en", - "doc": "Specifies the locale to use, as a BCP 47 language tag" - }, - "currency": { - "type": "string", - "example": "USD", - "doc": "An ISO 4217 code to use for currency-style formatting" - }, - "unit": { - "type": "string", - "example": "meter", - "doc": "A CLDR or ECMA-402 unit to use for quantity-style formatting" - }, - "min-fraction-digits": { - "type": "number", - "example": 1, - "doc": "Minimum number of fractional digits to include" - }, - "max-fraction-digits": { - "type": "number", - "example": 2, - "doc": "Maximum number of fractional digits to include" - } - }, - "doc": "Format options for the number" + "type": "{ \"locale\"?: string, \"currency\"?: string, \"min-fraction-digits\"?: number, \"max-fraction-digits\"?: number }" } ] }, @@ -4056,11 +3930,6 @@ "js": "0.54.0", "android": "8.4.0", "ios": "supported" - }, - "`unit` property": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/7024", - "android": "https://github.com/maplibre/maplibre-native/issues/4071", - "ios": "https://github.com/maplibre/maplibre-native/issues/4071" } } }, @@ -4229,7 +4098,7 @@ } }, "rgba": { - "doc": "Creates a color value from red, green, blue components, which must range between 0 and 255, and an alpha component which must range between zero and one. If any component is out of range, the expression is an error.", + "doc": "Creates a color value from red, green, blue components, which must range between 0 and 255, and an alpha component which must range between 0 and 1. If any component is out of range, the expression is an error.", "syntax": { "overloads": [ { @@ -4279,12 +4148,12 @@ { "name": "property_name", "type": "string", - "doc": "The name of the property to retrieve the value of." + "description": "The name of the property to retrieve the value of." }, { "name": "object", "type": "object", - "doc": "The object to retrieve the value from." + "description": "The object to retrieve the value from." } ] }, @@ -4311,12 +4180,12 @@ { "name": "property_name", "type": "string", - "doc": "The name of the property to test for the presence of." + "description": "The name of the property to test for the presence of." }, { "name": "object", "type": "object", - "doc": "The object in which to test for the presence of the `property_name` property." + "description": "The object in which to test for the presence of the `property_name` property." } ] }, @@ -4342,7 +4211,7 @@ "parameters": [ { "name": "array_or_string", - "type": ["array", "string"] + "type": "array | string" } ] }, @@ -4402,6 +4271,32 @@ } } }, + "global-state": { + "doc": "Retrieves a property value from the map's global state. Returns null if the requested property is not present in the global state. The global state can be set programmatically and defaults can be defined using the root-level `state` style property.", + "syntax": { + "overloads": [ + { + "parameters": ["property_name"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "property_name", + "type": "string" + } + ] + }, + "example": ["global-state", "chargerType"], + "group": "Feature data", + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" + } + } + }, "geometry-type": { "doc": "Returns the feature's simple geometry type: `Point`, `LineString`, or `Polygon`. `MultiPoint`, `MultiLineString`, and `MultiPolygon` are returned as `Point`, `LineString`, and `Polygon`, respectively.", "syntax": { @@ -4611,17 +4506,17 @@ { "name": "input_1", "type": "number", - "doc": "The number from which to subtract `input_2`." + "description": "The number from which to subtract `input_2`." }, { "name": "input_2", "type": "number", - "doc": "The number to subtract from `input_1`." + "description": "The number to subtract from `input_1`." }, { "name": "single_input", "type": "number", - "doc": "The number to subtract from 0." + "description": "The number to subtract from 0." } ] }, @@ -4648,12 +4543,12 @@ { "name": "input_1", "type": "number", - "doc": "The dividend." + "description": "The dividend." }, { "name": "input_2", "type": "number", - "doc": "The divisor." + "description": "The divisor." } ] }, @@ -4680,12 +4575,12 @@ { "name": "input_1", "type": "number", - "doc": "The dividend." + "description": "The dividend." }, { "name": "input_2", "type": "number", - "doc": "The divisor." + "description": "The divisor." } ] }, @@ -4712,12 +4607,12 @@ { "name": "input_1", "type": "number", - "doc": "The base." + "description": "The base." }, { "name": "input_2", "type": "number", - "doc": "The exponent." + "description": "The exponent." } ] }, @@ -4744,7 +4639,7 @@ { "name": "input", "type": "number", - "doc": "The radicand." + "description": "The radicand." } ] }, @@ -5195,7 +5090,7 @@ { "name": "collator", "type": "collator", - "doc": "Options for locale-dependent comparison." + "description": "Options for locale-dependent comparison." } ] }, @@ -5235,7 +5130,7 @@ { "name": "collator", "type": "collator", - "doc": "Options for locale-dependent comparison." + "description": "Options for locale-dependent comparison." } ] }, @@ -5279,7 +5174,7 @@ { "name": "collator", "type": "collator", - "doc": "Options for locale-dependent comparison." + "description": "Options for locale-dependent comparison." } ] }, @@ -5323,7 +5218,7 @@ { "name": "collator", "type": "collator", - "doc": "Options for locale-dependent comparison." + "description": "Options for locale-dependent comparison." } ] }, @@ -5367,7 +5262,7 @@ { "name": "collator", "type": "collator", - "doc": "Options for locale-dependent comparison." + "description": "Options for locale-dependent comparison." } ] }, @@ -5411,7 +5306,7 @@ { "name": "collator", "type": "collator", - "doc": "Options for locale-dependent comparison." + "description": "Options for locale-dependent comparison." } ] }, @@ -5666,66 +5561,6 @@ "ios": "4.2.0" } } - }, - "split": { - "doc": "Returns an array of substrings formed by splitting an input string by a separator string.", - "group": "String", - "syntax": { - "overloads": [ - { - "parameters": ["input", "separator"], - "output-type": "array" - } - ], - "parameters": [ - { - "name": "input", - "type": "string" - }, - { - "name": "separator", - "type": "string" - } - ] - }, - "example": ["split", ["get", "name"], ";"], - "sdk-support": { - "basic functionality": { - "js": "5.20.0", - "android": "https://github.com/maplibre/maplibre-native/issues/4133", - "ios": "https://github.com/maplibre/maplibre-native/issues/4133" - } - } - }, - "join": { - "doc": "Returns a string formed by concatenating the elements of the input array, inserting a separator between each element.", - "group": "String", - "syntax": { - "overloads": [ - { - "parameters": ["input", "separator"], - "output-type": "string" - } - ], - "parameters": [ - { - "name": "input", - "type": "array" - }, - { - "name": "separator", - "type": "string" - } - ] - }, - "example": ["join", ["split", ["get", "name"], ";"], "\n"], - "sdk-support": { - "basic functionality": { - "js": "5.20.0", - "android": "https://github.com/maplibre/maplibre-native/issues/4133", - "ios": "https://github.com/maplibre/maplibre-native/issues/4133" - } - } } } }, @@ -6639,7 +6474,7 @@ "line-dasharray": { "type": "array", "value": "number", - "doc": "Specifies the lengths of the alternating dashes and gaps that form the dash pattern. The lengths are later scaled by the line width. To convert a dash length to pixels, multiply the length by the current line width. GeoJSON sources with `lineMetrics: true` specified won't render dashed lines to the expected scale. Zoom-dependent expressions will be evaluated only at integer zoom levels. The only way to create an array value is using `[\"literal\", [...]]`; arrays cannot be read from or derived from feature properties.", + "doc": "Specifies the lengths of the alternating dashes and gaps that form the dash pattern. The lengths are later scaled by the line width. To convert a dash length to pixels, multiply the length by the current line width. Note that GeoJSON sources with `lineMetrics: true` specified won't render dashed lines to the expected scale. Also note that zoom-dependent expressions will be evaluated only at integer zoom levels.", "minimum": 0, "transition": true, "units": "line widths", @@ -6654,20 +6489,15 @@ "android": "2.0.1", "ios": "2.0.0" }, - "data-driven styling": { - "js": "5.8.0", - "ios": "https://github.com/maplibre/maplibre-native/issues/744", - "android": "https://github.com/maplibre/maplibre-native/issues/744" - } + "data-driven styling": {} }, "expression": { "interpolated": false, "parameters": [ - "zoom", - "feature" + "zoom" ] }, - "property-type": "cross-faded-data-driven" + "property-type": "cross-faded" }, "line-pattern": { "type": "resolvedImage", @@ -7749,36 +7579,9 @@ }, "property-type": "data-constant" }, - "resampling": { - "type": "enum", - "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter.\n![Visual comparison of linear resampling versus nearest resampling](assets/resampling.png)", - "values": { - "linear": { - "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" - }, - "nearest": { - "doc": "Nearest neighbor filtering interpolates pixel values using the nearest original source pixel creating a sharp but pixelated look when overscaled" - } - }, - "default": "linear", - "sdk-support": { - "basic functionality": { - "js": "5.20.0", - "android": "https://github.com/maplibre/maplibre-native/issues/4117", - "ios": "https://github.com/maplibre/maplibre-native/issues/4117" - } - }, - "expression": { - "interpolated": false, - "parameters": [ - "zoom" - ] - }, - "property-type": "data-constant" - }, "raster-resampling": { "type": "enum", - "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter. It is advised to use the generic `resampling` paint property instead.", + "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter", "values": { "linear": { "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" @@ -8020,7 +7823,7 @@ } }, "default": "standard", - "doc": "The hillshade algorithm to use, one of `standard`, `basic`, `combined`, `igor`, or `multidirectional`.\n![Visual comparison of standard, basic, igor, combined, and multidirectional hillshade-method](assets/hillshade_methods.png)", + "doc": "The hillshade algorithm to use, one of `standard`, `basic`, `combined`, `igor`, or `multidirectional`. ![image](assets/hillshade_methods.png)", "sdk-support": { "basic functionality": { "js": "5.5.0", @@ -8035,33 +7838,6 @@ ] }, "property-type": "data-constant" - }, - "resampling": { - "type": "enum", - "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter.\n![Visual comparison of linear resampling versus nearest resampling](assets/resampling.png)", - "values": { - "linear": { - "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" - }, - "nearest": { - "doc": "Nearest neighbor filtering interpolates pixel values using the nearest original source pixel creating a sharp but pixelated look when overscaled" - } - }, - "default": "linear", - "sdk-support": { - "basic functionality": { - "js": "5.20.0", - "android": "https://github.com/maplibre/maplibre-native/issues/4117", - "ios": "https://github.com/maplibre/maplibre-native/issues/4117" - } - }, - "expression": { - "interpolated": false, - "parameters": [ - "zoom" - ] - }, - "property-type": "data-constant" } }, "paint_color-relief": { @@ -8113,33 +7889,6 @@ ] }, "property-type": "color-ramp" - }, - "resampling": { - "type": "enum", - "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter.\n![Visual comparison of linear resampling versus nearest resampling](assets/resampling.png)", - "values": { - "linear": { - "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled" - }, - "nearest": { - "doc": "Nearest neighbor filtering interpolates pixel values using the nearest original source pixel creating a sharp but pixelated look when overscaled" - } - }, - "default": "linear", - "sdk-support": { - "basic functionality": { - "js": "5.20.0", - "android": "https://github.com/maplibre/maplibre-native/issues/4117", - "ios": "https://github.com/maplibre/maplibre-native/issues/4117" - } - }, - "expression": { - "interpolated": false, - "parameters": [ - "zoom" - ] - }, - "property-type": "data-constant" } }, "paint_background": { @@ -8258,150 +8007,5 @@ "type": "string", "doc": "A name of a feature property to use as ID for feature state." } - }, - "interpolation": { - "type": "array", - "value": "interpolation_name", - "minimum": 1, - "doc": "An interpolation defines how to transition between items. The first element of an interpolation array is a string naming the interpolation operator, e.g. `\"linear\"` or `\"exponential\"`. Elements that follow (if any) are the _arguments_ to the interpolation." - }, - "interpolation_name": { - "doc": "First element in an interpolation array. May be followed by a number of arguments.", - "type": "enum", - "values": { - "linear": { - "doc": "Interpolates linearly between the pair of stops just less than and just greater than the input", - "syntax": { - "overloads": [ - { - "parameters": [], - "output-type": "interpolation" - } - ], - "parameters": [] - }, - "example": ["linear"], - "sdk-support": { - "interpolate": { - "js": "0.42.0", - "android": "6.0.0", - "ios": "4.0.0" - }, - "interpolate-hcl": { - "js": "0.49.0", - "ios": "https://github.com/maplibre/maplibre-native/issues/2784", - "android": "https://github.com/maplibre/maplibre-native/issues/2784" - }, - "interpolate-lab":{ - "js": "0.49.0", - "ios": "https://github.com/maplibre/maplibre-native/issues/2784", - "android": "https://github.com/maplibre/maplibre-native/issues/2784" - } - } - }, - "exponential": { - "doc": "Interpolates exponentially between the stops just less than and just greater than the input.", - "syntax": { - "overloads": [ - { - "parameters": [ - "base" - ], - "output-type": "interpolation" - } - ], - "parameters": [ - { - "name": "base", - "type": "number literal", - "doc": "rate at which the output increases in `f(x) = x^r`. Values higher than 1 increase, close to one behaves linearly, and below one decrease." - } - ] - }, - "example": [ - "exponential", - 2 - ], - "sdk-support": { - "interpolate": { - "js": "0.42.0", - "android": "6.0.0", - "ios": "4.0.0" - }, - "interpolate-hcl": { - "js": "0.49.0", - "ios": "https://github.com/maplibre/maplibre-native/issues/2784", - "android": "https://github.com/maplibre/maplibre-native/issues/2784" - }, - "interpolate-lab":{ - "js": "0.49.0", - "ios": "https://github.com/maplibre/maplibre-native/issues/2784", - "android": "https://github.com/maplibre/maplibre-native/issues/2784" - } - } - }, - "cubic-bezier": { - "doc": "Interpolates using the cubic bézier curve defined by the given control points.", - "syntax": { - "overloads": [ - { - "parameters": [ - "x1", - "y1", - "x2", - "y2" - ], - "output-type": "interpolation" - } - ], - "parameters": [ - { - "name": "x1", - "type": "number literal", - "doc": "X-coordinate of the first control point. Must be between zero and one for a valid monotonic easing curve. Controls how quickly the curve speeds up at the beginning." - }, - { - "name": "y1", - "type": "number literal", - "doc": "Y-coordinate of the first control point. Typically between zero and one, but may exceed this range for overshoot effects. Influences the starting slope of the curve." - }, - { - "name": "x2", - "type": "number literal", - "doc": "X-coordinate of the second control point. Must be between zero and one for a valid monotonic easing curve. Controls when the curve begins to decelerate toward the end." - }, - { - "name": "y2", - "type": "number literal", - "doc": "Y-coordinate of the second control point. Typically between zero and one, but may exceed this range for overshoot effects. Influences the ending slope of the curve." - } - ] - }, - "example": [ - "cubic-bezier", - 2, - 3, - 2, - 3 - ], - "sdk-support": { - "interpolate": { - "js": "0.42.0", - "android": "6.0.0", - "ios": "4.0.0" - }, - "interpolate-hcl": { - "js": "0.49.0", - "ios": "https://github.com/maplibre/maplibre-native/issues/2784", - "android": "https://github.com/maplibre/maplibre-native/issues/2784" - }, - "interpolate-lab":{ - "js": "0.49.0", - "ios": "https://github.com/maplibre/maplibre-native/issues/2784", - "android": "https://github.com/maplibre/maplibre-native/issues/2784" - } - } - } - } } } From 72433f6925c3622918c2163174700f2fd96ae663 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Tue, 25 Aug 2026 19:54:33 +0900 Subject: [PATCH 21/32] Reapply update v8.json from @maplibre/maplibre-gl-style-spec without some properties --- scripts/style-spec-reference/v8.json | 618 ++++++++++++++++++++------- 1 file changed, 471 insertions(+), 147 deletions(-) diff --git a/scripts/style-spec-reference/v8.json b/scripts/style-spec-reference/v8.json index c94ce02c72e8..12758dcb36ed 100644 --- a/scripts/style-spec-reference/v8.json +++ b/scripts/style-spec-reference/v8.json @@ -28,6 +28,7 @@ "center": { "type": "array", "value": "number", + "length": 2, "doc": "Default map center in longitude and latitude. The style center will be used only if the map has not been positioned by other means (e.g. map options or user interaction).", "example": [ -73.9749, @@ -97,15 +98,6 @@ } } }, - "light": { - "type": "light", - "doc": "The global light source.", - "example": { - "anchor": "viewport", - "color": "white", - "intensity": 0.4 - } - }, "state": { "type": "state", "default": {}, @@ -126,6 +118,15 @@ } } }, + "light": { + "type": "light", + "doc": "The global light source.", + "example": { + "anchor": "viewport", + "color": "white", + "intensity": 0.4 + } + }, "sky": { "type": "sky", "doc": "The map's sky configuration. **Note:** this definition is still experimental and is under development in maplibre-gl-js.", @@ -212,15 +213,14 @@ "ios": "0.1.0" }, "omit to use local fonts": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/3302", + "js": "5.11.0", "android": "https://github.com/maplibre/maplibre-native/issues/165", "ios": "https://github.com/maplibre/maplibre-native/issues/165" } } }, "font-faces": { - "type": "array", - "value": "fontFaces", + "type": "fontFaces", "doc": "The `font-faces` property can be used to specify what font files to use for rendering text. Font faces contain information needed to render complex texts such as [Devanagari](https://en.wikipedia.org/wiki/Devanagari), [Khmer](https://en.wikipedia.org/wiki/Khmer_script) among many others.

Unicode range

The optional `unicode-range` property can be used to only use a particular font file for characters within the specified unicode range(s). Its value should be an array of strings, each indicating a start and end of a unicode range, similar to the [CSS descriptor with the same name](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range). This allows specifying multiple non-consecutive unicode ranges. When not specified, the default value is `U+0-10FFFF`, meaning the font file will be used for all unicode characters.\n\nRefer to the [Unicode Character Code Charts](https://www.unicode.org/charts/) to see ranges for scripts supported by Unicode. To see what unicode code-points are available in a font, use a tool like [FontDrop](https://fontdrop.info/).\n\n

Font Resolution

For every name in a symbol layer’s [`text-font`](./layers.md/#text-font) array, characters are matched if they are covered one of the by the font files in the corresponding entry of the `font-faces` map. Any still-unmatched characters then fall back to the [`glyphs`](./glyphs.md) URL if provided.\n\n

Supported Fonts

What type of fonts are supported is implementation-defined. Unsupported fonts are ignored.", "example": { "Noto Sans Regular": [{ @@ -243,7 +243,7 @@ }, "sdk-support": { "basic functionality": { - "js": "https://github.com/maplibre/maplibre-gl-js/issues/50", + "js": "https://github.com/maplibre/maplibre-gl-js/issues/6637", "android": "11.13.0", "ios": "6.18.0" } @@ -383,9 +383,9 @@ "js": "supported" }, "mlt": { - "android": "https://github.com/maplibre/maplibre-native/issues/3721", - "ios": "https://github.com/maplibre/maplibre-native/issues/3721", - "js": "https://github.com/maplibre/maplibre-gl-js/issues/6258" + "android": "12.1.0", + "ios": "6.20.0", + "js": "5.12.0" } } }, @@ -654,7 +654,7 @@ "doc": "Size of the tile buffer on each side. A value of 0 produces no buffer. A value of 512 produces a buffer as wide as the tile itself. Larger values produce fewer rendering artifacts near tile edges and slower performance." }, "filter": { - "type": "*", + "type": "filter", "doc": "An expression for filtering features prior to processing them for rendering." }, "tolerance": { @@ -948,7 +948,7 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "`global state` expression": { "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" @@ -1006,7 +1006,7 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "`global state` expression": { "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" @@ -1064,7 +1064,7 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "`global state` expression": { "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" @@ -1097,6 +1097,11 @@ "js": "0.41.0", "android": "6.0.0", "ios": "4.0.0" + }, + "`global state` expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -1140,6 +1145,11 @@ "js": "0.27.0", "android": "5.1.0", "ios": "3.6.0" + }, + "`global state` expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -1308,7 +1318,7 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "`global state` expression": { "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" @@ -2335,6 +2345,7 @@ "zoom" ] }, + "example": ["center", "left", "right"], "property-type": "data-constant" }, "text-variable-anchor-offset": { @@ -2367,6 +2378,7 @@ "feature" ] }, + "example": ["top", [0, 4], "left", [3, 0], "bottom", [1, 1]], "property-type": "data-driven" }, "text-anchor": { @@ -2774,7 +2786,7 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "`global state` expression": { "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" @@ -2808,7 +2820,7 @@ "android": "2.0.1", "ios": "2.0.0" }, - "[`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression": { + "`global state` expression": { "js": "5.16.0", "android": "https://github.com/maplibre/maplibre-native/issues/3302", "ios": "https://github.com/maplibre/maplibre-native/issues/3302" @@ -2841,6 +2853,11 @@ "js": "0.43.0", "android": "6.0.0", "ios": "4.0.0" + }, + "`global state` expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -2867,6 +2884,14 @@ "doc": "Whether this layer is displayed.", "sdk-support": { "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3408", + "ios": "https://github.com/maplibre/maplibre-native/issues/3408" + }, + "`global state` expression": { + "js": "5.16.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" } }, "expression": { @@ -2879,9 +2904,16 @@ } }, "filter": { - "type": "array", - "value": "*", - "doc": "A filter selects specific features from a layer." + "type": "boolean", + "doc": "A filter selects specific features from a layer.", + "expression": { + "interpolated": false, + "parameters": [ + "zoom", + "feature" + ] + }, + "property-type": "data-driven" }, "filter_operator": { "type": "enum", @@ -3018,12 +3050,12 @@ }, "expression": { "type": "array", - "value": "*", + "value": "expression_name", "minimum": 1, - "doc": "An expression defines a function that can be used for data-driven style properties or feature filters." + "doc": "An expression defines a function that can be used for data-driven style properties or feature filters. The first element of an expression array is a string naming the expression operator, e.g. `\"*\"` or `\"case\"`. Elements that follow (if any) are the _arguments_ to the expression. Each argument is either a literal value (a string, number, boolean, or `null`), or another expression array." }, "expression_name": { - "doc": "", + "doc": "First element in an expression array. May be followed by a number of arguments.", "type": "enum", "values": { "let": { @@ -3031,25 +3063,25 @@ "syntax": { "overloads": [ { - "parameters": ["var_1_name", "var_1_value", "...", "var_n_name", "var_n_value", "expression"], + "parameters": ["var_name_1", "var_value_1", "...", "var_name_n", "var_value_n", "expression"], "output-type": "any" } ], "parameters": [ { - "name": "var_i_name", + "name": "var_name_i", "type": "string literal", - "description": "The name of the i-th variable." + "doc": "The name of the i-th variable." }, { - "name": "var_i_value", + "name": "var_value_i", "type": "any", - "description": "The value of the i-th variable." + "doc": "The value of the i-th variable." }, { "name": "expression", "type": "any", - "description": "The expression within which the named variables can be referenced." + "doc": "The expression within which the named variables can be referenced." } ] }, @@ -3076,7 +3108,7 @@ { "name": "var_name", "type": "string literal", - "description": "The name of the variable bound using `let`." + "doc": "The name of the variable bound using `let`." } ] }, @@ -3148,13 +3180,13 @@ }, { "name": "type", - "type": "\"string\" | \"number\" | \"boolean\"", - "description": "The asserted type of the input array." + "type": ["string", "number", "boolean"], + "doc": "The asserted type of the input array." }, { "name": "length", "type": "number literal", - "description": "The asserted length of the input array." + "doc": "The asserted length of the input array." } ] }, @@ -3181,12 +3213,12 @@ { "name": "index", "type": "number", - "description": "The index into `array`." + "doc": "The index into `array`." }, { "name": "array", "type": "array", - "description": "The array of items to retrieve the specified item from." + "doc": "The array of items to retrieve the specified item from." } ] }, @@ -3217,22 +3249,22 @@ { "name": "item", "type": "T", - "description": "The needle to search for within `array`." + "doc": "The needle to search for within `array`." }, { "name": "array", "type": "array", - "description": "The haystack through which to search for `item`." + "doc": "The haystack through which to search for `item`." }, { "name": "substring", "type": "string", - "description": "The needle to search for within `string`." + "doc": "The needle to search for within `string`." }, { "name": "string", "type": "string", - "description": "The haystack through which to search for `substring`." + "doc": "The haystack through which to search for `substring`." } ] }, @@ -3263,27 +3295,27 @@ { "name": "item", "type": "T", - "description": "The needle to search for within `array`." + "doc": "The needle to search for within `array`." }, { "name": "array", "type": "array", - "description": "The haystack through which to search for `item`." + "doc": "The haystack through which to search for `item`." }, { "name": "substring", "type": "string", - "description": "The needle to search for within `string`." + "doc": "The needle to search for within `string`." }, { "name": "string", "type": "string", - "description": "The haystack through which to search for `substring`." + "doc": "The haystack through which to search for `substring`." }, { "name": "from_index", "type": "number", - "description": "The index from where to begin the search." + "doc": "The index from where to begin the search." } ] }, @@ -3314,22 +3346,22 @@ { "name": "array", "type": "array", - "description": "The original array from which to extract the subarray." + "doc": "The original array from which to extract the subarray." }, { "name": "string", "type": "string", - "description": "The original string from which to extract the substring." + "doc": "The original string from which to extract the substring." }, { "name": "start_index", "type": "number", - "description": "The inclusive index from which `slice` extracts items or characters from the subarray or substring." + "doc": "The inclusive index from which `slice` extracts items or characters from the subarray or substring." }, { "name": "end_index", "type": "number", - "description": "The non-inclusive index up to which `slice` extracts items or characters from the subarray or substring." + "doc": "The non-inclusive index up to which `slice` extracts items or characters from the subarray or substring." } ] }, @@ -3364,7 +3396,7 @@ { "name": "fallback", "type": "any", - "description": "The result when no condition evaluates to true." + "doc": "The result when no condition evaluates to true." } ] }, @@ -3390,23 +3422,23 @@ "parameters": [ { "name": "input", - "type": "string | number", - "description": "Any expression." + "type": ["string", "number"], + "doc": "Any expression." }, { "name": "label_i", - "type": "string literal | number literal | array | array", - "description": "The i-th literal value or array of literal values to match the input against." + "type": ["string literal", "number literal", "array", "array"], + "doc": "The i-th literal value or array of literal values to match the input against." }, { "name": "output_i", "type": "any", - "description": "The result when the i-th label is the first label to match the input." + "doc": "The result when the i-th label is the first label to match the input." }, { "name": "fallback", "type": "any", - "description": "The result when no label matches the input." + "doc": "The result when no label matches the input." } ] }, @@ -3451,7 +3483,7 @@ "syntax": { "overloads": [ { - "parameters": ["input", "output_0", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], + "parameters": ["input", "output_0", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], "output-type": "any" } ], @@ -3459,22 +3491,22 @@ { "name": "input", "type": "number", - "description": "Any numeric expression." + "doc": "Any numeric expression." }, { "name": "output_0", "type": "any", - "description": "The result when the `input` is less than the first stop." + "doc": "The result when the `input` is less than the first stop." }, { - "name": "stop_i_input", + "name": "stop_input_i", "type": "number literal", - "description": "The value of the i-th stop against which the `input` is compared." + "doc": "The value of the i-th stop against which the `input` is compared." }, { - "name": "stop_i_output", + "name": "stop_output_i", "type": "any", - "description": "The result when the i-th stop is the last stop less than the `input`." + "doc": "The result when the i-th stop is the last stop less than the `input`." } ] }, @@ -3489,34 +3521,34 @@ } }, "interpolate": { - "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, `color`, `array`, or `projection`.\n\nInterpolation types:\n\n- `[\"linear\"]`, or an expression returning one of those types: Interpolates linearly between the pair of stops just less than and just greater than the input.\n\n- `[\"exponential\", base]`: Interpolates exponentially between the stops just less than and just greater than the input. `base` controls the rate at which the output increases: higher values make the output increase more towards the high end of the range. With values close to 1 the output increases linearly.\n\n- `[\"cubic-bezier\", x1, y1, x2, y2]`: Interpolates using the cubic bezier curve defined by the given control points.\n\n - [Animate map camera around a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-camera-around-point/)\n\n - [Change building color based on zoom level](https://maplibre.org/maplibre-gl-js/docs/examples/change-building-color-based-on-zoom-level/)\n\n - [Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/examples/heatmap-layer/)\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", + "doc": "Produces continuous, smooth results by interpolating between pairs of input and output values (\"stops\"). The `input` may be any numeric expression (e.g., `[\"get\", \"population\"]`). Stop inputs must be numeric literals in strictly ascending order. The output type must be `number`, `array`, `color`, `array`, or `projection`.\n\n - [Animate map camera around a point](https://maplibre.org/maplibre-gl-js/docs/examples/animate-camera-around-point/)\n\n - [Change building color based on zoom level](https://maplibre.org/maplibre-gl-js/docs/examples/change-building-color-based-on-zoom-level/)\n\n - [Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/examples/heatmap-layer/)\n\n - [Visualize population density](https://maplibre.org/maplibre-gl-js/docs/examples/visualize-population-density/)", "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], - "output-type": "number | array | color | array | projection" + "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], + "output-type": ["number", "array", "color", "array", "projection"] } ], "parameters": [ { "name": "interpolation_type", - "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]", - "description": "The interpolation type." + "type": "interpolation", + "doc": "The interpolation type." }, { "name": "input", "type": "number", - "description": "Any numeric expression." + "doc": "Any numeric expression." }, { - "name": "stop_i_input", + "name": "stop_input_i", "type": "number literal", - "description": "The value of the i-th stop against which the `input` is compared." + "doc": "The value of the i-th stop against which the `input` is compared." }, { - "name": "stop_i_output", - "type": "number | array | color | array | projection", - "description": "The output value corresponding to the i-th stop." + "name": "stop_output_i", + "type": ["number", "array", "color", "array", "projection"], + "doc": "The output value corresponding to the i-th stop." } ] }, @@ -3535,26 +3567,27 @@ "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], - "output-type": "color | array" + "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], + "output-type": ["color", "array"] } ], "parameters": [ { "name": "interpolation_type", - "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]" + "type": "interpolation", + "doc": "The interpolation type." }, { "name": "input", "type": "number" }, { - "name": "stop_i_input", + "name": "stop_input_i", "type": "number literal" }, { - "name": "stop_i_output", - "type": "color | array" + "name": "stop_output_i", + "type": ["color", "array"] } ] }, @@ -3573,26 +3606,27 @@ "syntax": { "overloads": [ { - "parameters": ["interpolation_type", "input", "stop_1_input", "stop_1_output", "...", "stop_n_input", "stop_n_output"], - "output-type": "color | array" + "parameters": ["interpolation_type", "input", "stop_input_1", "stop_output_1", "...", "stop_input_n", "stop_output_n"], + "output-type": ["color", "array"] } ], "parameters": [ { "name": "interpolation_type", - "type": "[\"linear\"] | [\"exponential\", base] | [\"cubic-bezier\", x1, y1, x2, y2]" + "type": "interpolation", + "doc": "The interpolation type." }, { "name": "input", "type": "number" }, { - "name": "stop_i_input", + "name": "stop_input_i", "type": "number literal" }, { - "name": "stop_i_output", - "type": "color | array" + "name": "stop_output_i", + "type": ["color", "array"] } ] }, @@ -3797,7 +3831,7 @@ } }, "collator": { - "doc": "Returns a `collator` for use in locale-dependent comparison operations. The `case-sensitive` and `diacritic-sensitive` options default to `false`. The `locale` argument specifies the IETF language tag of the locale to use. If none is provided, the default locale is used. If the requested locale is not available, the `collator` will use a system-defined fallback locale. Use `resolved-locale` to test the results of locale fallback behavior.", + "doc": "Returns a `collator` for use in locale-dependent comparison operations. Use `resolved-locale` to test the results of locale fallback behavior.", "syntax": { "overloads": [ { @@ -3808,7 +3842,25 @@ "parameters": [ { "name": "options", - "type": "{ \"case-sensitive\"?: boolean, \"diacritic-sensitive\"?: boolean, \"locale\"?: string }" + "type": { + "case-sensitive": { + "type": "boolean", + "default": false, + "example": true, + "doc": "If characters of different case-ness are considered different" + }, + "diacritic-sensitive": { + "type": "boolean", + "default": false, + "example": true, + "doc": "If characters with different diacritics are considered different" + }, + "locale": { + "type": "string", + "example": "en", + "doc": "IETF language tag of the locale to use. If none is provided, the default locale is used. If the requested locale is not available, the `collator` will use a system-defined fallback locale." + } + } } ] }, @@ -3823,7 +3875,7 @@ } }, "format": { - "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n- `\"vertical-align\"`: Aligns vertically text section or image in relation to the row it belongs to. Possible values are: \n\t- `\"bottom\"` *default*: align the bottom of this section with the bottom of other sections.\n\"Visual\n\t- `\"center\"`: align the center of this section with the center of other sections.\n\"Visual\n\t- `\"top\"`: align the top of this section with the top of other sections.\n\"Visual\n\t- Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", + "doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)", "syntax": { "overloads": [ { @@ -3834,11 +3886,44 @@ "parameters": [ { "name": "input_i", - "type": "string | image" + "type": ["string", "image"] }, { "name": "style_overrides_i", - "type": "{ \"text-font\"?: string, \"text-color\"?: color, \"font-scale\"?: number, \"vertical-align\"?: \"bottom\" | \"center\" | \"top\" }" + "type": { + "text-font": { + "type": "string", + "doc": "Overrides the font stack specified by the root layout property.", + "example": "Arial Unicode MS Regular" + }, + "text-color": { + "type": "color", + "doc": "Overrides the color specified by the root paint property.", + "example": "#333" + }, + "font-scale": { + "type": "number", + "doc":"Applies a scaling factor on `text-size` as specified by the root layout property.", + "example": 1.2 + }, + "vertical-align": { + "type": "enum", + "doc": "Aligns a vertical text section or image in relation to the row it belongs to. Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.", + "default": "bottom", + "example": "bottom", + "values": { + "bottom": { + "doc": "align the bottom of this section with the bottom of other sections.\n![Visual representation of bottom alignment](https://github.com/user-attachments/assets/0474a2fd-a4b2-417c-9187-7a13a28695bc)" + }, + "center": { + "doc": "align the center of this section with the center of other sections.\n![Visual representation of center alignment](https://github.com/user-attachments/assets/92237455-be6d-4c5d-b8f6-8127effc1950)" + }, + "top": { + "doc": "align the top of this section with the top of other sections.\n![Visual representation of top alignment](https://github.com/user-attachments/assets/45dccb28-d977-4abb-a006-4ea9792b7c53)" + } + } + } + } } ] }, @@ -3903,8 +3988,35 @@ } } }, + "global-state": { + "doc": "Retrieves a property value from global state that can be set with platform-specific APIs. Defaults can be provided using the [`state`](https://maplibre.org/maplibre-style-spec/root/#state) root property. Returns `null` if no value nor default value is set for the retrieved property.", + "group": "Lookup", + "syntax": { + "overloads": [ + { + "parameters": ["property_name"], + "output-type": "any" + } + ], + "parameters": [ + { + "name": "property_name", + "type": "string literal", + "doc": "The name of the global state property to retrieve." + } + ] + }, + "example": ["global-state", "someProperty"], + "sdk-support": { + "basic functionality": { + "js": "5.6.0", + "android": "https://github.com/maplibre/maplibre-native/issues/3302", + "ios": "https://github.com/maplibre/maplibre-native/issues/3302" + } + } + }, "number-format": { - "doc": "Converts the input number into a string representation using the providing formatting rules. If set, the `locale` argument specifies the locale to use, as a BCP 47 language tag. If set, the `currency` argument specifies an ISO 4217 code to use for currency-style formatting. If set, the `min-fraction-digits` and `max-fraction-digits` arguments specify the minimum and maximum number of fractional digits to include.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", + "doc": "Converts the input number into a string representation using the provided format_options.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/display-html-clusters-with-custom-properties/)", "syntax": { "overloads": [ { @@ -3915,11 +4027,39 @@ "parameters": [ { "name": "input", - "type": "number" + "type": "number", + "doc": "number to format" }, { "name": "format_options", - "type": "{ \"locale\"?: string, \"currency\"?: string, \"min-fraction-digits\"?: number, \"max-fraction-digits\"?: number }" + "type": { + "locale": { + "type": "string", + "example": "en", + "doc": "Specifies the locale to use, as a BCP 47 language tag" + }, + "currency": { + "type": "string", + "example": "USD", + "doc": "An ISO 4217 code to use for currency-style formatting" + }, + "unit": { + "type": "string", + "example": "meter", + "doc": "A CLDR or ECMA-402 unit to use for quantity-style formatting" + }, + "min-fraction-digits": { + "type": "number", + "example": 1, + "doc": "Minimum number of fractional digits to include" + }, + "max-fraction-digits": { + "type": "number", + "example": 2, + "doc": "Maximum number of fractional digits to include" + } + }, + "doc": "Format options for the number" } ] }, @@ -3930,6 +4070,11 @@ "js": "0.54.0", "android": "8.4.0", "ios": "supported" + }, + "`unit` property": { + "js": "https://github.com/maplibre/maplibre-gl-js/issues/7024", + "android": "https://github.com/maplibre/maplibre-native/issues/4071", + "ios": "https://github.com/maplibre/maplibre-native/issues/4071" } } }, @@ -4098,7 +4243,7 @@ } }, "rgba": { - "doc": "Creates a color value from red, green, blue components, which must range between 0 and 255, and an alpha component which must range between 0 and 1. If any component is out of range, the expression is an error.", + "doc": "Creates a color value from red, green, blue components, which must range between 0 and 255, and an alpha component which must range between zero and one. If any component is out of range, the expression is an error.", "syntax": { "overloads": [ { @@ -4148,12 +4293,12 @@ { "name": "property_name", "type": "string", - "description": "The name of the property to retrieve the value of." + "doc": "The name of the property to retrieve the value of." }, { "name": "object", "type": "object", - "description": "The object to retrieve the value from." + "doc": "The object to retrieve the value from." } ] }, @@ -4180,12 +4325,12 @@ { "name": "property_name", "type": "string", - "description": "The name of the property to test for the presence of." + "doc": "The name of the property to test for the presence of." }, { "name": "object", "type": "object", - "description": "The object in which to test for the presence of the `property_name` property." + "doc": "The object in which to test for the presence of the `property_name` property." } ] }, @@ -4211,7 +4356,7 @@ "parameters": [ { "name": "array_or_string", - "type": "array | string" + "type": ["array", "string"] } ] }, @@ -4271,32 +4416,6 @@ } } }, - "global-state": { - "doc": "Retrieves a property value from the map's global state. Returns null if the requested property is not present in the global state. The global state can be set programmatically and defaults can be defined using the root-level `state` style property.", - "syntax": { - "overloads": [ - { - "parameters": ["property_name"], - "output-type": "any" - } - ], - "parameters": [ - { - "name": "property_name", - "type": "string" - } - ] - }, - "example": ["global-state", "chargerType"], - "group": "Feature data", - "sdk-support": { - "basic functionality": { - "js": "5.6.0", - "android": "https://github.com/maplibre/maplibre-native/issues/3302", - "ios": "https://github.com/maplibre/maplibre-native/issues/3302" - } - } - }, "geometry-type": { "doc": "Returns the feature's simple geometry type: `Point`, `LineString`, or `Polygon`. `MultiPoint`, `MultiLineString`, and `MultiPolygon` are returned as `Point`, `LineString`, and `Polygon`, respectively.", "syntax": { @@ -4506,17 +4625,17 @@ { "name": "input_1", "type": "number", - "description": "The number from which to subtract `input_2`." + "doc": "The number from which to subtract `input_2`." }, { "name": "input_2", "type": "number", - "description": "The number to subtract from `input_1`." + "doc": "The number to subtract from `input_1`." }, { "name": "single_input", "type": "number", - "description": "The number to subtract from 0." + "doc": "The number to subtract from 0." } ] }, @@ -4543,12 +4662,12 @@ { "name": "input_1", "type": "number", - "description": "The dividend." + "doc": "The dividend." }, { "name": "input_2", "type": "number", - "description": "The divisor." + "doc": "The divisor." } ] }, @@ -4575,12 +4694,12 @@ { "name": "input_1", "type": "number", - "description": "The dividend." + "doc": "The dividend." }, { "name": "input_2", "type": "number", - "description": "The divisor." + "doc": "The divisor." } ] }, @@ -4607,12 +4726,12 @@ { "name": "input_1", "type": "number", - "description": "The base." + "doc": "The base." }, { "name": "input_2", "type": "number", - "description": "The exponent." + "doc": "The exponent." } ] }, @@ -4639,7 +4758,7 @@ { "name": "input", "type": "number", - "description": "The radicand." + "doc": "The radicand." } ] }, @@ -5090,7 +5209,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5130,7 +5249,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5174,7 +5293,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5218,7 +5337,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5262,7 +5381,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5306,7 +5425,7 @@ { "name": "collator", "type": "collator", - "description": "Options for locale-dependent comparison." + "doc": "Options for locale-dependent comparison." } ] }, @@ -5561,6 +5680,66 @@ "ios": "4.2.0" } } + }, + "split": { + "doc": "Returns an array of substrings formed by splitting an input string by a separator string.", + "group": "String", + "syntax": { + "overloads": [ + { + "parameters": ["input", "separator"], + "output-type": "array" + } + ], + "parameters": [ + { + "name": "input", + "type": "string" + }, + { + "name": "separator", + "type": "string" + } + ] + }, + "example": ["split", ["get", "name"], ";"], + "sdk-support": { + "basic functionality": { + "js": "5.20.0", + "android": "https://github.com/maplibre/maplibre-native/issues/4133", + "ios": "https://github.com/maplibre/maplibre-native/issues/4133" + } + } + }, + "join": { + "doc": "Returns a string formed by concatenating the elements of the input array, inserting a separator between each element.", + "group": "String", + "syntax": { + "overloads": [ + { + "parameters": ["input", "separator"], + "output-type": "string" + } + ], + "parameters": [ + { + "name": "input", + "type": "array" + }, + { + "name": "separator", + "type": "string" + } + ] + }, + "example": ["join", ["split", ["get", "name"], ";"], "\n"], + "sdk-support": { + "basic functionality": { + "js": "5.20.0", + "android": "https://github.com/maplibre/maplibre-native/issues/4133", + "ios": "https://github.com/maplibre/maplibre-native/issues/4133" + } + } } } }, @@ -8007,5 +8186,150 @@ "type": "string", "doc": "A name of a feature property to use as ID for feature state." } + }, + "interpolation": { + "type": "array", + "value": "interpolation_name", + "minimum": 1, + "doc": "An interpolation defines how to transition between items. The first element of an interpolation array is a string naming the interpolation operator, e.g. `\"linear\"` or `\"exponential\"`. Elements that follow (if any) are the _arguments_ to the interpolation." + }, + "interpolation_name": { + "doc": "First element in an interpolation array. May be followed by a number of arguments.", + "type": "enum", + "values": { + "linear": { + "doc": "Interpolates linearly between the pair of stops just less than and just greater than the input", + "syntax": { + "overloads": [ + { + "parameters": [], + "output-type": "interpolation" + } + ], + "parameters": [] + }, + "example": ["linear"], + "sdk-support": { + "interpolate": { + "js": "0.42.0", + "android": "6.0.0", + "ios": "4.0.0" + }, + "interpolate-hcl": { + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + }, + "interpolate-lab":{ + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + } + } + }, + "exponential": { + "doc": "Interpolates exponentially between the stops just less than and just greater than the input.", + "syntax": { + "overloads": [ + { + "parameters": [ + "base" + ], + "output-type": "interpolation" + } + ], + "parameters": [ + { + "name": "base", + "type": "number literal", + "doc": "rate at which the output increases in `f(x) = x^r`. Values higher than 1 increase, close to one behaves linearly, and below one decrease." + } + ] + }, + "example": [ + "exponential", + 2 + ], + "sdk-support": { + "interpolate": { + "js": "0.42.0", + "android": "6.0.0", + "ios": "4.0.0" + }, + "interpolate-hcl": { + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + }, + "interpolate-lab":{ + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + } + } + }, + "cubic-bezier": { + "doc": "Interpolates using the cubic bézier curve defined by the given control points.", + "syntax": { + "overloads": [ + { + "parameters": [ + "x1", + "y1", + "x2", + "y2" + ], + "output-type": "interpolation" + } + ], + "parameters": [ + { + "name": "x1", + "type": "number literal", + "doc": "X-coordinate of the first control point. Must be between zero and one for a valid monotonic easing curve. Controls how quickly the curve speeds up at the beginning." + }, + { + "name": "y1", + "type": "number literal", + "doc": "Y-coordinate of the first control point. Typically between zero and one, but may exceed this range for overshoot effects. Influences the starting slope of the curve." + }, + { + "name": "x2", + "type": "number literal", + "doc": "X-coordinate of the second control point. Must be between zero and one for a valid monotonic easing curve. Controls when the curve begins to decelerate toward the end." + }, + { + "name": "y2", + "type": "number literal", + "doc": "Y-coordinate of the second control point. Typically between zero and one, but may exceed this range for overshoot effects. Influences the ending slope of the curve." + } + ] + }, + "example": [ + "cubic-bezier", + 2, + 3, + 2, + 3 + ], + "sdk-support": { + "interpolate": { + "js": "0.42.0", + "android": "6.0.0", + "ios": "4.0.0" + }, + "interpolate-hcl": { + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + }, + "interpolate-lab":{ + "js": "0.49.0", + "ios": "https://github.com/maplibre/maplibre-native/issues/2784", + "android": "https://github.com/maplibre/maplibre-native/issues/2784" + } + } + } + } } } From ee02997f4ca7f96fca262a4fd806869c2f15abed Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Wed, 26 Aug 2026 11:20:26 +0900 Subject: [PATCH 22/32] test: port global-state render tests from gl-js --- .../global-state-with-get/expected.png | Bin 0 -> 604 bytes .../global-state-with-get/style.json | 72 +++++++ .../circle-color/global-state/expected.png | Bin 0 -> 603 bytes .../circle-color/global-state/style.json | 62 ++++++ .../global-state-with-get/expected.png | Bin 0 -> 548 bytes .../global-state-with-get/style.json | 71 +++++++ .../circle-radius/global-state/expected.png | Bin 0 -> 430 bytes .../circle-radius/global-state/style.json | 59 ++++++ .../with-global-state/expected.png | Bin 0 -> 604 bytes .../with-global-state/style.json | 73 ++++++++ .../global-state-with-get/expected.png | Bin 0 -> 174 bytes .../global-state-with-get/style.json | 143 ++++++++++++++ .../fill-color/global-state/expected.png | Bin 0 -> 169 bytes .../fill-color/global-state/style.json | 134 +++++++++++++ .../global-state-with-get/expected.png | Bin 0 -> 1447 bytes .../global-state-with-get/style.json | 111 +++++++++++ .../global-state/expected.png | Bin 0 -> 1423 bytes .../global-state/style.json | 101 ++++++++++ .../global-state/expected.png | Bin 0 -> 83102 bytes .../global-state/style.json | 176 ++++++++++++++++++ .../fill-pattern/global-state/expected.png | Bin 0 -> 5675 bytes .../fill-pattern/global-state/style.json | 167 +++++++++++++++++ .../filter/global-state/expected.png | Bin 0 -> 292 bytes .../filter/global-state/style.json | 59 ++++++ .../icon-size/global-state/expected.png | Bin 0 -> 3302 bytes .../icon-size/global-state/style.json | 39 ++++ .../light/anchor/global-state/expected.png | Bin 0 -> 192 bytes .../light/anchor/global-state/style.json | 75 ++++++++ .../light/color/global-state/expected.png | Bin 0 -> 232 bytes .../light/color/global-state/style.json | 75 ++++++++ .../light/intensity/global-state/expected.png | Bin 0 -> 158 bytes .../light/intensity/global-state/style.json | 75 ++++++++ .../light/position/global-state/expected.png | Bin 0 -> 207 bytes .../light/position/global-state/style.json | 79 ++++++++ .../line-cap/global-state/expected.png | Bin 0 -> 70398 bytes .../line-cap/global-state/style.json | 50 +++++ .../global-state-with-get/expected.png | Bin 0 -> 855 bytes .../global-state-with-get/style.json | 98 ++++++++++ .../line-color/global-state/expected.png | Bin 0 -> 663 bytes .../line-color/global-state/style.json | 88 +++++++++ .../global-state/expected.png | Bin 0 -> 3623 bytes .../line-miter-limit/global-state/style.json | 54 ++++++ .../line-pattern/global-state/expected.png | Bin 0 -> 1962 bytes .../line-pattern/global-state/style.json | 133 +++++++++++++ .../global-state/expected.png | Bin 0 -> 5013 bytes .../line-round-limit/global-state/style.json | 54 ++++++ .../global-state-with-get/expected.png | Bin 0 -> 1204 bytes .../global-state-with-get/style.json | 81 ++++++++ .../text-color/global-state/expected.png | Bin 0 -> 1202 bytes .../text-color/global-state/style.json | 67 +++++++ .../text-field/global-state/expected.png | Bin 0 -> 3876 bytes .../text-field/global-state/style.json | 56 ++++++ .../text-size/global-state/expected.png | Bin 0 -> 992 bytes .../text-size/global-state/style.json | 43 +++++ .../visibility/global-state/expected.png | Bin 0 -> 90334 bytes .../visibility/global-state/style.json | 47 +++++ 56 files changed, 2342 insertions(+) create mode 100644 metrics/integration/render-tests/circle-color/global-state-with-get/expected.png create mode 100644 metrics/integration/render-tests/circle-color/global-state-with-get/style.json create mode 100644 metrics/integration/render-tests/circle-color/global-state/expected.png create mode 100644 metrics/integration/render-tests/circle-color/global-state/style.json create mode 100644 metrics/integration/render-tests/circle-radius/global-state-with-get/expected.png create mode 100644 metrics/integration/render-tests/circle-radius/global-state-with-get/style.json create mode 100644 metrics/integration/render-tests/circle-radius/global-state/expected.png create mode 100644 metrics/integration/render-tests/circle-radius/global-state/style.json create mode 100644 metrics/integration/render-tests/feature-state/with-global-state/expected.png create mode 100644 metrics/integration/render-tests/feature-state/with-global-state/style.json create mode 100644 metrics/integration/render-tests/fill-color/global-state-with-get/expected.png create mode 100644 metrics/integration/render-tests/fill-color/global-state-with-get/style.json create mode 100644 metrics/integration/render-tests/fill-color/global-state/expected.png create mode 100644 metrics/integration/render-tests/fill-color/global-state/style.json create mode 100644 metrics/integration/render-tests/fill-extrusion-color/global-state-with-get/expected.png create mode 100644 metrics/integration/render-tests/fill-extrusion-color/global-state-with-get/style.json create mode 100644 metrics/integration/render-tests/fill-extrusion-color/global-state/expected.png create mode 100644 metrics/integration/render-tests/fill-extrusion-color/global-state/style.json create mode 100644 metrics/integration/render-tests/fill-extrusion-pattern/global-state/expected.png create mode 100644 metrics/integration/render-tests/fill-extrusion-pattern/global-state/style.json create mode 100644 metrics/integration/render-tests/fill-pattern/global-state/expected.png create mode 100644 metrics/integration/render-tests/fill-pattern/global-state/style.json create mode 100644 metrics/integration/render-tests/filter/global-state/expected.png create mode 100644 metrics/integration/render-tests/filter/global-state/style.json create mode 100644 metrics/integration/render-tests/icon-size/global-state/expected.png create mode 100644 metrics/integration/render-tests/icon-size/global-state/style.json create mode 100644 metrics/integration/render-tests/light/anchor/global-state/expected.png create mode 100644 metrics/integration/render-tests/light/anchor/global-state/style.json create mode 100644 metrics/integration/render-tests/light/color/global-state/expected.png create mode 100644 metrics/integration/render-tests/light/color/global-state/style.json create mode 100644 metrics/integration/render-tests/light/intensity/global-state/expected.png create mode 100644 metrics/integration/render-tests/light/intensity/global-state/style.json create mode 100644 metrics/integration/render-tests/light/position/global-state/expected.png create mode 100644 metrics/integration/render-tests/light/position/global-state/style.json create mode 100644 metrics/integration/render-tests/line-cap/global-state/expected.png create mode 100644 metrics/integration/render-tests/line-cap/global-state/style.json create mode 100644 metrics/integration/render-tests/line-color/global-state-with-get/expected.png create mode 100644 metrics/integration/render-tests/line-color/global-state-with-get/style.json create mode 100644 metrics/integration/render-tests/line-color/global-state/expected.png create mode 100644 metrics/integration/render-tests/line-color/global-state/style.json create mode 100644 metrics/integration/render-tests/line-miter-limit/global-state/expected.png create mode 100644 metrics/integration/render-tests/line-miter-limit/global-state/style.json create mode 100644 metrics/integration/render-tests/line-pattern/global-state/expected.png create mode 100644 metrics/integration/render-tests/line-pattern/global-state/style.json create mode 100644 metrics/integration/render-tests/line-round-limit/global-state/expected.png create mode 100644 metrics/integration/render-tests/line-round-limit/global-state/style.json create mode 100644 metrics/integration/render-tests/text-color/global-state-with-get/expected.png create mode 100644 metrics/integration/render-tests/text-color/global-state-with-get/style.json create mode 100644 metrics/integration/render-tests/text-color/global-state/expected.png create mode 100644 metrics/integration/render-tests/text-color/global-state/style.json create mode 100644 metrics/integration/render-tests/text-field/global-state/expected.png create mode 100644 metrics/integration/render-tests/text-field/global-state/style.json create mode 100644 metrics/integration/render-tests/text-size/global-state/expected.png create mode 100644 metrics/integration/render-tests/text-size/global-state/style.json create mode 100644 metrics/integration/render-tests/visibility/global-state/expected.png create mode 100644 metrics/integration/render-tests/visibility/global-state/style.json diff --git a/metrics/integration/render-tests/circle-color/global-state-with-get/expected.png b/metrics/integration/render-tests/circle-color/global-state-with-get/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..43b8af6ea3c8b1ab7abc62490ef623b92a2851f8 GIT binary patch literal 604 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Or9=|Ar*{or0OmKIieum+k+ds zLeeD|E_}Z0J0sSSW7&qyh1}h|I}$crXx||07^C7V)UmYnXw~{wfzv-2lq9YwZKz~% zTpba?;knUiLzkEmhu-BMPg%v@*&AO!^YP)_}Wp#BG)BaP=3 zi2UHjJB$FuhRKWwn)YJzX` zl=#Q-HK~Qmt7`e$)Ws(5?3UMk&R+X}b>fc{m*r<`&*uIJu{a^lWw^$7f8udr=y@!N? z-aTSVV3VsUeW;vLU-Xgyl0nUeQV?wrLPneDE3HZ9oDamM#gYyOs< z+K*<1ru=J*-%{EC-t|rPmSv&O#Qq)IRhIts@3zE>6v>&@I~LuwTKDp|JHEtDHXA$6#Qw>!w14qCp7*f2vQ_T8Gs#h@a)%QtjqhAvV60Zm zJbjJU=f1F+W1M^;mC~(~;D*N_w-8xq^5zj`=(bOg8FIN|O@rdC_B?X?48U zI?qbI_eIS^tH*IO&q+VNH{+Z(Q0X_HWW7hthJx}2=DoH+zT9l5XUuDjj7F|wbFFw6n^U>Te!S@B1KRvr~F~PSx@yC+)-#=_V`d9Oj zdbd=Y`mZ|a*CbXfdB6W?WbVDk)`I^tM5ayL z*)2W8@=kG#=*LSUM%xSc(`EAdZY$+Q21K|%Q#<#V#qJU7yiSV`mMM7;Wc(g;#64cw jQ45PIaHO4D@t@K6;6x`?0mq5J)WYED>gTe~DWM4fSfCX8 literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/circle-color/global-state/style.json b/metrics/integration/render-tests/circle-color/global-state/style.json new file mode 100644 index 000000000000..352ac4d2e766 --- /dev/null +++ b/metrics/integration/render-tests/circle-color/global-state/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "state": { + "color": { + "default": "cyan" + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "id": 0, + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -10, + 0 + ] + }, + "properties": {} + }, + { + "id": 1, + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 10, + 0 + ] + }, + "properties": {} + } + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 10, + "circle-color": [ + "global-state", + "color" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/circle-radius/global-state-with-get/expected.png b/metrics/integration/render-tests/circle-radius/global-state-with-get/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..6cf7abe3b19a774c5410a5d91454697e3f8fb70d GIT binary patch literal 548 zcmV+<0^9wGP)VAB#|LbaX9RECGBqr0A5Q6< zeVCM|z)Osy@OP_m0yB6eLup}Gb=ClO%ku=Aq`8418A{dGV?gIkNOu^+^4yW3RBc>$ z3`;jD-wKRLRH`?8czZ*on{bT|o;wQTWp|cHogrh4z->3;Hb1X0yQXN^HS)tU(j6@f7ny m2qAqwzHl8eK|T|{oLvUTI=P(%*tYQ=p7~e!&F`ycy|*b6o2=3&+?P!KYM5{of6~)BQ8z8Xf(GnCxO%4;@HaLSC2}i74|vDth^HR zT(U0V<__zcL+SjlZS58q=5=h}A#g?L`r-1)ydP!0FS~q5UT1cWG&5taVXTbC<%7~+ ze4m@gX>4v~?h(m4@}@h|)qk&C*wHuK8B?N<&)H%7_1lrNiLndacFLBD87Swmald+_ zaYbYMqI-*WxPA>2$+9WzElPjYd!+Y@b?}P7b?eWm<{iEJC}{3Jr`{s>9b(&8-&?mM zTSoIr$4!yqrp7AfIY7n*G2=t_IWu!QrZ4+0`u>99O|v?#jPBpv`4R5NflhZhfB0up z^&Yp=I~3y{PG2zX<(Xt=?;QsX{cP9iT{Gw|mU-j_4+n6FTw?jp*uUPc^xWR^SHK8h N@O1TaS?83{1ORwVz_I`U literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/circle-radius/global-state/style.json b/metrics/integration/render-tests/circle-radius/global-state/style.json new file mode 100644 index 000000000000..b078ce493a3f --- /dev/null +++ b/metrics/integration/render-tests/circle-radius/global-state/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "state": { + "radius": { + "default": 8 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -10, + 0 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 10, + 0 + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": [ + "global-state", + "radius" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/feature-state/with-global-state/expected.png b/metrics/integration/render-tests/feature-state/with-global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..43b8af6ea3c8b1ab7abc62490ef623b92a2851f8 GIT binary patch literal 604 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Or9=|Ar*{or0OmKIieum+k+ds zLeeD|E_}Z0J0sSSW7&qyh1}h|I}$crXx||07^C7V)UmYnXw~{wfzv-2lq9YwZKz~% zTpba?;knUiLzkEmhu-BMPg%v@*&AO!^YP)_}Wp#BG)BaP=3 zi2UHjJB$FuhRKWwn)YJzX` zl=#Q-HK~Qmt7`e$)Ws(5?3UMk&R+X}b>fc{m*r<`&*uIJu{a^lWw^$7f8udr=y@!N? z-aTSVT<1T1_0;ppD!_7=}SOgiE@ha_|c0({OgPuF91nY)Jb0?;-F MPgg&ebxsLQ0QU|?Bme*a literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/fill-color/global-state/style.json b/metrics/integration/render-tests/fill-color/global-state/style.json new file mode 100644 index 000000000000..a419b3528b4f --- /dev/null +++ b/metrics/integration/render-tests/fill-color/global-state/style.json @@ -0,0 +1,134 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "state": { + "color": { + "default": "cyan" + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -12, + -12 + ], + [ + -12, + 12 + ], + [ + -4, + 12 + ], + [ + -4, + -12 + ], + [ + -12, + -12 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -4, + -12 + ], + [ + -4, + 12 + ], + [ + 4, + 12 + ], + [ + 4, + -12 + ], + [ + -4, + -12 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 4, + -12 + ], + [ + 4, + 12 + ], + [ + 12, + 12 + ], + [ + 12, + -12 + ], + [ + 4, + -12 + ] + ] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "fill", + "type": "fill", + "source": "geojson", + "paint": { + "fill-antialias": false, + "fill-color": [ + "global-state", + "color" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/fill-extrusion-color/global-state-with-get/expected.png b/metrics/integration/render-tests/fill-extrusion-color/global-state-with-get/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..fd08ca201b7b58bac514209143a656bf2bb4d562 GIT binary patch literal 1447 zcmd5+{ZCV86h1A&sI1|K#%6&ZG%jhENR&BkMFVJuBB7-d<}BG5&4Q~M!TQNXD-_B^ zhZqeRAfoPv5hhzGgn*rV?80V(U&R=Kg?2zqD)Fn-RKty8>U&P1>`z#Kxlhmgoaa2x z`@Z*{`64S_x-@Pn07x^or)2|(85BWu6noq}?=-Q8I3sP#mz?N%ThPvJFx_x~;ttj@t`?+nhsm|Ber0QqA zO$=$ldQ<`YMkQ~0k`rGvQIe$OCmR8t;X;=6&yg0DyC!fb%c@@}hS*9bQ9O+jYjk0W z{HHnbzEPA&Uo560a|W4uY?XoWGJ|g(o~kz#L!%Dv%PhBken0i7E5i{bar zm}*k_O-AcNTJs{UmC?|qVb45`hVz<}`yztqGyz*fwY69y$%1)qiOleLY_d$sANy|{ z9k+d#%fBP|VeqxRY%b2&g7zz`4tGhxGRX3p20#B}JW5!3pzOp6@f|Jq(JmM8rUnee zMH6l`&$e}otZQg9%wvStp%3$Q#P&qb>b$}T$loh&|Hm~safR2&e9jRS_@xxgcN=ky z!d>%QAxv!|Wk(Du7q2Ci0IJOF8PxTRM?rt5gwG}~nL0-BDeRLTPXcm zEgC)k*5#5JnTi*yAttFgy)W+%&JCz4$9c+1UxMyQ$d??^$`kW zD0**TlaekOw-cN6D$mF_tVwOeJ@)J*{2k#cbXzo3x%??>c6>@(o_d!>HQOk5>1h<( zrW1S^FJ6UF{dwp>T|@#s=)lKU&fs*1j07g1BmP*{e+z|7B-B5SxGIEkFU>6qO9zUd zqYG}ifY=$r-H1h6ApzanXs?B@5l7G!6@J0>0j-cpHA*V{inT2|p%$Ozq%$|b=J=;* zW{QLPlsEV%SgHMQJOd3T+KP*{UWB_9)tD=WS~C%55{yAut*gVJb(io)$q^0}oMDr! zvm_A71R#a0jVuj%>j8ob7hEgnX6Gzo2)}~bx=^84J^u=5!17h-#|bD35$!z zX|bA&pws4wxoo<_o?Yaww7$)^9O0-{x4`0H$rix8Y|yPS4$TVH%&$7I&@l zk3DFyw>X()W>H$yS+u2KV*+ve?;hLsJcXr3(8k~Ow)cVTohL+PH{J zzYrJ2S5^?pRKlXS=?VmU`R_uXQVb0+5a{8grQN5p_iX2lVziW~(x=^q`B)%oLF-J7B#;)A3RNJ4Z2sa_kKRW@#}r{57c~BsBO} zRBfi8ivVesrv4V%V$vY|NO8R0bW<5K=vKq1T;rGl#7S&s3tkS99leM*QhdD|v0W4Y z5J}Fa!Q~<(sa6_>h@&Nxy-qp_l3u>-LwwGvbou}EA5`2~(O2E&W^5z5-u6da5a@U; zgLgB#^n6BHtQ8oFrTL7`#N47BV$%mgRJjL7pzq{kB+tsh!q+Oi@_6R zA!Ll8_e82>z|Ne(8BCSlAy&y9+fMBjVh6QdrQa8$Pcxkp+#?W~RB0jFj?0}*LL4M> zku4#1hggpOp(}9L#43tc_RXNDmfF_Rdon-fWHfnEnn_L_%bkcxlntOyY*{lNH0@Uz zc&!>Mlzo$kalOyRJY8Mbl-iesYJS-wa(iNTRD=qwKU2b`U+q-@=H;j|Ze8ALDe# z2iGFbzt8~u0C>p&N^D&@a%5~kjQ#l~pEK3M{7*8(&C$Ws8*_1~_6^>9j%G-k+dabV z&mRrl-Vkfl|IZqTi8xa*ueKMStv@p~JZ+(3`l~k`u|qg+*R$7@?76 zlBI0U{2!Xs)KtpsmOOvdnwlYeLXxFFbiz@5X(V);PPI2*u^Q{lb9GbS8d z?!J0nH?C2d8<+$SmupklduSS@k;cVj3cG&I(+-k1(6iDG>Nvmp{K`UIO$Mg#a7$q7 z>7hhp%UW%r9Y}T^g>a7?{?PG8_-6q>{hn_*7&;Wi9c4@FbJ|Ti2K4TmERZ<9LCE&U zvwFEyO6~}sfd}SZ4Q<_qAmE9aYZtsW!)|7dJPjTm%a=del0Q+ekGu?d?`pZ@G)Rc< zTIv{ulE4zgz*pYEUnwA2H?(z3+i6 zJlytT0)|Al4riJ$f~}5mPc6%Xd1|m;Nr_N~iZ;K0*3Afqcgqaa@Q^#u;ma7;G4gvY zzF)4duD&iD+ba)x=xGnRFEOxNcyLTd8uS{xaKeAc057DE$3S^U^Czyow$}`< z4nW&iI2VJ118~3rVRjOLe$lzqG^jqJTc37 zZ#xbqi7RPOYul4qpkM5!20=hQWTLo?^v1*+v5%s3lkR`^oFwKHD!O|2K0u8~8FDCO zn%IRxb43f2qz|n3Rorw`mH*`090+Ot!x&b93uf=nh%NB1u_(~tcaCJ)%fi6|Xr4qP zp|#fKBx-_Vu6#6F&K;M9v0Q;sFW#(q_!lYCK#y&Y40bZsK7y+;JDZ;jnSk{u=)m`8tEowQ zPw@Ny?7>$*yCSN;zyEOSY$cw6QZHIxNekoCCpX=xnjh!yPml6RYd%#wX{#xUVYXzH zQc)?p-J!;rm13XidJ7}K!ZhprkSIpE%He^5V}x}KhOr3}fRAh+uUEUaN=S?yIg00m z#OBISUfAiuU|^v~Zye;ca9e-*`cnHTfG_pKpH2kV=i^f=^FK4}zW`QANh8hB!AWwh z4jb)VEq-t9ZPeE1N3vI;R%WpK+`Z`kYzry$&(nd!k?eYDCy(l~YLYQR_SD7ndG+3g zKc~0qJbMFN73HCIJPHW;;bnGeTI}bD@AN@at-+PZwGf@2skZjCPdd z?l)Q4$;(ah1f3RQ1*eaw)ok&w+5hZP2T2c>qpzmU6O3LrEBsk^6<%ZROsD}&y+4}O z!X3#;O@fZT!ckQH?Cz8T~P zXk&qNT)i&IHp9v8lkX~I$<$EMO(zycY|K_%h3-F#2IS$QA5J&J zS_Kh_`WRF_Ki@M|bU=UR^j^L;G1(6uR3nJl*Iq%`ytMXgc)adkMiu^Z-T-XCQi{co zjqLhBzWhas#_^r|MLgXrC5;t%eUkiTwPY7TIf%JXl**ZHcsPbbolGUU8LY17Mmjbv z)kt<8V}S9|(3(ap>|u4(*JbTaOP$jjHDR2{M#&Gt$XV}#$Y2`w9hL(N`O`>_M1LS? zjgAz^dR7n0d<#$F@x3ldD&9KMhd-4%1|B1aSHr@N2z#(xs=y0GdhhOJY&}Al;M!)D zA@K|qI(h7-#qo*pw=d@68}H=|n? z;-3334A5A^x8cWfL!%FDTk<=xw*jH!`!XyY zQhpfrC^9&*5Bhl)L++drhJ5FXflr5`h7!GZQN(}=^GxZFxy5PDE2qQ(o)$qPdxX$hb>FEEHS>NQr6&IqeTx+itK#cH%EEn5drta=oswyCE|EB z1-b8vb^`6Z=^JfMtwRAOB>C+x|m^ zfvshE#Z`{5FE+{ouEKALP8*?O=1ZQ{fK)~Qje|K&gHg;LdgG(;f&;8&K;z)_DGU~9H6 z?K=}H-<)+hH}D}aL5rR;9=rb<1|P2RS)s6FciS}t=J3pfA|4&=P8Q`M2f+>Mxsurm z^_g9`uVHC7n;fq%0JVm#V595iSTgqd zJs&jvr=#%y9meKiQQ1h06>M7nV|;7t{#}`TDmcty42s8O)iJU@JnSo(h>HF7U^*3+ z<=cgGi|}Wa6V_Hx(|Amb@MR0>2`r0b@kD_d1v?K1TGX#BB@LXngui$h{5{GX+Nafx+Zu3b_lEK2+kI^)%t9H)2nbM)@x`dZq zA$cWPVx-<@gN1kfY;gLbmSKe&#;l1BQxc6tl2}6wG117_K3#GX!{#V_AM@wGRFTyO zC2Ejk1d8oqELk-`etH5`@CBC1I4JxGx7Wwmt^nlkaZ&)$A-ofwvQI3;s8MPrM`2j*XDH_jG&6JHwOfCofLmcP}H&FQI><3Q!F zu`)?T6)kBrw6q1oz}!2WrV9t2!=)WbQP?OnT$lW^$_3sJY6z4Iws;AUXzBNnFg~yf zKiob`Ln#lj*go|S3=EnP#Y}g{SRiDwYeIs*y}#oz6J(mXa1gV~bF7Ry-(d2lZ`mYR zDkUHWk%SEJ430&pbM6T3A+_lE3YqsgJr&&DykeUT2I8I@sR)jYwyX<#>S=%FZQMzZ36C{ zrP^LqvSzHhn_WA1YU{ZT46xCt!RUcWdzprNBCat1i+!)tk2M6Z3EduU3K|ilFZ8n~Z+N+7)#`+{ zifsO94Rdy(LHLS?DNl8#u{_+OQW|J%V^}QSO8{^7IhtL23)qQ1<2dQ5Hs^Q5=~;m=bd%Xl48iO z5P&TRlik{yZuVg12{Vb7Q?-x6Yx#Mmk)Y!E^}kZ>LH<=n`U#^(w1Igp|ALF;Zs&{V z^zBs-lBxTwk$Q~>eVJ;lNPnuKjOoh$H;2$&PRSx-`Wb?lZk zm>_s}n4b{P!Opbm_}qJU*|nE1ob=_g($PJ9l@{v;r6@TBcxaXg(d!*wzP9Cx>uQG? zj9ZVU-fTD7?|E>QYD>{Z0gWWciR{nW3lF0TPaHbP+A)brvEO4fk9myBy;?DVdliNa^h{zCHpJEyM?^R|ko;`3~dr@JYy#u&MNH0@XGyXk^jK?dpm<^)EQq1FbI zE)vmc#NuxwdW1(FJr&oEku4lyS&Q`zUbCAW$bZG6k zD>MeG3B-d*27=W)5=A-}v&z8IDzNSFO%`>~dWDJ+dCntdsjI?2_ZAz&ka4tg2qHLR z;6{_)uHj0xFBBEu2O-y>Rf>^-HNOX?2a!&KEBMs|o0OmshqKT<2XdM~MRY{AG+DNAdU}WLLj?*83%FmJ z{VMn8mQ(N-FmZ+b;xE<~WzbJh&uA1!<)rttKm9PW?-N2q2i4K>a6 zfyUypHnxILrEF&?ifAOgM8`yY`m>b1^sskj?$%p`f9q26*$FE196jl+(*GBRpP+D` z-1j`XL(#_Z;H}vX`_-E00yIwpupplJQyzMv`V;+uRfW53BD4JSfqc3?z=> zTn8Cqb<1$3(K9ng;BN2G0lQpeOS|r`YVEFmRTWiXAp<|b$&M;n+!dx`G*guV;;RzA zA%RbgVlzQir+M5~_`k*n2g$YUVh8&e*LUnaq6&NvD6o2b+ghqd0rDb5yVswllWPm3 z$oRgV{47Ui2DQ@PNbS1nc_q{C{#tj5y6Jor(N;z2e5AQ_EhN5U&UqAb`nfu^gL`lZ zu2U5W0$&u)&MFrG{;QfUFy@aXiADb!>uFvnw)Y`*Un6$;+oleQy zj)%vZQ5BrT%q?hhkKvH?CtEovwx{jnlqr&1{0{t(iYGPU|}%<~mmgREq1c?}oP z;99jFT!(!01lHLG?LZHB5exDeb2nra|1)7EUYn^uDR*h*>1uO=tN`n%^~5+Cr86f8 z3kbuWg*1+I@J>iFoi!0Q`C%=Zv8U+%Vx=vv3>pw~W?)sS+9`AO_ag4U>T~~26{15* zX&PR1cR~-|m7*BemyDd8Mi52wV$|rjnd>25Z*b?v+sn;;t_IWnqD#`q%z9Pa#*pL)A8Bv^i-ZOdiEPDBCuKA1n~$&f*M7=i@wMhs&y zMPO7L?3zdkSD1AgqklP_;RaBr95gVS*$?h~jEW=^VnRjRm9$g^@k;h0bhdO})5y~E z>6nZwU84VfN5BMgx>y|$8y7pMJIRRff5;iF@;70?AU@prZDj#w$rExM+q%Da4~rxG zWg8hbk7PA$c%T6tU`L?eMYgjK7&45cz^Y1~=w@W}hYshGMTDi)p>^P(rNKIm$F~yJ z=VeC@=)Z~r%=`Md(}g-P-2ew&cate~S3OGB@vtZz6dWB!6zXv$6|&JyOoAIhMA8R~ zRV5qrA=nL6)>b;s1P1@|y-^>LmJ!n=EhsGffB)q`6zQ+&3=oAf9UO8I+?8*9z->yl zsNW?jsj%B`mC8A?b>KRj>h2d$Tqd)irR>X`dwuy#?35$DriV1c zfeb$SbsuDy8^&tfO)1^$``}glwtoM&tMFa;IhOH<(JXmMED&x0LNz(zKT{Ex(arT40cnUC%+3|AH=`7KVJpj0^Z;K*vIc#QqFtLq2P^ivJ8ua-w0r~R^65q zo9Er_mWV1q>_r~c3{@Q#Z)JBwJHp+eXi(q%^L}^>X0gHifGG?(jIi&kD;&+aD^}IH z4DIJE@%IggvD}FE6UBw6JAv|zHVj&DJXDe5j=x97a$#}0SS-f}{BNiRVogqNR5+6h z_8ed7MT#9T{?y_;vMc}#O$>sC8sV~517ar?lbCNf^1OniDxickGcUm?Uw`evUcqd| zH&+^Ba6`|}RwT~XwOYi00lYO|iQ%IFB5|Tf2;?juoXLIxV)1^N&MeQ>;Kx)NMmf3< zN)YSwXz2aXRDH_e%Ru70CtlMp{%EFQD_qtdL7MQS?Z=S(|H_JI0KLRQw?Agh zU5iow@{{cK=j^p<*Bfn0s4i-D$mT=>=xgz#8R?1EUIS(2dpZ?-?h!`tbGLj|YM_ zaW7UZ53E@9xy3BqZSmCgZX0S5|E|judbbzN*H)~Jp6$r>##i^)7S}amKCcHrB0i0) zH`0V<9gA``A+@gpak8-a{>6YinJ^^*B)2Iiwr@k?{LPQzYM-R1DBI!&2;PtmBswk8 z>6B2>p|m#t&|Ee!ziJV0fG!s2qa;1xzrOOzWlJ5EcG6l<^GgmQPgIJsWE|dhz9JAA z+sdUht0y9|5zs0bm!0=4xOz!0vwo4nOZ`~#CGV=EhVvyZnhX!`2JiWm@bL!vmGk(b zk{aZrN%x*VFYkjnAkj_re7G;{111c7#2$PcMateWi7ZVI_t^eJ)I>a^K}IeFcBIXe z?GV;xg(}+iV4isz+2oHVlb(pENEsYFwV0{Mk$&TITx_h@PQjyB`~k#O1y9sb$Tc#qY`7qO1-Y^EK3e$*$&<^9L!R9I1Q8LD@Mm$5+mQ{(Pcu&L z-dk1b>qF+=pF@b!w?OoouFECA#lr64t3q2DKc{z-oZG_xUp`534Q3kh%bXo56sl9(a~1%uWIe56mW=4-&BnIWG8)%wq?K((b(tBL@P?pNW)95+YzuWs zdsy6&+%{EXP8d9S?FUf9)VD)@4T+MIbuLRZLJ^4^wG-F;t^8=(UKxWyF2Z&=gaB(% zv(T>I-3{Cr5@mQk*Y*_EP#@fDuiam!y+t!GB|}9rGEH7-6*E8TpD&cQ+KhjjKVN@k z_5KP`o$~uxP**5P5C6aD0SQdYT*MYXV0Qgw^;VbHdU@G(G|jU(YH(SrKeK9ZCwTnd zRPkW?b?u^F7_*TK=z%E4eH?d?hW-?*90x zD`D89Zu6rHM4EN^->?`XE>O|@m8+8n89Q1Rmxl|z`o#-kjOL8up`i}Q+W1m>@`Hxa z=W9M1i1fO1@V(=Wi$?j>#|Xs^qrtrkHHyYQJMJJj)D8P~6qL6PL?SPv>Xv--<;9Ab ze{%|PjRn#hHqO%goE`Xf8i{h(rI`bdF-Z49>)b)r*)=-G%QB=X=zr%$-M~xk>^O8b zeH2Ra{5`;fr~#%DK=rU1`01f?LK}QYby3a5x;f$PH!#txnJ5f@R7LHj$n=c=(nvus zn!0DPRGB%?q#^bXCnv}dNFfQ~zk~?8lKQ%RMkS%}lIocF zV6pF8;BO0mngf5a?`Teqjctdc+NgIYEk>0oJ_M0BG8D1aZ|aks_YQS@OcDFXk}&)z zgnY)o)N7(z1e(7b3Ix}7{*WO=+Pi*(l{epYx8UoOx`kVXcSqyHb~as*VSBHt1o!=2 z5h~_3CD*uCvL3V|>q@beukM@&Ih~ifFmj_2hbw8}f`SI$5?XB$csivLSO7aiT$ zPfvoFW!@$>Pu*)~e9AWd;9af4jKYdxJ|S(Kg2+3swK)c}^JAL&FSH(3?s<|kuS~Mi zFhM)_$(<}wZlsyzudhcl@34Z0xU$N$THYF_C*4sX{cO!{SyQYKtt&VOr+fl*3TV_m zzAX$*C_CXGa4=cFnbRWu)cdxe& zp!_&?bCnw297L0G4*mR*e7C=^?**5QNY#qJ6^K$r`zI!$GJFCLg4C+sF=mv~{$qh| z3Gm5dt?EN1mBU$8xZhr){(pJU63>wQAYCPUj~9;6w#vwf?hLlo1h- zD3irYQzi(_x~Ks5AMFNG`Dpu!A9t~FgLMxVWN7mjJSEty7%hJ^BxMj8mB-{CIb4pf zVD8D>R2QSL&eeBt<0%F%D}VincP`9s`_r5XZe{|wo;PAVaoHY{<9Vm-b;nl6IXl$u z5&mJ~^0Fc*60+s9qPOoU>0uS|H%;AC{@jrwy);yER=-MVsm7>TVMhGNou$K_E!>=<_2Y1oxM$#Vju-1d)|C9{FBx;gibR3FN z6ciGN3@Ix<6HE*iOET)+lK8BarEvxP#U2l-h50h>_13BO8psyYA;u?*@ETFAHF|`g z-q_QxrBlwK<;|C6OtrBt5@DBxP!MWi^h5;MXp(})ytd`Z23q^4W$C$d@^A0%UQW-_ z-c=op?|-8_1_k&`u8?ci04kg_JA&!dn|{o3Y}*Xov6NUQn(N#aWu`s_9*u`#hBE(+ zw`3nVS2ETCU3t>3=z<*wVuyX~uf#JPSk90Y+@R6YEDL!Q&sipFGHIsjS8Y- z%I>nZ0O2pM`C;Kz*(dEnj9yXaV{LIJxwSif+>qDCx**hZp*%C1=i_?qN0KbZE9Yi& zXUatSL9}XeqzX>vGmr4>QanP2BKKYvL))$8EVVEYhQp*9*4izbtHJO7oEJzzt97zN z(zv&bI+(JrBDZ!?{pJ!T1$Sr4RG#77*P-VF#tA6&w-B#u;%e=zvgr}TE=um*yB7qU|IwL;0Ng0Xl>u$MGcq2*s$JNL@f3XpHl#h3V*V{M55exotG^Y+ zPxJ-4xm{p3ol=P{S0ynRq?O1Me;_N{*f)@w{pMR`Yxnegy_CHs<){UR1KX@NBT~t? zCeZ(^tpdw&g-mXOy;pL z=b^iM_%D4_?DNmN$kB{V}I)a-B@JxVj53<@aP zqXY@Q8R%`Myf;w$eybSje~&sb+1E;{Hjg5t6FIaxOWyU_)x!=d^hURZ{o-Zg5XZyA zyKUX4FSv0;X3TD#P&V1P+73IQoN#=sE8*b#`(LN$yI}tgE6>#G>~?^7$|`GBeS`VmEfGKhQHbFPgy(Om?KaB3 zRVE~EIWA6nuxM<)QI!rSUq10#_88Qt6ro$)IPm_t+Gv|+J-SeoFl^FF*vQK|_|f|;iS``&?Qv{g6Af4vQI;4&@=e$UTfk@1jgJ#P{PFIR4zMY_uN z7Xg!F%u)q1Qdn5r=m3@VNxy>$F}x>Lk$$tLqx8K2+0J{}&45w^<~m^UafEZttdj?Jb5o;YUzBe;{nM z5f>l!(Zo%rSVH$;EDRPCMQ;WlHFKajaFv>t@s}VRoU#M7xL3V(L9p{>w8Y~>v5(xC z*f&Sc?Pa7X6#^X$hG37ZjMDhE5^LN7>jU?}?gJDFZV7saYcK7>Q~tv@wrJ?U;k97H z^1|BtDt$aZg=&H@{=0xa5ues>jcmJ9#KhmBgLx)_cG+hSH!ZLcUCPgijEp;v_G2jj z|LQMsBj*{rSJVzsDJ>IHC*J__>sk7C2R|%}B@52W?Fi)Q47_1Pz0G9JO|*_>^>DdC zIkRJ8P-5ZlY_Pk-P6akBn>W-TyX1R%P*q|Fr|^CRC6+xty#Myb9|}O*W-jbJmkn!P zI6J(1A#&sC&L-zP<^A3pU0iRqGi~nKh(u#{NJ{G0%f(8KhUVe>Agpm^!2E>SO_)ma zS3L1NpebfAe2eYf-(U$0JBb5f7o1pJJkyi6nud4gELxij_!#3q*)xK6Nu2%LI+dQ9 zO}Drw6^k3D8!Rb?Dng34sP2W;6dkoAvekJFyA9V3jKmONRba^1_5l`0#$BS0p8fzJ z01iTKFtj@8RmRmHy^bF^GY0hAow!~-!Y}C0Srq}2l7g*e?qE+;Eo)k9=UG7>I{K9F zxNUNf4mJwVwxzTh!TEs0<>TafmGJi0)>QDp1vvbg3)*>YzVSko;M(A>wM;UI>N?Q= zrb%eMXqsf_dwF@@_%{ZAh!NL#uk;MikWiX4S4f=6j7%e@d?|r)xx^Kf=(U4UhdTw}k(yvv< zYOrn@G0}%{Rvn?s?27(jYwu;#Oov1N1tjV^5Cz0hA2<8oS=BW&vqu1&!wahIh>x7mrYB-zxXtnr|cbC|&3%1Ei-G z0Ce45m@0*LMS#2G0+?oxl70xTLTU5mzDT;MLmSrHFaZclSnL%r_=xGW;|(=UF{Q}6 zZ`6=bVH?%veEJq-(=7?&b8=jXsUYtjCcuu|e`7+xgu!{$mV&f5zIP0H7q#}voF!9& zxq)0=|16MCA{OL0d@J4ySTXc5aToRsFvuNcEH6sz3~FJf6C27 zbmtp7sl0AIt@A2;(Li=fe@y=;nA+dD*uQ|_kofRIbzgh_O=-Lab!(Qq;+QL8uwNh3 zEo{Rw$40Z8kIjfrPx5@%wg0-_29M_#O3WLc^}OI$W7ebqXe!6k>~AQ@23cd21>w)% z3A(YEzo_;|GP`<27QN1rIQzvs$*p|Q@J>=aGO=3zf%A<3+u@WLcHF?%amr<2!8fpk8l%IJ{N`3>E^E&EbU z6AXFKjH$kj)QMe}nwhzo;y)zQ?Jam2wIe=Ma1-OGfGwQ+m(e=@Yrgl`jwA=~C((OG zMygLRswD?6Um8dS}6ZOt3tPkaac zrylkk(uQCY$rbxk&9Y)wgv79j)952Z)crc8&!=i4g{1Dn82`o_ar^{wPZrB?Q!~6s zxwBFSR4Vg9Nii4ubSO{KWF~d)BD%$J=L?0(;A|C} z5Ati}9!PFQqsqn?{)FOlSEGDlEGOfHgxn3w*vR)x2vEgC=1( z63!EFsX1l!Fk3~Ho4wkh6!1wqIg3969WYDo;#z+4v+P3zvwpPsa)ho3@cf{SkPP=W z=p^d8vk??{;!GXdQRVz~8lmb;kLLb5)olpz{o#2&;aeHgBg!viwp6U3c#rN9mRSq_ ze$p9iU?if}TN7*ZNFaE2nBZ>3;z_>JUw!Sn2qVBKyB=5SK>0@PUmJgyZD(_)@HF32 zd;)hHUbiOdw0zNSNIT9Fq_P)l6{z{ChJu2C_f+}Y;oL#xgIs=7j<2TN+{znivDNYZ zVSrd!`n4G9MLEX7=iJUx4$c)4c~l3(lt*q4Fj$4JdgPl-=_OIV&#&b;{CM56uKu^= zOcTetGSwWo!A>1Hyr+BwOLXADN(Vz~lQ-&<5(i}Q>bS8{e*`hb> zpMxifDcV0fCqqalOr;f!BT-PJkpOr={#0#c#+IlO|p5+O%zMdFPQKy zj|*Nxn~qOs(%%pfo-J2wYs9|q&iwE&G++J~#~sGd-REQKc-~d4R%r$r@AXk>wGSVo zne9$5cHDi*MOF!29J{Z-<7Ed7LDzNzF5Ibv`Puh49i8;GXPC@UZ#1gy-g)kqY~<{j zb59!UxGUkS2vmsZTb`8~*NZEnLaNZ?!(0&IOL7zYMlH=svgoM)=NL?E>^UU|0$>cbzv21;bY^C{ciiS6ppZ@ZNt?FXv@?b}mqCwxD-OJW^UX=qKiyFQU zFNyPamFARb47Vd-&}_L^+GoZ7Xl@n)N{!tpVhum@2dgtStoY=D$aU@EGITd&-Kz#R z3UvNeF+>&}k=-pp#r|wrWg+W0OND~-@f#Yo&sJWqOvzsJOAM$8iAa6}OnBYC2rGiQ zzRIj==*_nA4*~9=cbD2UT}eauCzm!;RBr3h={zw;a;~?VRzH@C2DZ+v%yXIAor>w* zrfK`ojwoN`G_Rl`nsh{O0)Y@oin*escrksC^i_X{R}#}e4<>q&R%SCLI%{Z~;kHYd zm?Ofbf{6QOjcZ}?)0SY=fT707{dwLW{FhLNnyV&-ERKkRa7V_(F$PU#otI<*mT&QY z{d4ez@pZ+Hf#^HQ zXv=tWMTt3<_DZoni7&I3z0hmFhk;k=XwLv_0{b=1rqyW&?Y1PbB z5~$cx7B)a4x4e|FLO(I$#@^7-)V$gWHJ!QL3@Zl z4NoqgC5HG&x&zcPMsw5(zbgSEES$zJ5tA{bpaxt=s8io*>0f7O z+qL-p9Pg5tFcDVALgMETM6Ffp6u%N{!^KgcFngBdJ6aXtf86;z77?M-r1V3PFd|e*?E2r2HE+%B7DdHB~Kc#C!7#t^p}$)?&riQ zCY@!pl0!*;1xyxJeV@=GA=~aN{}~Is#q!xail(vB)tTC!&qhr5z{Xbl*6WogdThB& z%bC5ZP=LPM8F=D`-4WxyBPPqA)C*{|9_K8BBH&aW9tv8R>gf2SGw8KGy;tivvabi+ z@gBIiT5A5LP;0YCe9dQp_TYjQeINBm?_JMJ z#h8Lr?C{ z#du$H(u`v33sTUHm6RNG=vWz^TsjNJM1Kpg5qu}iIkXTe=&_uDn{SPI2Xl)B7`j+* z|6y>xOz`cK2RaiK`DPS~TWSI#L%T;zDC%`zY~M4x+Lx<<8)fJ%&fU#Rlh%)NL*sm> zkGAd~7j`aq7k2D4Nri(Fs#e|=aIj6vSisyrlGR)P&f5-idF7Uo!PIK=nDWp595;H` zIQnj2Vv>4lt{e6WvC}Z}7oW!&%{ucKo51F|q_~iul9ListTa z-cLBjRKho1@=2QkMV-xf?7LcvGqg+yvTot%qWy`lY`rps%QECo z;*+tm*IqR#;mv@(&jHLtfB+mz%gVu2VnEx^xw$zIxW&SI=VsBm@w5jIS){DdDWKXF z`tx3=_@Cq(#-DkQB=Hqk90FuY&n+CUS4n*jN*ea0yAMP6x#2DJKUsP9=Ubc+%>x?q z#|jQW^@}Sxx)~vFI_g<^*K~m^qI_t z7tB}IldIc#7CLM-0Tj-?s#3Ua){<4Rl0v#MQBig3+7vRbtgYC6awk0y;X35RE=2;# zJ6o#^5`RP-*NdOnI`W77TdY{?P-8I-&sA=W890C7`}(JSA0;IYxA!bFDMJcD_Pwfq z)b3VZCl<0qMP3cBn)1y9%$-B6(PB_gN6$j%^u`j+zyPn$vm_$Y(8=8-K&K_}O@#j< ztzhE~MDC9JB|y`o+)Ur+&BZ$4ogpQ>mQ95>0hLuJzH@TC;(Q#i}_HjfUq9~}|><3kE6aOF(JLqdgD7Fu}u4$yVF^=<&?rq7Ia?oi4Z8GQh|p6vsWM!$|BWdHcB?|h3J@Y z;zYbyBARg=rVaHOSU7&MY20~o_$F5jy4aFvR~h`^G6&82M?ry*FMA>c9&Lu!!(3ez z<=zRWFN|jqQr4(3DZbucFV*Cz`ta0On$h3pu|D2DO!YdeLZAeA!kRA|nk@;6$xP*1 z?c#+s(?@l6D~Ft~EaSSlylGCMumFion^rmSXR7zAVToPZ6a@Kr+RgXMir{F5z|D#b z3g^qrRb5n_?LFz{tNh5PAjCd4iM^&3!vYR61|^S#iJTPgTT$>=3itJO(f*SL!!x2E z2wP75uAMX3?TCbS_QmIF81HHjkhhU?wmI1a6WCG=aQ3ib`#F*8C^H=~>{9Y$r1$mq z_AnVK32!F8|8(bHN)bV`x9_F$As3D2XvN@FBbwQxUif3L)=sA?5GCn+9=!cy;->R; zB1YWv72lx@cr&Dr&7 zSy$8I9Ms4wJqc8nYg^TmEpr-oP?7^~9?xVpNIOh-}g}Bi(1|`OWI?r1dChODE)AIX=qK;QnTju^~0i#Xnhmgxz zhcmrPmz9f|Ae6YsVXALbT7t#G8;f`SmnOUVhq-txZNVxLal zMQY(+Y;Fdr1|G!XPFRiWWP``92S)|V;)jcVgQF@{Vj}q{W&DYv z=kJ%_t*WP$rFvcLt_L-_b{9_6oI>7-IT%>LK%{NGxiK|E!yFM`k7ho;qC=0Zm0sZy zxM;QvBRQgpk)c$cq^yBN=ju_IP?5-?4-26CoPw%P$$Hqa7}TYIOMhD>zJpBcW}X|F zZ@h_N*d``5?b+CicAq$#?MtM;3g8}%-?S<@eYU%nTwAvF!gS&-?Qy`5x?fJa zwcc<=OB5E?Fs3t_U{S9(!+1GqICx9CJ>C5q^uBlZFbh>rubxd#G1IdtN3$!w2feoZ z572B*2M*3qJKS|d@6RyqbI^)^`M<%v__iM4x(5DscrP@&!t1#+jc`<&NKBb+bL#+% zyUw^KF(5hrEVWLceDt|MWbnNsAGGt0dk32&4jsq?6^Zfo&+}-JMZJw^@@0Xll4Cod z<=?voPs_(9(`hq@x)!*5`+HMYAK;E21o3^W%a^2=EapOSJM9v!!g2^bKRcXBD>-d%7Rg{9qu}T0}H_+tfKnu>{T$}l9 zgf(P09SiKVw_eNu;*X1?Cg7N#Qy?|w*GL*f7xB@)?Z2xvhN$q_?!GC4rY{F9<)$|F z7WTBr%B(-Sax4Ww3g14ww5Jzw1ln?PHEC%6|x6L1yQN!L;8bT1L`mJVoe=OQN41b1AT9``5^E7yVVuKkqx*52su8G7?AG#7b`t*T%vg$KZA77vh zXaNru5MOE|9H^AUK+Okgf)-);G-Eajn;K#rJp`aWwwf=>(Xt?#^Xhh-MD?0e+LB!g zb=t?e-{h!XluS{&zK!)GEvKsgU?J$9{TZwMwT8G{rBBPORg=kOv5t!Z5dQ*qaq)@R zp=?TnOI{a>F!ECoUq}9Rm2YxQC;~hatb=nQdzkX&x3>_Z^H)7b^`P(4PrO+N#t(eY z@PE;tw7~Ljdtwr~;mta>Kl0M$? zq;1exHZRZSE1x+umgzUr@nAM(;Os8bGQ-wd_O>HgQO!QnPhhq5`2-A%;qgsh)#U3! z1qc9CS)xl}Q~)lrTX%ODD-xf0NV8<`*8DlRJ`L%M+4WojAE7@#55pk8K0LA&3k2ng zZ#~VkZ|Qt4K(yPel-{ZU;w z`H2<>g0?U7cT-yhGdd*NNX|Bkm4|(GW@=xYz-OX>H~F{qdNC7!ve~q-_|VqxMZ6ih zb$@gp!6Rs%aD@8i!;3(3jJS=+AAOnQld z)(X%hb^kZA@FnIsMB7)z>Luz&a4tHz;{`UO`qqis${Z^%M1IA+xL|{FVP*$pc+#OH z!2NY)v_lK;8dft2QI-yd%*K>Oh?a!OefJF7v!GKrXTCsL#OjmXZlerksDG#`?;z`o zRfTb}>{5Ixv~GoIfW}j>oM+i!PEC=#I+)+?v#;dlc|0%Ukkz?zJj?RAyLqKBLtKj# zx(4hK*wuHN#+_7|ysrcW7v2;emtZLmjKKAK;lzL8fJG&u4AaoYv+0Of=@mH$={}XM zT2z1ol=)P#oI0w$2`fKrIx!ib(JRW3&EuvbAhfEpYJ7`RMHL|jX&$Iq3}xz-K)nnD zYQYkFbwT%5rc!Ph*e~f3{Pf40wtT6b^Y!!z`&R4BqW-}|ov+^km1OunaYKmet;}EQ zHyoMdzLLUu?&!JtJ)>C*?lJ1>vmbxQ5wahKC8N_qvq^`31V;=gb{RpCV4_y1#sbb%A4h5F+2i z(oQZR9G_~cam9_ZIex&Anu+7;u=gwQSCD?s_UU`y$7eEpbUN4pOb|9Jd^I}p$s2QS zrSW4duf^Mt?L(H~p5)F=me=F+R&Is+^LNUJw{2_Y3rG;+p=wcB)=|GFgXcVH6K&VY z5}9ihZWqzJC`wd7GdyK9R>IR}xz6Zr~ zr_42=a^Vz~{z{0C|9l26D(YAxu4_`rw4}$gXc?Su%VKtQ178Zg%fu8~;q~~YaEjUn zD>hNzcTio1Y7GB-%brJQ@7_d^PB1)sL8E|r80}vZk{6-ywy2#ob1v~$88a>%-ZNwT z&wijAiklrDDQ*`KbKVbf`(+9t-|<_x8)<-C;f?px>iB2vLdVoNJ$vddT-I(ARdHgc?~ze8Tkb3A0QTHx@uj{u0mjrO*xJ4} zqn&<-ZMlH#*^i0n+1TXwAcFQ0(>Nnh)jxEj!?}f6Ab2>!M}bvLMl3XpDD$G+NGIFXaKUxYbi?QW{FfMD9d~Ndu31El`y{Ir}|m zv+D?7E0;-<3G?o~YP+uQEwhW=eZ9eNo>TCk{nWx7O%)v6XaAUWfT~vt5pwW*7<@xM zTvWtTQ`5YOHm;xZS5<)^6~OmKr2I%n_tK+_#rnE{1i;gE`s4M|&jXfRZ)60pUTc*J z{n>%})5E$JNv^ zIKrsfm8bbIO6XH6qc0BinV3t9PJUlUCm_8y`P z{=q(%mn%Vo%eKeJckmEE35YovFLeZvJ+S+DoP2liWUNGVV{P~9xpR`iFg!RADaqT~ z)FhGTO?9<|7zK2D3+1s#hGYm>gMokp-5uojxM;AimoivsL6mr3cekVyR$RXpxn_^< z+zO=-YG{KyjdZeb>RPyFf#0POQtNse<5exaodX zrFcYe?tYJxWd>6z{WInoYOOqlRLg3%^v`8y(S_U`c?LAQjjmt3AlI|{!2@|#&?BNK zKaZa9b?9zuo+(3Q;yOT)xR0uKSjA3Y*LeMs-SSzIjVyMOmiTC;AQEDzv%`rVH4 zvMZM&Sq*n9DrjnIiaJ`FXY=Og!|vE~Wxn=kZ#66q6wfpbS%rYaMx zprG(03I0**Z+uoAvHUTR+ie*hmLEs}M1CjKy)8)oN%WxVCVt@UKDSoOR?<-I;|G+T zVu)-5a>S1*(ylfiI3~oNpwmhH`mhH8#KJNx!vj4p&KKlQ8hHOTyARO1JqKy?zQcs~ zu>So%A3b^WkYW>)gyduOM%s5WfzmRwq=?YIQ;Bpu)Nf$k=3QcWr-!UG%p3Cua6Xoy zA*w1V76QcayIY&Wg@vZWdFCKPl&QsX2MWbBJ+GDAbpW~JgZ=$M0t}sNUaCpl$1G!r z_aEfzF<~3(7bPN6r@$2qa2xZvY1Ho_>+qlyB0^wb&>~HxE}jc35goy6>KMfwPa{b0 z@+HZUhfHKK%p_IFhf;y#*FSwM%PUO&oh+jc%Pi=&7oI1sdF_+Oay=&}#%a{&mBv13 zuZ)h1!FdK10iMSsD)E6GbpCX<%oWO8ea%xLTL7tvtuJ1P$0(QQT7LC1DPHdGS@?UQSfLRef_P4LF;|-3R%ZTwZ5&}w$ofz2H+9Eck`F6AbnB-JUR98 zr;htz$-lK^IemM~5nge9%;^}w8w206wgE;9I>>X^)DM*LqcI3raHA%m|;3o=RyGBYCIUp$j1DV?wS z|5!mmLE(J?Rr~md`K~uyG-pOS)VmGjL!;9l2otVR1PVfqS9-78sP4&wNbdX0kz_Q} zid_f9rH|bCyA`EEIBb5N6bUU{EQ87>y21IMKm5uiZ z1dKXxT*?b)WM#`h;9x?ESrh=I9YO{Q_CU^x9s5N2$YS!lb)#7N0HD7-S`Oz|J(3P| za7!Z{5Uzi5RiC*|Coh&ujrp>t&m}Dl=iJ&*E7jxD5)(rfVgfzBUBwo(MaXV=WOxdl z>C#K@QrxARv_C&sR{(P14u@MD?(eMf`}C}(ovf@N1pC$3lQuknYIqIfUdLacCq3>9 zloI6Gcd^28`F@SGKt6W9Okx*gxgyjK8NrVD4%=W`CcAMd)9kiYLXm)c%oPe6mR*MX zyX4rI1LlG`MO^kd!2n9k9p`{^!8!4{Ns%1%ggC?2a=7*Yt_7|Mt_`jct`(L6aP4pn zLBc4?gNS^j=fSQ2FQK*jdCONz8Xri1=gys?2*4wNr2s_k-@R2aGV0d8v~|m-dAoOR z_wL%UZ91AQe#pSszk3(OA303P$B$9Usgp7v!^so!-50*|7XAC{^F+4aWB(?eQ@*DA zM?DU$r>#k1TE6SE&d(gkABBkc8upBNU2i}J-|Jm|kHgm6-Yh)=X72MU|CbdM6cpY! z5X9$mJHE<4ayQv>9K+f)Iqeee(^Y6UR&&-s8^1W<5evW?_FTnkb zDuAf`*4`A90m4pXg2o#Dj>E_4{Mo!}q?%=9Wc-iJjMNNP3VO3M(rnooslKd?)T!bt zm&PBmb^heR9VxB@87RziwdevJjfJQ-am=p1IjJHSH+Tl=-dIStgce!DkTP~;#p0dmpA27ljc>ZAiqwoo*Vo-3vDr}gA^ z*-?VH+iA7_?@^cQ582Yp^tx=LO`ToR?iO3PV62wwwS57bmZ)P82g zBZ-fIQUoPq*fK2FKpOe9G)a{{ekP=-QsM2#K}CY+J9p_y3DyJABcdPfc+}`emA-Is z9z^qlpf^TEizfiZeg5$qZ_r$}^qK6n3l}c@I-alS=;&XFii-N%om;m??T^`0x?@}P z#Ln$or}ysONqcylIC_Zg-?~YiEid@Kr_rJnD@F0x6t#uE_SRy0>3RQ2x7+$jz81%L z^g|Iv1;h@1zQWJhP;X~Y$#py0d7EhO$jHYL;5tLbhkQQYCs^s-!QXdcTPO-YnEzM> z1qFrg6ZpsM!^qtac%^Q=kw=rC$sWye_-_vr$5a0MgwJjJn$K$e1ahO_MaGBlBk6Nk zzcT7}9%ak^iQlDpM21K6V~rkxeVbVkKqNmC{&a3`?k}^IeF{6$|b@leWo_3DQUIeHN2+b$hRjaD9X0 zyiL`!M$FeQa{2lNJkIgTXAR;-2uIo?9j4#buAXWOpYxWY+AGOKD3c%PG-42<&kgp7eNzqyd!T(XkB zxnRk=5;J;L@csLHe%7M`E=L)-+xvS1KqdD13l$SuB6VKYmRJ)M{(2ljbPK8P(Fl1Iq%J@^fjR3qSgr-{-cc zfb1b-1f=h>=|{Z|AFL<@fxX)@0;>EijNU7Ody>b0DvmCmJ$sm~wHs&(G%sqG5H+lK zGz>z;K#;dNR`My#X&=%wYv{Q0tTgO;MN#!M&zEo^*Fq)A1)(2RfSZ=8FKG8b&6b! z-gl8}ae1h@Z-n;emqg^AyHg#B6$l3--mIygB^N1=5xG{!Y`GfCb$X;eLQJUG59D6F z_Y}d3P#6F7lKq>gQ@63xt z^J5^=A7y-X@v+idf}b~^-(!#Wcs#!r6&3YotUQdvx<@tu1_A+5Flh0LRg$4`eE&XK zY6zDMmWqMvHRyQJ4;o5y6W*+&NGPI-wuSMRnAfQ*1gw{A+1C~t4` zY1)@rDftNGNG$-5y$AQL`FTy~|3@taC@3iWkb|w2BCI_luOB~JsA`6Yf1l4Q9C9(_Ki|lr{;-#hi%YU#%39jz%a;=aloO z+5(5&^oZuq&pqwuF$}W*TBs=y8V9Y|c|b@R9)a@(`7|^*K$8;_G(0fycJ=`3@taMw zde?qY7{Wz^=4{+SM`Ghe0k~9nR`wS~fsc78K8Z}}nY8azf_MPFv!1t2(9xTlc8Mne z0g4D%1mTT1LEvtxZtIN@C9MC}?jGqpXdfA(hNq7SOC8PK-K-cqjBE#MTJ~o-kOi3- zAXUh;@OSV$>P~g=nJpUqi-)-Ca(VtQClJbeP4WIKJDDT zb*kdVb=tFYyDSHMiOK%&LdpTi{3~xR5HCSSS}J9Qf=NNDoy`sKxY!TwkLg~%dbPv{ zz`CEiY^A(H{%i9WODf>ug9jun5TBu1AjluF1&BG(EQ82cG2q$dYtn^fuAERD0D}II zMSuWLJhup5o#1Bpv45t>NMDVD`+2nWFAR`_3A8{p8A(NE_G@SCdTuK2@P*C{c z1e5j{%D9Gu_3#6Sv_Ft`L+vxE^Iv=^*fnirXo#*97DR%g9#mF90hkO~sgt+MN+LDZ zQRg1H_D}%uH{>Ag3~5qNA3UJrY;A`<0O$>X_4%Bv45{~z&p^ru7DTMTu4A#{5r|Dn zqO&>KA-NWj8wt=lUm$%23@ItJW&aV1Iev;xCMHrkTmEOVGiji=mntjDrR^{jjJ*;A zpqC7S^sH>j96-^bu;*a=p<{IU;yKDl(MQM;)-LJ<)U|bxL$gY40i^Ds5z>vzMO63X zp#;Va4i8aTb-m=dhnE7r+rTzhQ@>sJf<`RE5>rs~@Sa5QgHk*YWzi5?M zfn7QOxm*my3S4>kjCxsd0!dp3d+D?(NT?60D#^{~;c;oyz)FXn$s92Y--}$-?nfL% zJ6~5FlU=IGNK03(4qD=0eO(B5@v3#SW$RWc&%1clT4}`#GKSTj5PNbOL81ru?iO#r z+V$(ieLrXMGKtvV9vwwST{2l%>1=LjkUafxbDm3luh*?#FBU)I1lDZaBm>I_kdL6k z9owQM_zzwLDHz1xLn(*wVJmo0)7EhkzV>muH&f~-vHXlapVP51a3;n}@kRVCRY z?aj?}HWc-b?L*lfVNcEUh1~4OF)B++B%=T+qp$k^TtPuWVHViBEDCrXgQ(OoRcyV1P<$+d$2Fvkp+9=WUCKYy$e$h_926PoRM${j8|gHM zb^5RZ@aD#y2-ce7F}--}o=9;Yv<%*H^c2PFQeKA`N~D3x#9@9&OT({C_4` ztlxtyv4)SJI1I!ITw==`);@v>{T_|%BmGoae2ud7dMdwCBy|JWN-bf6DMAWg<1^&- z_lf%+`=EKwl?OGEj(d=M@W4RA8y^eBf~5N&TlJ_M`1xQ3ta%i9sno^;q$(J;hfwj zlmN#6!KlaKdUU%ol7@*6R=^Wd&DMfq;2Q1A}iMbn?5K9onT<{!N;iX4~r zCKFyG8pzzdekD@*@4W_GtsvPY+k+{2QTvWyeSQ!<>yhs-YxGHaDe8mjeux(US;Muz z_25y;Of`x!lF#IP{qjZWNuMff{Aq!#H1kAWcIIgH!+TSAZkACle?GGVUj{2WXheji zg4KHt(Sg`RNg)gu6N1ta?mP&E1X1K^spd%E0n7)r23|fVl#oSAox?H+YyI}P-}Pw( zmU$iaQz(vvqBm|PbfgF(O58p?K&|z)qBKPm2%aZO`XYsn2^#^E`Qh@j9;e;RO28OW z*3jlRmx&SkA}a#2AQNlwSf578WfTD#?(Lcmc$`+=Zy#hJ&V%I^vtls`>w9Lchna+R zA?GyJ+`lV@kEAJ2FkWB^fsQMgNZ(q%N=Oy%_hqZs2-(7l|IWfC;>Q0R()>c*`C$En zcoBUM@4&+4D{1@IEn3XlwI5>zAckd+v^GAUPUq_vd-#wj6I(;6eVaCIlvgxIiaT z54SvA_9*ZB25+CvBxxD!broN`+?|$cnmCb=D20NM)!iGxyjA_(j3PrH@UfKUvkrPzc_vD#BuB-_BfugJ>%(>DGV?a0TR9U4x*}eAt4Zm zA`nO*(R*(K1W2Ir_9}^@{jJ|RXTQ7%7>`ptiSzE~Igg}Q?^$P`eb(8l|JU?I%e46r z(8J8lmWavPwP;;=F!&oJn7P93JZ|P>1U!3?J7AFpS+AfAyaTulap`b4{OROUGhJto z=NF@X?`)wPid&?@=mQ`ii`th&8_WNE%%myq zf0y#c>yNw-Hyj|Ru>Y*Q5AT@AybnA-C%?dd!FeBU=(+#qFK}w4Mrx$SzjRPq6$*uZ zCLRj?Su7Noh$0}HR+zXZmTNrZKOn!S$AZB>2?lR`s(&d~5}hBGP48#p;qYI{#yJDz zIZ+6rPJGlK6grKvAr6ZM0)GTx(4u9TTIM%r(Gmo@2-&RSf`Woi0}S-shZ&lgkD$%U z%{R-}YOG(Z_tQ69t~?{x&Xj96 zyXA~`&YnCLm2>ZpoH|Rx3dxlN2zF>pbrgwP1-V&QC?ix@V)@Qgna9}K3(X614d%%A zSLfy#IwI0K7exRNpqAEFQ@!t)y5@<)D*#Y(+`rfe_dN^^2-ZAhh2Qfi4P0 z&)iuvs!0s|oU3GzTu5jE)IhupnqAz#*Rtv8#KhpFCbh&S*q~& zQ;|sfXW-e#@p45)#SaAnfuGi9Ka>TA{d-mEq9o9PRX*Mw3QUoA<}VQR4Gj&CrQ^I? ztv?eBh5l65c$}Pj_+R>-rbcR{Mr!<<3^us1M>hY6zc?QDcGDCE*F8|ExGZg!KQ903 z(h}2DSBsU{7YFDj;#*|P{;-~KE6WC1C>v!rsCv**nApr^Npv#J2BFjF(&>o=0<;Ad zOZ>VB*{=M&tcE@HJH8@9u3+c(t@r2yK=YratFuf_UV$d*k*r6jKTQyvKF5?57nq8& z;=Zvn9i;$(pY)mY)um4-!8b-tFl*MVHtWhtOjYs2^C5qrtRUayWUtZw0)zv;;r1Os z`5@j;NI?XM=kS5OcS{N%BqvUsyGZi}22Y%+3c#x}Px2J-An<=+iviw2FmvSA9W)nO zI}}KV*0)z(fSp^upn~}1?mVcdgM63sns458;_-ma`2By)a}tGOcO>Y$d-Bp1MftM= zzP$06xpb>jX*mEx9xXL&m1~{9*{)av2ysgGXpH27_Vmhen&*WjOW%Ny-=pP=vn!oa zL>tQ|X8*?fgU#nf;GzILCkMixY&3Mrx$Sf9VnR`~N@$N+a>;t?MgwQ`4lV4eJwl|0F%}lkR`oG{;Tq1K(fW z)RE9c==4N_6~vu)*KSEFc)@glMeWArB~GGV-1BN`IeNjhB0(fjIEHB>P!Qf7`M1O1tjFMHTSnO~;gWo+uRa z*6%WhE?qZggm&qkV~Gle6+-*Mwd+co5U8tQkc^^E)7*3RSlC(|Bu zK7=f2eKy(d+iwie#Jv^s=b5=8XeW#tV}=hOu66w{zx}RxUj9#M;F$DLX6EEcX0`Ad z4URON6bcN5hc2H#y&vIDwm#NAv~{CLpnQY^rwA0)yKeI)M?qE9XxfP2dX9l#0q4&i|zMfz(Ki)c7wr zK;$D9Zaej{DV;xkS{t97IDAOKf6DXn%y8cDWR2Y9ad8XzItjx+$t6seYC52{l)jFNe?j1YK&Fj}q zQ$xL3TUej~L7)wi?5C9jqEwLmze$v%+~rQqcS(M(DiV|;f?$Z?2Qz@`0m>A?6R?;D zT$7vzni=gw7y3y%ZggqP}wLYe4(>)mGDRx>-Sh@HA{Xbmk%2C=$>n8PSc0s~dl*JR4dLBd`&~ z2-!Ep5>FeQuE};}^M4?MdBmvE=A{7+v@S07$>Ya58S|pxtrfva$G$OcA|HZXGx{e@ zFqyLUv!+Zk%S1Ti3BvjgdOQ7Mt>3p$y;KeW8H~5VTKLwJfba(D_f=6YtO7ue!9Tw6 zAk=_&VD{80PO_&cca*bIzhAw+N)?r_zw(At-!Gq0)@PIRsDN=RTHtG6Pc+y^Gba0g z>iH?>YrWp$U=ql^oI7<&t7PbUXqyEUtX!^2Rm|y#>7#OkHrW=yYR9Hc+D@Bt+E02P zNR8A;jsIdJ7WR8&bGuIqQ!8vKWx8Q{P@eP|YTNQ?Z zVs zG#r4-zGU}N70}=Y6x8iEfc^x!Zfm<>&BcfW`VO7Dq;>iU1>p2$74VnZI?a*GH%vI* zqqG46I(FrzDi{18Pk}vW&9)|O_(MekiEV%FHs+yzf8zLYS~jQk^{zlArcN^72SZ1g zF=NM?_dXn^UIDWFamyo=SG!8x8bydKH~X5J)RVBTVy&4hf}ih<%ShKy0T%p(G3jRY zvL%WiapLHq5G8#Gc-;Sl7DyT-^gzO)4K>gTc*eA;W@Y9wGw8!18j|=sw}FsF7UUl0 z&z;>@Q(2y{+<9)>YBrhCqedpY8Yl!lPnW6P_@EUqRV!dPxaE8v5C=~l+MlRq;d|gb zSU@27i1!6hD*&@BZ}SdN0*K&;4oWh~f5}}ji`T3%w{G4@c>z)*HB#eyJ49%&i3Yv* zakX3FW)I^3NA~YCJ8Ehk`kc*gj?(G4Db(`{&}t^A^A8cLI7Gh zNj24WqrKjI0ju`DY__ito;X8M_vq4BtCrHku)$@bs90!Vvj4HbhfkiNb@^2?r&Grc zYeFD`mv}r0gA>L2r&JG!-}#xV9OYp4JX29tVj6etG`V8!WsC9vsL+I2bJg`fZ^;r> z2+E5KPV8@Nd`6U{R?6wBiXSLS*&=*U7~aX4q{95o^vN0y08$wI04f%2^C6NO3v;vX zlIK7kf>mNrOaMIs-L@0901^j!Of=|A&Gb)f0u?@dZM9QA;15tR9{4bK5gie zxp}vS6F}lCYB-{Fo^H+i$(6b@_DJQ{gGXlmxj9xAcyzdE1-^;g6f0 z#yr&8TWcGjonga=_0?2Y^^w{BNZF^X+2RHBRrrI}_spwrnit+2pa4LW_RX6;(|Fq3 z?#-Pwa|?poLI;2I^>@Tdf6Kf%aEKxY0wkoKh<68?g>z<`dDEw=XMr3Alm?U+4(2>@ z+?W%>-!o#u-=G{)?nh{X?{h!o6Tp)V+qXY7SDx?M{9L#CA?Sm@N&bVs;ep4M#y0KR zrL+oHKU_vh{)`*qxE~J(M?xze@vn`@yF(5@e~95vdOt{w)JTp0B7=-)Vw173e*@@e zBromQv_bo!Clcg7yd1}=o)GODP^f_~?cILD^(`1VR zfr}p3Jc__FU|2d(RIYqe*$d3zIBJN5F}57n{W^oVYx> z0MN)?xk3d!l?QxA1ppR1UWM02PB2R{GfiQBcC&2oUz2-YC!2RqTT8Q{o|`!_7v{jZ z)+<30APNVS0VoFKF3gy}i0cIkato}3=<|{;KjvJG1Mvfd|hL0Gg*PxrA99^^s9yfaQpIy9o>bGT1ZPzSy+HYHt*TVq{*bTpj;w7{IN*KO;x16BqE#Ic|M5WA?YpOs9yFUP z*J{WN&w}!}(8K+|zNE+?;5AWJUKlxNIgPU1_wEO}{r+ExhJ2+wKi0`flr7LO#V5BT zq(*9_#(%j%C$m`4^Cx1>ze@%*b?4e-MLVC-;y2J75BW>v|L@<4h5poTi2aAwI=8ib zTCCK6=nnhGip5kH4+j99>2*5?hUD)>$oY3O$0qK)WrlV-=7A0UWWsTMYfR{q< zsTYNX?+u?a+weO|ON^TGkbk;7gTLyJv*51x526r=Rrx&Ys_trS=j$^;zt?~`==XW_ zXF&c|)I#|`8TDBGxYiNoSo{c9-22vjjzC{9cXq5GXU$zKbdvY}_8TuIJONfwAQ_K1 zd-^C5xanz6*|{-a%0C01+ryVHTij+d`Z?!?x8GCc!+HPrca%6e9Y)k*M&x}9G&T-N5)5w z8gVRrj7d&VB^v{wIh^K(C1xk$YWYl(~q$8qO{A=asV43@0%KHBJliry?OUqA3 zgWkUvtA2EM$iGe2pYDK`&%=OX^+W=T;*rn`RK+wmH~;7p-kBc>g}Q!IuK9wTv#6Vr z+mJu}ao^q4NR8A;jeqe#_=_d~WjSiQqrpxQ*0G*w@E%D|?jgSiqVlstEYO;;zw2|R zrRBdrjuyZ_k&st5Pya!oL*f(wKlHKhbAWGxcYs%w->-;JUm5J|{Pq4cK>9;a(}-u- zdYydT8``pILm!a83zjZ*fPF-W0`Gfxe^YDF*E#ac(WAd%Y2@bS{<;X+9?JeG z9V9+a4}s-tvNb`^Cg$P3C()0C+yg!zAp+R;8{p>~qbKTjMH%TUF3MXK=xTpMtjR`M zD-W6Y+@G)WcHfpw)!KZB`~%7c$^0ig0T7YqC*)>D3i5I$$-D>PaR63WuQzRu<_t_* zxJ(ri6aab-P~Bjw4CLIqaj*B69=GNw0THkFcOpU0T3O$A5k8c$^^q%J69zeO>1%xd z`B&9_kNX=10KrejJQ@91+lr7U7C4#vyX&_1o;-HAZ}P

Qbki4rKuloiB(oK!Y8W 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≷v%1wrGtfa$Dtt=Qe4 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 SlM`S*E?G%Mi3%};fd3CT{R;^I literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/fill-extrusion-pattern/global-state/style.json b/metrics/integration/render-tests/fill-extrusion-pattern/global-state/style.json new file mode 100644 index 000000000000..d83f07d61f47 --- /dev/null +++ b/metrics/integration/render-tests/fill-extrusion-pattern/global-state/style.json @@ -0,0 +1,176 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "operations": [ + [ + "setGlobalStateProperty", + "icon", + "generic_icon" + ], + [ + "wait" + ] + ], + "allowed": 0.002 + } + }, + "state": { + "icon": { "default": "generic_rail" } + }, + "sources": { + "a": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0003, + -0.0003 + ], + [ + -0.0003, + 0.0003 + ], + [ + 0.0003, + 0.0003 + ], + [ + 0.0003, + -0.0003 + ], + [ + -0.0003, + -0.0003 + ] + ] + ] + } + } + ] + } + }, + "b": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "icon": "generic_metro", + "height": 20 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + 0 + ], + [ + 0, + 0.0002 + ], + [ + 0.0002, + 0 + ], + [ + 0, + -0.0002 + ], + [ + -0.0002, + 0 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "height": 30 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.00008, + -0.00008 + ], + [ + -0.00008, + 0.00008 + ], + [ + 0.00008, + 0.00008 + ], + [ + 0.00008, + -0.00008 + ], + [ + -0.00008, + -0.00008 + ] + ] + ] + } + } + ] + } + } + }, + "sprite": "local://sprites/emerald", + "pitch": 60, + "zoom": 18, + "layers": [ + { + "id": "extrusion-a", + "type": "fill-extrusion", + "source": "a", + "paint": { + "fill-extrusion-pattern": [ + "global-state", + "icon" + ], + "fill-extrusion-height": 10 + } + }, + { + "id": "extrusion-b", + "type": "fill-extrusion", + "source": "b", + "paint": { + "fill-extrusion-pattern": [ + "coalesce", + [ + "get", + "icon" + ], + [ + "global-state", + "icon" + ] + ], + "fill-extrusion-height": [ + "get", + "height" + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/fill-pattern/global-state/expected.png b/metrics/integration/render-tests/fill-pattern/global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..93294624ade190d2d7b28526937d6234f7f39e97 GIT binary patch literal 5675 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 P-!nQISJf-kY$E;!t8F_n literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/fill-pattern/global-state/style.json b/metrics/integration/render-tests/fill-pattern/global-state/style.json new file mode 100644 index 000000000000..572fe4efb118 --- /dev/null +++ b/metrics/integration/render-tests/fill-pattern/global-state/style.json @@ -0,0 +1,167 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 64, + "operations": [ + [ + "setGlobalStateProperty", + "icon", + "zoo_icon" + ], + [ + "wait" + ] + ] + } + }, + "state": { + "icon": { "default": "generic_icon" } + }, + "sources": { + "a": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 20, + -10 + ], + [ + 20, + 10 + ], + [ + 40, + 10 + ], + [ + 40, + -10 + ], + [ + 20, + -10 + ] + ] + ] + } + } + ] + } + }, + "b": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -10, + -10 + ], + [ + -10, + 10 + ], + [ + 10, + 10 + ], + [ + 10, + -10 + ], + [ + -10, + -10 + ] + ] + ] + }, + "properties": { + "icon": "hospital_icon" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -40, + -10 + ], + [ + -40, + 10 + ], + [ + -20, + 10 + ], + [ + -20, + -10 + ], + [ + -40, + -10 + ] + ] + ] + } + } + ] + } + } + }, + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "fill-a", + "type": "fill", + "source": "a", + "paint": { + "fill-antialias": false, + "fill-pattern": [ + "global-state", + "icon" + ] + } + }, + { + "id": "fill-b", + "type": "fill", + "source": "b", + "paint": { + "fill-antialias": false, + "fill-pattern": [ + "coalesce", + [ + "get", + "icon" + ], + [ + "global-state", + "icon" + ] + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/filter/global-state/expected.png b/metrics/integration/render-tests/filter/global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..d2563476b269bb59092a076835fea6f293a4fed3 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Or9=|Ar*{or0OmKd3Qm)w+9z` zIVUo(UD#Zosvsfe!*+PmRW*)liX2Vv6~g3Q*6eFm=*Yde=3v5vqm3I9gu__m6AqQV zkNuH&-%<$)Y;!&TjK}a8N3oAV=+1fX%0Bc2pH8zlyxZ$*>E?F*owKHJhqP@;%zZeG0Ogb(4wl?FLIQLY|Tf48GSbZ|=ooes?dBAtvVna@w@SYVRaBD>ckwEDWW*I{nd9UT{?|~j= N@O1TaS?83{1ONcEaZdmM literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/filter/global-state/style.json b/metrics/integration/render-tests/filter/global-state/style.json new file mode 100644 index 000000000000..01841499e344 --- /dev/null +++ b/metrics/integration/render-tests/filter/global-state/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "state": { + "position": { "default": "left" } + }, + "sources": { + "circles": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "position": "right" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 8, + 0 + ] + } + }, + { + "type": "Feature", + "properties": { + "position": "left" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -8, + 0 + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "circles", + "filter": ["==", ["get" ,"position"], ["global-state", "position"]], + "type": "circle", + "source": "circles", + "paint": { + "circle-radius": 7 + } + } + ] +} diff --git a/metrics/integration/render-tests/icon-size/global-state/expected.png b/metrics/integration/render-tests/icon-size/global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f239f85ed6e33bf38b204c16eb9e8f7547f0d1 GIT binary patch literal 3302 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 ev%zSI!2T>pfi@Ln;{G9&8LeYQW=iuw1I# zmHXAV-K-hjR~X73rIhTRF%t~FDx0l3^N4L$aE_AM$}^ALfD8c>aT9g36=xnXoYfR# zJTLD)gZ2?tDnm{r-UW|IUX-H literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/light/intensity/global-state/style.json b/metrics/integration/render-tests/light/intensity/global-state/style.json new file mode 100644 index 000000000000..4b5f643f9fad --- /dev/null +++ b/metrics/integration/render-tests/light/intensity/global-state/style.json @@ -0,0 +1,75 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "state": { + "lightIntensity": { + "default": 1 + } + }, + "light": { + "intensity": [ + "global-state", + "lightIntensity" + ] + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0003, + -0.0003 + ], + [ + -0.0003, + 0.0003 + ], + [ + 0.0003, + 0.0003 + ], + [ + 0.0003, + -0.0003 + ], + [ + -0.0003, + -0.0003 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 60, + "bearing": 90, + "zoom": 15, + "layers": [ + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 30, + "fill-extrusion-color": "#ffffff" + } + } + ] +} diff --git a/metrics/integration/render-tests/light/position/global-state/expected.png b/metrics/integration/render-tests/light/position/global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..48752f119679d5ef455ea89e00fd95641e2ec69c GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=(>z@qLn;{G9z2+P#DIt4pz1`e zo89s6T4Q&LEp7L(h(9pbagvJXq$QP2&AX~UGKPxE2+3T!+s(gf)q{qV9Vt5=G`x%3 zvtU(|@5zFb1qYV_IV)B*@q#&R@8W72S8@H*kbKw9d;ar>`>KbLA?xgSzvK42*tIUf l*(e4Cl{`T<0g=fO{c{tgj!hLvl>)ku!PC{xWt~$(699lNR-^y` literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/light/position/global-state/style.json b/metrics/integration/render-tests/light/position/global-state/style.json new file mode 100644 index 000000000000..a61c87674507 --- /dev/null +++ b/metrics/integration/render-tests/light/position/global-state/style.json @@ -0,0 +1,79 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "state": { + "lightPosition": { + "default": [ + 1.5, + 90, + 80 + ] + } + }, + "light": { + "position": [ + "global-state", + "lightPosition" + ] + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0003, + -0.0003 + ], + [ + -0.0003, + 0.0003 + ], + [ + 0.0003, + 0.0003 + ], + [ + 0.0003, + -0.0003 + ], + [ + -0.0003, + -0.0003 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 60, + "bearing": 90, + "zoom": 15, + "layers": [ + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 30, + "fill-extrusion-color": "#ffffff" + } + } + ] +} diff --git a/metrics/integration/render-tests/line-cap/global-state/expected.png b/metrics/integration/render-tests/line-cap/global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..dd9d888fa7f8850f7451be8a2d8b69b171fa3bd9 GIT binary patch literal 70398 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�mhD?=C&BRcVEo2$thFwPpFFTcdX<}!dAx4M9V8% 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 literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/line-color/global-state-with-get/style.json b/metrics/integration/render-tests/line-color/global-state-with-get/style.json new file mode 100644 index 000000000000..09d63727cf0b --- /dev/null +++ b/metrics/integration/render-tests/line-color/global-state-with-get/style.json @@ -0,0 +1,98 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "state": { + "color": { + "default": "cyan" + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -10, + 10 + ], + [ + 10, + 10 + ], + [ + -10, + -10 + ], + [ + 10, + -10 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "color": "magenta" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -10, + 9 + ], + [ + 10, + 9 + ], + [ + -10, + -9 + ], + [ + 10, + -9 + ] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "geojson", + "paint": { + "line-color": [ + "coalesce", + [ + "get", + "color" + ], + [ + "global-state", + "color" + ], + "yellow" + ], + "line-width": 2 + } + } + ] +} diff --git a/metrics/integration/render-tests/line-color/global-state/expected.png b/metrics/integration/render-tests/line-color/global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..735bd77f52b6ae56c8d68b5bdd9668c5b8387e58 GIT binary patch literal 663 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 literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/line-pattern/global-state/style.json b/metrics/integration/render-tests/line-pattern/global-state/style.json new file mode 100644 index 000000000000..e367e07f520b --- /dev/null +++ b/metrics/integration/render-tests/line-pattern/global-state/style.json @@ -0,0 +1,133 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 128, + "operations": [ + [ + "setGlobalStateProperty", + "icon", + "generic_icon" + ], + [ + "wait" + ] + ] + } + }, + "state": { + "icon": { "default": "generic_rail" } + }, + "sources": { + "a": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 10, + -10 + ], + [ + -10, + -10 + ] + ] + } + } + ] + } + }, + "b": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "icon": "generic_metro" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -10, + 0 + ], + [ + 10, + 0 + ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -10, + 10 + ], + [ + 10, + 10 + ] + ] + } + } + ] + } + } + }, + "zoom": 2, + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road-a", + "type": "line", + "source": "a", + "paint": { + "line-width": 10, + "line-pattern": [ + "global-state", + "icon" + ] + } + }, + { + "id": "road-b", + "type": "line", + "source": "b", + "paint": { + "line-width": 10, + "line-pattern": [ + "coalesce", + [ + "get", + "icon" + ], + [ + "global-state", + "icon" + ] + ] + } + } + ] +} diff --git a/metrics/integration/render-tests/line-round-limit/global-state/expected.png b/metrics/integration/render-tests/line-round-limit/global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..2edc29b93ee21c0c9ea13f7e2e96d03a99cc2eab GIT binary patch literal 5013 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 SMgH9Y0000|DMzhU{SM4|(6c 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 GlK%$mCGqzF literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/text-field/global-state/style.json b/metrics/integration/render-tests/text-field/global-state/style.json new file mode 100644 index 000000000000..87305c42f242 --- /dev/null +++ b/metrics/integration/render-tests/text-field/global-state/style.json @@ -0,0 +1,56 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "state": { + "prefix": { "default": "pre_"}, + "suffix": { "default": "_suf" } + }, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "Napoli" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "concat", + ["global-state", "prefix"], + ["get", "name"], + ["global-state", "suffix"] + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 32 + } + } + ] +} diff --git a/metrics/integration/render-tests/text-size/global-state/expected.png b/metrics/integration/render-tests/text-size/global-state/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..aba8e806a615e6a30e53efcfd0cf6f01f88f5791 GIT binary patch literal 992 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

I&!2D)G4XRw>jkk1$e5vyFYm)m|7(bSz~Ix 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 literal 0 HcmV?d00001 diff --git a/metrics/integration/render-tests/visibility/global-state/style.json b/metrics/integration/render-tests/visibility/global-state/style.json new file mode 100644 index 000000000000..f8c6ad2d5eff --- /dev/null +++ b/metrics/integration/render-tests/visibility/global-state/style.json @@ -0,0 +1,47 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "state": { + "vis": { "default": "visible" } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14, + "bearing": 90, + "sources": { + "maplibre": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line-visible", + "type": "line", + "source": "maplibre", + "source-layer": "road", + "layout": { + "visibility": ["global-state", "vis"] + }, + "paint": { + "line-opacity": 1 + } + } + ] +} From 5b07b3b7a7f87a7487c6be260af471a87e2b090a Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Wed, 26 Aug 2026 11:33:26 +0900 Subject: [PATCH 23/32] fix(core): defer visibility evaluation until global state is available --- src/mln/style/layer.cpp | 14 +++++++------- test/style/style.test.cpp | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/mln/style/layer.cpp b/src/mln/style/layer.cpp index 7c7789b656ba..9724b8692b72 100644 --- a/src/mln/style/layer.cpp +++ b/src/mln/style/layer.cpp @@ -97,9 +97,9 @@ void Layer::setVisibility(VisibilityType value) { namespace { VisibilityType evaluateVisibilityExpression(const expression::Expression& expression, - const GlobalStateMap* globalState) { + const GlobalStateMap& globalState) { const expression::EvaluationResult result = expression.evaluate( - expression::EvaluationContext().withGlobalState(globalState)); + expression::EvaluationContext().withGlobalState(&globalState)); if (result) { if (const auto string = expression::fromExpressionValue(*result)) { if (const auto visibility = Enum::toEnum(*string)) { @@ -120,7 +120,7 @@ Value Layer::getVisibilityExpression() const { void Layer::reevaluateVisibility(const GlobalStateMap& globalState) { if (!baseImpl->visibilityExpression) return; - const VisibilityType value = evaluateVisibilityExpression(*baseImpl->visibilityExpression, &globalState); + const VisibilityType value = evaluateVisibilityExpression(*baseImpl->visibilityExpression, globalState); if (value == baseImpl->visibility) return; auto impl_ = mutableBaseImpl(); impl_->visibility = value; @@ -276,11 +276,11 @@ std::optional Layer::setVisibility(const conversion::Converti return Error{"visibility expressions may only use the \"global-state\" expression"}; } - std::shared_ptr expression = std::move(*parsed); - const VisibilityType visibility = evaluateVisibilityExpression(*expression, nullptr); auto impl_ = mutableBaseImpl(); - impl_->visibilityExpression = std::move(expression); - impl_->visibility = visibility; + // The layer may not belong to a style yet, so its global state is not + // available here. The style reevaluates the expression when the layer + // is added or when this change reaches its observer. + impl_->visibilityExpression = std::move(*parsed); baseImpl = std::move(impl_); observer->onLayerChanged(*this); return std::nullopt; diff --git a/test/style/style.test.cpp b/test/style/style.test.cpp index 2d63fbc84b51..9528d4d23131 100644 --- a/test/style/style.test.cpp +++ b/test/style/style.test.cpp @@ -148,6 +148,7 @@ TEST(Style, GlobalStateNumericEquality) { TEST(Style, GlobalStateVisibility) { util::RunLoop loop; + FixtureLog log; auto fileSource = std::make_shared(); Style::Impl style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; @@ -167,6 +168,9 @@ TEST(Style, GlobalStateVisibility) { Layer* layer = style.getLayer("background"); ASSERT_TRUE(layer); + // Parsing the layer happens before it has access to the style's global + // state, but a valid expression must not emit a spurious warning. + EXPECT_TRUE(log.empty()) << log.unchecked(); // The default from the root "state" property applies. EXPECT_EQ(VisibilityType::Visible, layer->getVisibility()); @@ -185,6 +189,7 @@ TEST(Style, GlobalStateVisibility) { TEST(Style, VisibilityExpressionSetAtRuntime) { util::RunLoop loop; + FixtureLog log; auto fileSource = std::make_shared(); Style::Impl style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; @@ -220,6 +225,7 @@ TEST(Style, VisibilityExpressionSetAtRuntime) { error = layer->setProperty("visibility", conversion::Convertible(json)); EXPECT_FALSE(error); EXPECT_EQ(VisibilityType::None, layer->getVisibility()); + EXPECT_TRUE(log.empty()) << log.unchecked(); } TEST(Style, VisibilityExpressionRejectsOtherDependencies) { From 89dcf1ea9fe8d272598cca74d555847e25106376 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Wed, 26 Aug 2026 14:23:38 +0900 Subject: [PATCH 24/32] fix(core): align global-state null semantics with gl-js --- src/mln/style/parser.cpp | 7 +++---- src/mln/style/style_impl.cpp | 5 +---- test/style/style.test.cpp | 19 ++++++++++++++----- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/mln/style/parser.cpp b/src/mln/style/parser.cpp index dd9f664f6a63..5a13bb7ee30b 100644 --- a/src/mln/style/parser.cpp +++ b/src/mln/style/parser.cpp @@ -292,11 +292,10 @@ void Parser::parseState(const JSValue& value) { Log::Warning(Event::ParseStyle, "state property \"" + propertyName + "\" must be an object"); continue; } - mln::Value defaultValue; - if (member.value.HasMember("default")) { - defaultValue = parseStateValue(member.value["default"]); + if (!member.value.HasMember("default")) { + continue; } - globalStateDefaults.emplace(std::move(propertyName), std::move(defaultValue)); + globalStateDefaults.emplace(std::move(propertyName), parseStateValue(member.value["default"])); } } diff --git a/src/mln/style/style_impl.cpp b/src/mln/style/style_impl.cpp index 521b0d0644c3..5c9634227e40 100644 --- a/src/mln/style/style_impl.cpp +++ b/src/mln/style/style_impl.cpp @@ -213,10 +213,7 @@ void Style::Impl::setGlobalStateProperty(const std::string& property, const Valu } const auto current = globalState->find(property); - // A missing property already evaluates to null, so setting an absent - // property to null is a no-op as well. - const bool unchanged = current != globalState->end() ? stateValuesEqual(current->second, *newValue) - : newValue->is(); + const bool unchanged = current != globalState->end() && stateValuesEqual(current->second, *newValue); if (unchanged) { return; } diff --git a/test/style/style.test.cpp b/test/style/style.test.cpp index 9528d4d23131..5f33304eeb20 100644 --- a/test/style/style.test.cpp +++ b/test/style/style.test.cpp @@ -92,10 +92,10 @@ TEST(Style, GlobalState) { })STYLE"); auto state = style.getGlobalState(); - ASSERT_EQ(3u, state.size()); + ASSERT_EQ(2u, state.size()); EXPECT_EQ(Value(true), state.at("showLabels")); EXPECT_EQ(Value(mapbox::base::ValueArray({Value("restaurant"), Value("hotel")})), state.at("categories")); - EXPECT_EQ(Value(NullValue()), state.at("noDefault")); + EXPECT_EQ(state.end(), state.find("noDefault")); // Setting a property updates the state. style.setGlobalStateProperty("showLabels", false); @@ -117,10 +117,19 @@ TEST(Style, GlobalState) { style.loadJSON(R"STYLE({"version": 8, "sources": {}, "layers": []})STYLE"); EXPECT_TRUE(style.getGlobalState().empty()); - // Setting an absent property to null is a no-op: a missing property - // already evaluates to null. + // As in GL JS, setting an absent property to null creates an observable + // null-valued entry even though both forms evaluate to null in expressions. + const auto beforeNull = style.getGlobalStateShared(); style.setGlobalStateProperty("neverSet", NullValue()); - EXPECT_TRUE(style.getGlobalState().empty()); + EXPECT_NE(beforeNull, style.getGlobalStateShared()); + state = style.getGlobalState(); + ASSERT_EQ(1u, state.size()); + EXPECT_EQ(Value(NullValue()), state.at("neverSet")); + + // Repeating the same null assignment remains a no-op. + const auto afterNull = style.getGlobalStateShared(); + style.setGlobalStateProperty("neverSet", NullValue()); + EXPECT_EQ(afterNull, style.getGlobalStateShared()); } TEST(Style, GlobalStateNumericEquality) { From a52a206b56ff42a1d089414a31fee6652b3e63c8 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Wed, 26 Aug 2026 17:28:51 +0900 Subject: [PATCH 25/32] fix(android): allow snapshotter style access after style load --- .../src/cpp/snapshotter/map_snapshotter.cpp | 4 +-- .../src/cpp/snapshotter/map_snapshotter.hpp | 2 +- .../android/snapshotter/MapSnapshotter.kt | 18 ++++++------- .../android/snapshotter/MapSnapshotterTest.kt | 19 +++++++++----- .../snapshotter_global_state_style.json | 18 +++++++++++++ src/mln/style/style.cpp | 2 +- test/style/style.test.cpp | 25 +++++++++++++++++++ 7 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 platform/android/MapLibreAndroidTestApp/src/main/assets/snapshotter_global_state_style.json diff --git a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp index 0828c91c2ca2..7e1bdbba1292 100644 --- a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.cpp @@ -319,7 +319,7 @@ jni::Local> MapSnapshotter::getSource(JNIEnv& env, const jni return jni::NewLocal(env, Source::peerForCoreSource(env, *coreSource)); } -jni::jboolean MapSnapshotter::isFullyLoaded(JNIEnv&) { +jni::jboolean MapSnapshotter::isStyleLoaded(JNIEnv&) { return snapshotter->getStyle().isLoaded(); } @@ -378,7 +378,7 @@ void MapSnapshotter::registerNative(jni::JNIEnv& env) { METHOD(&MapSnapshotter::addImages, "nativeAddImages"), METHOD(&MapSnapshotter::getLayer, "nativeGetLayer"), METHOD(&MapSnapshotter::getSource, "nativeGetSource"), - METHOD(&MapSnapshotter::isFullyLoaded, "nativeIsFullyLoaded"), + METHOD(&MapSnapshotter::isStyleLoaded, "nativeIsStyleLoaded"), METHOD(&MapSnapshotter::setGlobalStateProperty, "nativeSetGlobalStateProperty"), METHOD(&MapSnapshotter::getGlobalState, "nativeGetGlobalState"), METHOD(&MapSnapshotter::setStyleJson, "setStyleJson"), diff --git a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp index 596462cbeda6..28302b355bf0 100644 --- a/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/snapshotter/map_snapshotter.hpp @@ -68,7 +68,7 @@ class MapSnapshotter final : public mln::MapSnapshotterObserver { jni::Local> getLayer(JNIEnv&, const jni::String&); jni::Local> getSource(JNIEnv&, const jni::String&); - jni::jboolean isFullyLoaded(JNIEnv&); + jni::jboolean isStyleLoaded(JNIEnv&); void setGlobalStateProperty(JNIEnv&, const jni::String& name, const jni::Object& value); jni::Local> getGlobalState(JNIEnv&); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt index 432efc269a1d..2ada1dd45939 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt @@ -45,8 +45,8 @@ open class MapSnapshotter(context: Context, options: Options) { private val nativePtr: Long = 0 private val context: Context // Not cached: core resets the load state whenever a new style is set. - private val fullyLoaded: Boolean - get() = nativeIsFullyLoaded() + private val styleLoaded: Boolean + get() = nativeIsStyleLoaded() // Sources, layers, and images from Options.builder are applied to the // first loaded style only. @@ -445,7 +445,7 @@ open class MapSnapshotter(context: Context, options: Options) { external fun setStyleJson(styleJson: String?) @Keep - private external fun nativeIsFullyLoaded(): Boolean + private external fun nativeIsStyleLoaded(): Boolean /** * Adds the layer to the map. The layer must be newly created and not added to the snapshotter before @@ -737,7 +737,7 @@ open class MapSnapshotter(context: Context, options: Options) { */ fun getLayer(layerId: String): Layer? { checkThread() - return if (fullyLoaded) nativeGetLayer(layerId) else null + return if (styleLoaded) nativeGetLayer(layerId) else null } /** @@ -748,7 +748,7 @@ open class MapSnapshotter(context: Context, options: Options) { */ fun getSource(sourceId: String): Source? { checkThread() - return if (fullyLoaded) nativeGetSource(sourceId) else null + return if (styleLoaded) nativeGetSource(sourceId) else null } /** @@ -758,7 +758,7 @@ open class MapSnapshotter(context: Context, options: Options) { * Setting a null value (or [com.google.gson.JsonNull]) resets the property to the default * defined in the style's root `state` property, or to null if there is none. * - * Only takes effect once the style is fully loaded; when the style is set by URL, call this + * Only takes effect once the style is loaded; when the style is set by URL, call this * from [Observer.onDidFinishLoadingStyle]. * * @param name the name of the state property @@ -766,7 +766,7 @@ open class MapSnapshotter(context: Context, options: Options) { */ fun setGlobalStateProperty(name: String, value: JsonElement?) { checkThread() - if (fullyLoaded) { + if (styleLoaded) { nativeSetGlobalStateProperty(name, value) } } @@ -776,11 +776,11 @@ open class MapSnapshotter(context: Context, options: Options) { * used by the * [global-state](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression. * - * @return the current global state, or an empty object if the style is not fully loaded yet + * @return the current global state, or an empty object if the style is not loaded yet */ fun getGlobalState(): JsonObject { checkThread() - return if (fullyLoaded) nativeGetGlobalState() else JsonObject() + return if (styleLoaded) nativeGetGlobalState() else JsonObject() } /** diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt index be0485f5b9b3..169c4b3cd8a5 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/snapshotter/MapSnapshotterTest.kt @@ -92,17 +92,24 @@ class MapSnapshotterTest { mapSnapshotter!!.setObserver(object : MapSnapshotter.Observer { override fun onDidFinishLoadingStyle() { - // The new style defines no state. - Assert.assertEquals(0, mapSnapshotter!!.getGlobalState().entrySet().size) + Assert.assertEquals( + JsonPrimitive(true), + mapSnapshotter!!.getGlobalState().get("reloadState") + ) + mapSnapshotter!!.setGlobalStateProperty("reloadState", JsonPrimitive(false)) + Assert.assertEquals( + JsonPrimitive(false), + mapSnapshotter!!.getGlobalState().get("reloadState") + ) countDownLatch.countDown() } override fun onStyleImageMissing(imageName: String) {} }) - mapSnapshotter!!.setStyleUrl("asset://fill_color_style.json") - // While the new style is loading, the previous style's - // state must not be visible. - Assert.assertEquals(0, mapSnapshotter!!.getGlobalState().entrySet().size) + mapSnapshotter!!.setStyleUrl("asset://snapshotter_global_state_style.json") + // The previous style's state must not be visible, whether + // the asset finished loading synchronously or not. + Assert.assertNull(mapSnapshotter!!.getGlobalState().get("showLabels")) }, { Assert.fail(it) diff --git a/platform/android/MapLibreAndroidTestApp/src/main/assets/snapshotter_global_state_style.json b/platform/android/MapLibreAndroidTestApp/src/main/assets/snapshotter_global_state_style.json new file mode 100644 index 000000000000..9e811b4e4530 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/assets/snapshotter_global_state_style.json @@ -0,0 +1,18 @@ +{ + "version": 8, + "state": { + "reloadState": { + "default": true + } + }, + "sources": { + "pending": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [] + } + } + }, + "layers": [] +} diff --git a/src/mln/style/style.cpp b/src/mln/style/style.cpp index 8bee110ff931..9c3193484e2b 100644 --- a/src/mln/style/style.cpp +++ b/src/mln/style/style.cpp @@ -40,7 +40,7 @@ std::string Style::getURL() const { } bool Style::isLoaded() const { - return impl->isLoaded(); + return impl->loaded; } std::string Style::getName() const { diff --git a/test/style/style.test.cpp b/test/style/style.test.cpp index 5f33304eeb20..b387e869bfe3 100644 --- a/test/style/style.test.cpp +++ b/test/style/style.test.cpp @@ -348,6 +348,31 @@ TEST(Style, LoadJSONCancelsPendingLoadURL) { ASSERT_NE("Streets", style.getName()); } +TEST(Style, PublicLoadStateDoesNotWaitForResources) { + util::RunLoop loop; + + auto fileSource = std::make_shared<::StubFileSource>( + ResourceOptions::Default(), ClientOptions(), StubFileSource::ResponseType::Manual); + style::Style style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; + + EXPECT_FALSE(style.isLoaded()); + + style.loadJSON(R"STYLE({ + "version": 8, + "sources": { + "pending": { + "type": "geojson", + "data": { "type": "FeatureCollection", "features": [] } + } + }, + "layers": [] + })STYLE"); + EXPECT_TRUE(style.isLoaded()); + + style.loadURL("http://some-url"); + EXPECT_FALSE(style.isLoaded()); +} + TEST(Style, SourceImplsOrder) { util::RunLoop loop; auto fileSource = std::make_shared(); From a75c0e9d7e57c9d3042fcec26fa39eafe7f2ff34 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Wed, 26 Aug 2026 21:51:40 +0900 Subject: [PATCH 26/32] test(core): add global-state update regression coverage --- test/api/query.test.cpp | 47 +++++++++++++++++++++++++++++++++++++++ test/style/style.test.cpp | 21 +++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/test/api/query.test.cpp b/test/api/query.test.cpp index da9e6f737f90..27a1850f9bf3 100644 --- a/test/api/query.test.cpp +++ b/test/api/query.test.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -41,6 +43,11 @@ class QueryTest { MapOptions().withMapMode(MapMode::Static).withSize(frontend.getSize())}; }; +Filter parseFilter(const std::string& expression) { + conversion::Error error; + return *conversion::convertJSON(expression, error); +} + std::vector getTopClusterFeature(QueryTest& test) { test.fileSource->sourceResponse = [&](const Resource& resource) { EXPECT_EQ("http://url"s, resource.url); @@ -132,6 +139,46 @@ TEST(Query, QueryRenderedFeaturesFilter) { EXPECT_EQ(features3.size(), 1u); } +TEST(Query, GlobalStateChangeRelayoutsSourceFilter) { + QueryTest test; + Layer* layer = test.map.getStyle().getLayer("layer4"); + ASSERT_NE(layer, nullptr); + layer->setFilter(parseFilter(R"(["==", ["get", "key1"], ["global-state", "filterValue"]])")); + + const auto point = test.map.pixelForLatLng({0, 0}); + const RenderedQueryOptions options{{{"layer4"}}, {}}; + + test.map.getStyle().setGlobalStateProperty("filterValue", "value1"); + test.frontend.render(test.map); + EXPECT_EQ(1u, test.frontend.getRenderer()->queryRenderedFeatures(point, options).size()); + + test.map.getStyle().setGlobalStateProperty("filterValue", "other"); + test.frontend.render(test.map); + EXPECT_TRUE(test.frontend.getRenderer()->queryRenderedFeatures(point, options).empty()); + + test.map.getStyle().setGlobalStateProperty("filterValue", "value1"); + test.frontend.render(test.map); + EXPECT_EQ(1u, test.frontend.getRenderer()->queryRenderedFeatures(point, options).size()); +} + +TEST(Query, QueryFiltersUseGlobalState) { + QueryTest test; + const auto filter = parseFilter(R"(["==", ["get", "key1"], ["global-state", "queryValue"]])"); + const auto point = test.map.pixelForLatLng({0, 0}); + const RenderedQueryOptions renderedOptions{{{"layer4"}}, {filter}}; + const SourceQueryOptions sourceOptions{{}, {filter}}; + + test.map.getStyle().setGlobalStateProperty("queryValue", "value1"); + test.frontend.render(test.map); + EXPECT_EQ(1u, test.frontend.getRenderer()->queryRenderedFeatures(point, renderedOptions).size()); + EXPECT_EQ(1u, test.frontend.getRenderer()->querySourceFeatures("source4", sourceOptions).size()); + + test.map.getStyle().setGlobalStateProperty("queryValue", "other"); + test.frontend.render(test.map); + EXPECT_TRUE(test.frontend.getRenderer()->queryRenderedFeatures(point, renderedOptions).empty()); + EXPECT_TRUE(test.frontend.getRenderer()->querySourceFeatures("source4", sourceOptions).empty()); +} + TEST(Query, QuerySourceFeatures) { QueryTest test; diff --git a/test/style/style.test.cpp b/test/style/style.test.cpp index b387e869bfe3..8d0fb739696c 100644 --- a/test/style/style.test.cpp +++ b/test/style/style.test.cpp @@ -237,6 +237,27 @@ TEST(Style, VisibilityExpressionSetAtRuntime) { EXPECT_TRUE(log.empty()) << log.unchecked(); } +TEST(Style, LayerAddedAfterGlobalStateUpdateUsesCurrentState) { + util::RunLoop loop; + + auto fileSource = std::make_shared(); + Style::Impl style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; + style.loadJSON(R"STYLE({"version": 8, "sources": {}, "layers": []})STYLE"); + style.setGlobalStateProperty("showLayer", false); + + auto layer = std::make_unique("line", "source"); + rapidjson::GenericDocument, rapidjson::CrtAllocator> document; + document.Parse<0>(R"(["case", ["to-boolean", ["global-state", "showLayer"]], "visible", "none"])"); + ASSERT_FALSE(document.HasParseError()); + const JSValue* json = &document; + ASSERT_FALSE(layer->setProperty("visibility", conversion::Convertible(json))); + EXPECT_EQ(VisibilityType::Visible, layer->getVisibility()); + + Layer* addedLayer = style.addLayer(std::move(layer)); + ASSERT_NE(addedLayer, nullptr); + EXPECT_EQ(VisibilityType::None, addedLayer->getVisibility()); +} + TEST(Style, VisibilityExpressionRejectsOtherDependencies) { util::RunLoop loop; From 9441917d3f6bf61036dc4db2d4ef276dcd82d598 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Sun, 30 Aug 2026 16:08:05 +0900 Subject: [PATCH 27/32] test(darwin): ignore fill-extrusion-pattern/global-state test --- metrics/ignores/platform-macos-webgpu.json | 1 + vendor/expected-lite | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/metrics/ignores/platform-macos-webgpu.json b/metrics/ignores/platform-macos-webgpu.json index a14c4a9cc82f..584836c73823 100644 --- a/metrics/ignores/platform-macos-webgpu.json +++ b/metrics/ignores/platform-macos-webgpu.json @@ -9,6 +9,7 @@ "render-tests/fill-extrusion-pattern/feature-expression": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/function": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/function-2": "https://github.com/mapbox/mapbox-gl-js/issues/3327", + "render-tests/fill-extrusion-pattern/global-state": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/literal": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/opacity": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/tile-buffer": "https://github.com/mapbox/mapbox-gl-js/issues/3327" diff --git a/vendor/expected-lite b/vendor/expected-lite index 3583e95bad46..95b9cb015fa1 160000 --- a/vendor/expected-lite +++ b/vendor/expected-lite @@ -1 +1 @@ -Subproject commit 3583e95bad46f5af7d8a826632bc68a6f7281551 +Subproject commit 95b9cb015fa17baa749c2b396b335906e1596a9e From 86ab6e6eeb66d40fa09594fb9166c88f89cad8fb Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Sun, 6 Sep 2026 23:37:52 +0900 Subject: [PATCH 28/32] refactor(core): pass global state through Tile::setLayers --- src/mln/renderer/tile_pyramid.cpp | 6 ++---- src/mln/tile/geometry_tile.cpp | 10 +++------- src/mln/tile/geometry_tile.hpp | 7 ++----- src/mln/tile/tile.hpp | 8 ++------ test/tile/custom_geometry_tile.test.cpp | 2 +- test/tile/geojson_tile.test.cpp | 6 +++--- 6 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/mln/renderer/tile_pyramid.cpp b/src/mln/renderer/tile_pyramid.cpp index 67319431ea28..6695ebbd60b6 100644 --- a/src/mln/renderer/tile_pyramid.cpp +++ b/src/mln/renderer/tile_pyramid.cpp @@ -164,8 +164,7 @@ void TilePyramid::update(const std::vector>& l } if (needsRelayout) { - tile.setGlobalState(parameters.globalState); - tile.setLayers(layers); + tile.setLayers(layers, parameters.globalState); } }; auto getTileFn = [&](const OverscaledTileID& tileID) -> Tile* { @@ -191,8 +190,7 @@ void TilePyramid::update(const std::vector>& l if (!tile) { tile = createTile(tileID, observer); if (!tile) return nullptr; - tile->setGlobalState(parameters.globalState); - tile->setLayers(layers); + tile->setLayers(layers, parameters.globalState); } return tiles.emplace(tileID, std::move(tile)).first->second.get(); diff --git a/src/mln/tile/geometry_tile.cpp b/src/mln/tile/geometry_tile.cpp index 82a5c393c466..0aab2f0b96cc 100644 --- a/src/mln/tile/geometry_tile.cpp +++ b/src/mln/tile/geometry_tile.cpp @@ -192,7 +192,6 @@ GeometryTile::GeometryTile(const OverscaledTileID& id_, TileObserver* observer_) : Tile(Kind::Geometry, id_, std::move(sourceID_), observer_), ImageRequestor(parameters.imageManager), - globalState(parameters.globalState), threadPool(parameters.threadPool), mailbox(std::make_shared(*Scheduler::GetCurrent())), worker(parameters.isUpdateSynchronous, @@ -294,7 +293,8 @@ std::unique_ptr GeometryTile::createRenderData() { return std::make_unique(layoutResult, atlasTextures); } -void GeometryTile::setLayers(const std::vector>& layers) { +void GeometryTile::setLayers(const std::vector>& layers, + std::shared_ptr globalState) { MLN_TRACE_FUNC(); // Mark the tile as pending again if it was complete before to prevent @@ -325,15 +325,11 @@ void GeometryTile::setLayers(const std::vector>& laye ++correlationID; worker.self().invoke(&GeometryTileWorker::setLayers, std::move(impls), - globalState, + std::move(globalState), imageManager->getAvailableImages(), correlationID); } -void GeometryTile::setGlobalState(const std::shared_ptr& globalState_) { - globalState = globalState_; -} - void GeometryTile::setShowCollisionBoxes(const bool showCollisionBoxes_) { MLN_TRACE_FUNC(); diff --git a/src/mln/tile/geometry_tile.hpp b/src/mln/tile/geometry_tile.hpp index 1c04ae0c8285..5e17b73843e2 100644 --- a/src/mln/tile/geometry_tile.hpp +++ b/src/mln/tile/geometry_tile.hpp @@ -41,8 +41,8 @@ class GeometryTile : public Tile, public GlyphRequestor, public ImageRequestor { void reset(); std::unique_ptr createRenderData() override; - void setLayers(const std::vector>&) override; - void setGlobalState(const std::shared_ptr&) override; + void setLayers(const std::vector>&, + std::shared_ptr) override; void setShowCollisionBoxes(bool showCollisionBoxes) override; void onGlyphsAvailable(GlyphMap, HBShapeRequests) override; @@ -111,9 +111,6 @@ class GeometryTile : public Tile, public GlyphRequestor, public ImageRequestor { // Used to signal the worker that it should abandon parsing this tile as soon as possible. std::atomic obsolete{false}; - // The style's global state, forwarded to the worker for use during layout. - std::shared_ptr globalState; - private: void markObsolete(); diff --git a/src/mln/tile/tile.hpp b/src/mln/tile/tile.hpp index 6f7337d7e7d8..5c02ce57aca3 100644 --- a/src/mln/tile/tile.hpp +++ b/src/mln/tile/tile.hpp @@ -80,14 +80,10 @@ class Tile : public TileLoaderObserver { // tile (and i.e. it was successfully updated); returns `false` otherwise. virtual bool layerPropertiesUpdated(const Immutable& layerProperties) = 0; virtual void setShowCollisionBoxes(const bool) {} - virtual void setLayers(const std::vector>&) {} + virtual void setLayers(const std::vector>&, + std::shared_ptr) {} virtual void setMask(TileMask&&) {} - // Notifies this tile of the style's current global state, used to - // evaluate "global-state" expressions during layout. Must be called - // before setLayers() to take effect for the subsequent re-layout. - virtual void setGlobalState(const std::shared_ptr&) {} - virtual void queryRenderedFeatures(std::unordered_map>& result, const GeometryCoordinates& queryGeometry, const TransformState&, diff --git a/test/tile/custom_geometry_tile.test.cpp b/test/tile/custom_geometry_tile.test.cpp index 87c264e49406..61060375bdb2 100644 --- a/test/tile/custom_geometry_tile.test.cpp +++ b/test/tile/custom_geometry_tile.test.cpp @@ -134,7 +134,7 @@ TEST(CustomGeometryTile, InvokeTileChanged) { }; std::vector> layers{layerProperties}; - tile.setLayers(layers); + tile.setLayers(layers, test.tileParameters.globalState); tile.setObserver(&observer); tile.setTileData(features); diff --git a/test/tile/geojson_tile.test.cpp b/test/tile/geojson_tile.test.cpp index 68d985ffa787..a733bb4c246d 100644 --- a/test/tile/geojson_tile.test.cpp +++ b/test/tile/geojson_tile.test.cpp @@ -89,7 +89,7 @@ TEST(GeoJSONTile, SynchronousUpdate) { Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; - tile.setLayers(layers); + tile.setLayers(layers, tileParameters.globalState); ASSERT_TRUE(tile.isComplete()); ASSERT_TRUE(tile.isRenderable()); } @@ -113,7 +113,7 @@ TEST(GeoJSONTile, Issue7648) { }; std::vector> layers{layerProperties}; - tile.setLayers(layers); + tile.setLayers(layers, test.tileParameters.globalState); tile.setObserver(&observer); while (!tile.isComplete()) { @@ -141,7 +141,7 @@ TEST(GeoJSONTile, Issue9927) { Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; - tile.setLayers(layers); + tile.setLayers(layers, test.tileParameters.globalState); while (!tile.isComplete()) { test.loop.runOnce(); From 9cedafcddc9a895035b18c66ab6ef53d11402c91 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 7 Sep 2026 00:00:02 +0900 Subject: [PATCH 29/32] refactor(core): pass global state separately in query paths --- include/mln/renderer/query.hpp | 10 ---------- src/mln/annotation/render_annotation_source.cpp | 6 ++++-- src/mln/annotation/render_annotation_source.hpp | 3 ++- src/mln/geometry/feature_index.cpp | 7 ++++++- src/mln/geometry/feature_index.hpp | 3 +++ src/mln/renderer/render_orchestrator.cpp | 15 +++++---------- src/mln/renderer/render_source.hpp | 3 ++- src/mln/renderer/sources/render_image_source.cpp | 3 ++- src/mln/renderer/sources/render_image_source.hpp | 3 ++- .../renderer/sources/render_raster_dem_source.cpp | 4 +++- .../renderer/sources/render_raster_dem_source.hpp | 3 ++- src/mln/renderer/sources/render_raster_source.cpp | 3 ++- src/mln/renderer/sources/render_raster_source.hpp | 3 ++- src/mln/renderer/sources/render_tile_source.cpp | 9 ++++++--- src/mln/renderer/sources/render_tile_source.hpp | 3 ++- src/mln/renderer/tile_pyramid.cpp | 8 +++++--- src/mln/renderer/tile_pyramid.hpp | 3 ++- src/mln/tile/custom_geometry_tile.cpp | 6 ++++-- src/mln/tile/custom_geometry_tile.hpp | 4 +++- src/mln/tile/geojson_tile.cpp | 6 ++++-- src/mln/tile/geojson_tile.hpp | 4 +++- src/mln/tile/geometry_tile.cpp | 8 ++++++-- src/mln/tile/geometry_tile.hpp | 5 ++++- src/mln/tile/tile.cpp | 3 ++- src/mln/tile/tile.hpp | 5 ++++- test/tile/vector_tile.test.cpp | 2 +- 26 files changed, 81 insertions(+), 51 deletions(-) diff --git a/include/mln/renderer/query.hpp b/include/mln/renderer/query.hpp index f5632067f2a2..fd36b27f972b 100644 --- a/include/mln/renderer/query.hpp +++ b/include/mln/renderer/query.hpp @@ -1,9 +1,7 @@ #pragma once #include -#include -#include #include #include #include @@ -24,10 +22,6 @@ class RenderedQueryOptions { std::optional> layerIDs; std::optional filter; - - /// The style's current global state, injected by the renderer so that - /// `global-state` expressions in the filter can be evaluated. - std::shared_ptr globalState; }; /** @@ -44,10 +38,6 @@ class SourceQueryOptions { std::optional> sourceLayers; std::optional filter; - - /// The style's current global state, injected by the renderer so that - /// `global-state` expressions in the filter can be evaluated. - std::shared_ptr globalState; }; } // namespace mln diff --git a/src/mln/annotation/render_annotation_source.cpp b/src/mln/annotation/render_annotation_source.cpp index 08666e190ad8..71afc70f2b95 100644 --- a/src/mln/annotation/render_annotation_source.cpp +++ b/src/mln/annotation/render_annotation_source.cpp @@ -52,11 +52,13 @@ std::unordered_map> RenderAnnotationSource::qu const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix) const { - return tilePyramid.queryRenderedFeatures(geometry, transformState, layers, options, projMatrix, {}); + return tilePyramid.queryRenderedFeatures(geometry, transformState, layers, options, globalState, projMatrix, {}); } -std::vector RenderAnnotationSource::querySourceFeatures(const SourceQueryOptions&) const { +std::vector RenderAnnotationSource::querySourceFeatures(const SourceQueryOptions&, + const GlobalStateMap*) const { return {}; } diff --git a/src/mln/annotation/render_annotation_source.hpp b/src/mln/annotation/render_annotation_source.hpp index 598e5d06a658..6155eda6d76a 100644 --- a/src/mln/annotation/render_annotation_source.hpp +++ b/src/mln/annotation/render_annotation_source.hpp @@ -20,9 +20,10 @@ class RenderAnnotationSource final : public RenderTileSource { const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix) const final; - std::vector querySourceFeatures(const SourceQueryOptions&) const final; + std::vector querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const final; private: const AnnotationSource::Impl& impl() const; diff --git a/src/mln/geometry/feature_index.cpp b/src/mln/geometry/feature_index.cpp index fc32c8a2d7ae..e76d248438d5 100644 --- a/src/mln/geometry/feature_index.cpp +++ b/src/mln/geometry/feature_index.cpp @@ -153,6 +153,7 @@ void FeatureIndex::query(std::unordered_map>& const double tileSize, const double scale, const RenderedQueryOptions& queryOptions, + const GlobalStateMap* globalState, const UnwrappedTileID& tileID, const std::unordered_map& layers, const float additionalQueryPadding, @@ -183,6 +184,7 @@ void FeatureIndex::query(std::unordered_map>& addFeature(result, indexedFeature, queryOptions, + globalState, tileID.canonical, layers, queryGeometry, @@ -196,6 +198,7 @@ void FeatureIndex::query(std::unordered_map>& std::unordered_map> FeatureIndex::lookupSymbolFeatures( const std::vector& symbolFeatures, const RenderedQueryOptions& queryOptions, + const GlobalStateMap* globalState, const std::unordered_map& layers, const OverscaledTileID& tileID, const FeatureSortOrder& featureSortOrder) const { @@ -234,6 +237,7 @@ std::unordered_map> FeatureIndex::lookupSymbol addFeature(result, symbolFeature, queryOptions, + globalState, tileID.canonical, layers, GeometryCoordinates(), @@ -248,6 +252,7 @@ std::unordered_map> FeatureIndex::lookupSymbol void FeatureIndex::addFeature(std::unordered_map>& result, const RefIndexedSubfeature& indexedFeature, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const CanonicalTileID& tileID, const std::unordered_map& layers, const GeometryCoordinates& queryGeometry, @@ -292,7 +297,7 @@ void FeatureIndex::addFeature(std::unordered_map(tileID.z), geometryTileFeature.get()} - .withGlobalState(options.globalState.get()))) { + .withGlobalState(globalState))) { continue; } diff --git a/src/mln/geometry/feature_index.hpp b/src/mln/geometry/feature_index.hpp index 9c2bdf3d9dc3..7cf4f7ff35d0 100644 --- a/src/mln/geometry/feature_index.hpp +++ b/src/mln/geometry/feature_index.hpp @@ -125,6 +125,7 @@ class FeatureIndex { double tileSize, double scale, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const UnwrappedTileID&, const std::unordered_map&, float additionalQueryPadding, @@ -141,6 +142,7 @@ class FeatureIndex { std::unordered_map> lookupSymbolFeatures( const std::vector& symbolFeatures, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const std::unordered_map& layers, const OverscaledTileID& tileID, const FeatureSortOrder& featureSortOrder) const; @@ -149,6 +151,7 @@ class FeatureIndex { void addFeature(std::unordered_map>& result, const RefIndexedSubfeature&, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const CanonicalTileID&, const std::unordered_map&, const GeometryCoordinates& queryGeometry, diff --git a/src/mln/renderer/render_orchestrator.cpp b/src/mln/renderer/render_orchestrator.cpp index 602f6ba764cf..fd3c2e284b2d 100644 --- a/src/mln/renderer/render_orchestrator.cpp +++ b/src/mln/renderer/render_orchestrator.cpp @@ -670,6 +670,7 @@ void RenderOrchestrator::queryRenderedSymbols(std::unordered_maplookupSymbolFeatures(renderedSymbols[queryData.bucketInstanceId], options, + globalState.get(), crossTileSymbolIndexLayers, queryData.tileID, queryData.featureSortOrder); @@ -683,13 +684,10 @@ void RenderOrchestrator::queryRenderedSymbols(std::unordered_map RenderOrchestrator::queryRenderedFeatures( const ScreenLineString& geometry, - const RenderedQueryOptions& options_, + const RenderedQueryOptions& options, const std::unordered_map& layers) const { MLN_TRACE_FUNC(); - RenderedQueryOptions options = options_; - options.globalState = globalState; - std::unordered_set sourceIDs; std::unordered_map filteredLayers; for (const auto& pair : layers) { @@ -707,7 +705,7 @@ std::vector RenderOrchestrator::queryRenderedFeatures( for (const auto& sourceID : sourceIDs) { if (RenderSource* renderSource = getRenderSource(sourceID)) { auto sourceResults = renderSource->queryRenderedFeatures( - geometry, transformState, filteredLayers, options, projMatrix); + geometry, transformState, filteredLayers, options, globalState.get(), projMatrix); std::ranges::move(sourceResults, std::inserter(resultsByLayer, resultsByLayer.begin())); } } @@ -759,16 +757,13 @@ std::vector RenderOrchestrator::queryShapeAnnotations(const ScreenLineS } std::vector RenderOrchestrator::querySourceFeatures(const std::string& sourceID, - const SourceQueryOptions& options_) const { + const SourceQueryOptions& options) const { MLN_TRACE_FUNC(); const RenderSource* source = getRenderSource(sourceID); if (!source) return {}; - SourceQueryOptions options = options_; - options.globalState = globalState; - - return source->querySourceFeatures(options); + return source->querySourceFeatures(options, globalState.get()); } FeatureExtensionValue RenderOrchestrator::queryFeatureExtensions( diff --git a/src/mln/renderer/render_source.hpp b/src/mln/renderer/render_source.hpp index aae4aefaadc4..c1905d8896c6 100644 --- a/src/mln/renderer/render_source.hpp +++ b/src/mln/renderer/render_source.hpp @@ -85,9 +85,10 @@ class RenderSource : protected TileObserver { const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix) const = 0; - virtual std::vector querySourceFeatures(const SourceQueryOptions&) const = 0; + virtual std::vector querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const = 0; virtual FeatureExtensionValue queryFeatureExtensions(const Feature&, const std::string&, diff --git a/src/mln/renderer/sources/render_image_source.cpp b/src/mln/renderer/sources/render_image_source.cpp index f25a0422b04e..61382e084f8f 100644 --- a/src/mln/renderer/sources/render_image_source.cpp +++ b/src/mln/renderer/sources/render_image_source.cpp @@ -68,11 +68,12 @@ std::unordered_map> RenderImageSource::queryRe const TransformState&, const std::unordered_map&, const RenderedQueryOptions&, + const GlobalStateMap*, const mat4&) const { return std::unordered_map>{}; } -std::vector RenderImageSource::querySourceFeatures(const SourceQueryOptions&) const { +std::vector RenderImageSource::querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const { return {}; } diff --git a/src/mln/renderer/sources/render_image_source.hpp b/src/mln/renderer/sources/render_image_source.hpp index d961717ce007..3367fd7292c7 100644 --- a/src/mln/renderer/sources/render_image_source.hpp +++ b/src/mln/renderer/sources/render_image_source.hpp @@ -53,9 +53,10 @@ class RenderImageSource final : public RenderSource { const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix) const final; - std::vector querySourceFeatures(const SourceQueryOptions&) const final; + std::vector querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const final; void reduceMemoryUse() final {} void dumpDebugLogs() const final; diff --git a/src/mln/renderer/sources/render_raster_dem_source.cpp b/src/mln/renderer/sources/render_raster_dem_source.cpp index ff40de4415d8..c42f7da79e84 100644 --- a/src/mln/renderer/sources/render_raster_dem_source.cpp +++ b/src/mln/renderer/sources/render_raster_dem_source.cpp @@ -113,11 +113,13 @@ std::unordered_map> RenderRasterDEMSource::que const TransformState&, const std::unordered_map&, const RenderedQueryOptions&, + const GlobalStateMap*, const mat4&) const { return std::unordered_map>{}; } -std::vector RenderRasterDEMSource::querySourceFeatures(const SourceQueryOptions&) const { +std::vector RenderRasterDEMSource::querySourceFeatures(const SourceQueryOptions&, + const GlobalStateMap*) const { return {}; } diff --git a/src/mln/renderer/sources/render_raster_dem_source.hpp b/src/mln/renderer/sources/render_raster_dem_source.hpp index 929d7ab1f156..f93723996c61 100644 --- a/src/mln/renderer/sources/render_raster_dem_source.hpp +++ b/src/mln/renderer/sources/render_raster_dem_source.hpp @@ -14,9 +14,10 @@ class RenderRasterDEMSource final : public RenderTileSetSource { const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix) const override; - std::vector querySourceFeatures(const SourceQueryOptions&) const override; + std::vector querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const override; private: // RenderTileSetSource overrides diff --git a/src/mln/renderer/sources/render_raster_source.cpp b/src/mln/renderer/sources/render_raster_source.cpp index 7cdef6f05996..7db99d465482 100644 --- a/src/mln/renderer/sources/render_raster_source.cpp +++ b/src/mln/renderer/sources/render_raster_source.cpp @@ -47,11 +47,12 @@ std::unordered_map> RenderRasterSource::queryR const TransformState&, const std::unordered_map&, const RenderedQueryOptions&, + const GlobalStateMap*, const mat4&) const { return std::unordered_map>{}; } -std::vector RenderRasterSource::querySourceFeatures(const SourceQueryOptions&) const { +std::vector RenderRasterSource::querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const { return {}; } diff --git a/src/mln/renderer/sources/render_raster_source.hpp b/src/mln/renderer/sources/render_raster_source.hpp index a74407d658cb..e03d497a153c 100644 --- a/src/mln/renderer/sources/render_raster_source.hpp +++ b/src/mln/renderer/sources/render_raster_source.hpp @@ -17,9 +17,10 @@ class RenderRasterSource final : public RenderTileSetSource { const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix) const override; - std::vector querySourceFeatures(const SourceQueryOptions&) const override; + std::vector querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const override; // RenderTileSetSource overrides void updateInternal(const Tileset&, diff --git a/src/mln/renderer/sources/render_tile_source.cpp b/src/mln/renderer/sources/render_tile_source.cpp index a10d76b3c7b8..a3856f4085c0 100644 --- a/src/mln/renderer/sources/render_tile_source.cpp +++ b/src/mln/renderer/sources/render_tile_source.cpp @@ -532,12 +532,15 @@ std::unordered_map> RenderTileSource::queryRen const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix) const { - return tilePyramid.queryRenderedFeatures(geometry, transformState, layers, options, projMatrix, featureState); + return tilePyramid.queryRenderedFeatures( + geometry, transformState, layers, options, globalState, projMatrix, featureState); } -std::vector RenderTileSource::querySourceFeatures(const SourceQueryOptions& options) const { - return tilePyramid.querySourceFeatures(options); +std::vector RenderTileSource::querySourceFeatures(const SourceQueryOptions& options, + const GlobalStateMap* globalState) const { + return tilePyramid.querySourceFeatures(options, globalState); } void RenderTileSource::setFeatureState(const std::optional& sourceLayerID, diff --git a/src/mln/renderer/sources/render_tile_source.hpp b/src/mln/renderer/sources/render_tile_source.hpp index 8af09564234f..04f1b047dd11 100644 --- a/src/mln/renderer/sources/render_tile_source.hpp +++ b/src/mln/renderer/sources/render_tile_source.hpp @@ -34,9 +34,10 @@ class RenderTileSource : public RenderSource { const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix) const override; - std::vector querySourceFeatures(const SourceQueryOptions&) const override; + std::vector querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const override; void setFeatureState(const std::optional&, const std::string&, const FeatureState&) override; diff --git a/src/mln/renderer/tile_pyramid.cpp b/src/mln/renderer/tile_pyramid.cpp index 6695ebbd60b6..a45781207b0e 100644 --- a/src/mln/renderer/tile_pyramid.cpp +++ b/src/mln/renderer/tile_pyramid.cpp @@ -344,6 +344,7 @@ std::unordered_map> TilePyramid::queryRendered const TransformState& transformState, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix, const SourceFeatureState& featureState) const { std::unordered_map> result; @@ -399,17 +400,18 @@ std::unordered_map> TilePyramid::queryRendered } tile.queryRenderedFeatures( - result, tileSpaceQueryGeometry, transformState, layers, options, projMatrix, featureState); + result, tileSpaceQueryGeometry, transformState, layers, options, globalState, projMatrix, featureState); } return result; } -std::vector TilePyramid::querySourceFeatures(const SourceQueryOptions& options) const { +std::vector TilePyramid::querySourceFeatures(const SourceQueryOptions& options, + const GlobalStateMap* globalState) const { std::vector result; for (const auto& pair : tiles) { - pair.second->querySourceFeatures(result, options); + pair.second->querySourceFeatures(result, options, globalState); } return result; diff --git a/src/mln/renderer/tile_pyramid.hpp b/src/mln/renderer/tile_pyramid.hpp index 0f0a92362c9a..56eda6aeb0ae 100644 --- a/src/mln/renderer/tile_pyramid.hpp +++ b/src/mln/renderer/tile_pyramid.hpp @@ -55,10 +55,11 @@ class TilePyramid { const TransformState& transformState, const std::unordered_map&, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix, const mln::SourceFeatureState& featureState) const; - std::vector querySourceFeatures(const SourceQueryOptions&) const; + std::vector querySourceFeatures(const SourceQueryOptions&, const GlobalStateMap*) const; void setCacheEnabled(bool); void reduceMemoryUse(); diff --git a/src/mln/tile/custom_geometry_tile.cpp b/src/mln/tile/custom_geometry_tile.cpp index c602293cbf8e..772c3360a368 100644 --- a/src/mln/tile/custom_geometry_tile.cpp +++ b/src/mln/tile/custom_geometry_tile.cpp @@ -69,7 +69,9 @@ void CustomGeometryTile::setNecessity(TileNecessity newNecessity) { } } -void CustomGeometryTile::querySourceFeatures(std::vector& result, const SourceQueryOptions& queryOptions) { +void CustomGeometryTile::querySourceFeatures(std::vector& result, + const SourceQueryOptions& queryOptions, + const GlobalStateMap* globalState) { // Ignore the sourceLayer, there is only one auto layer = getData()->getLayer({}); @@ -81,7 +83,7 @@ void CustomGeometryTile::querySourceFeatures(std::vector& result, const // Apply filter, if any if (queryOptions.filter && !(*queryOptions.filter)(style::expression::EvaluationContext{ static_cast(id.overscaledZ), feature.get()} - .withGlobalState(queryOptions.globalState.get()))) { + .withGlobalState(globalState))) { continue; } diff --git a/src/mln/tile/custom_geometry_tile.hpp b/src/mln/tile/custom_geometry_tile.hpp index 522c86e478fb..649edcfbd3f1 100644 --- a/src/mln/tile/custom_geometry_tile.hpp +++ b/src/mln/tile/custom_geometry_tile.hpp @@ -29,7 +29,9 @@ class CustomGeometryTile : public GeometryTile { void setNecessity(TileNecessity) final; - void querySourceFeatures(std::vector& result, const SourceQueryOptions&) override; + void querySourceFeatures(std::vector& result, + const SourceQueryOptions&, + const GlobalStateMap* globalState) override; private: bool stale = true; diff --git a/src/mln/tile/geojson_tile.cpp b/src/mln/tile/geojson_tile.cpp index 7dd5e8f55b5e..df7459a12662 100644 --- a/src/mln/tile/geojson_tile.cpp +++ b/src/mln/tile/geojson_tile.cpp @@ -36,7 +36,9 @@ void GeoJSONTile::updateData(std::shared_ptr data_, bool nee runSynchronously); } -void GeoJSONTile::querySourceFeatures(std::vector& result, const SourceQueryOptions& options) { +void GeoJSONTile::querySourceFeatures(std::vector& result, + const SourceQueryOptions& options, + const GlobalStateMap* globalState) { MLN_TRACE_FUNC(); // Ignore the sourceLayer, there is only one @@ -49,7 +51,7 @@ void GeoJSONTile::querySourceFeatures(std::vector& result, const Source // Apply filter, if any if (options.filter && !(*options.filter)(style::expression::EvaluationContext{ static_cast(this->id.overscaledZ), feature.get()} - .withGlobalState(options.globalState.get()))) { + .withGlobalState(globalState))) { continue; } diff --git a/src/mln/tile/geojson_tile.hpp b/src/mln/tile/geojson_tile.hpp index 46fa51003089..70fa842e421f 100644 --- a/src/mln/tile/geojson_tile.hpp +++ b/src/mln/tile/geojson_tile.hpp @@ -21,7 +21,9 @@ class GeoJSONTile final : public GeometryTile { void updateData(std::shared_ptr data, bool needsRelayout, bool runSynchronously); - void querySourceFeatures(std::vector& result, const SourceQueryOptions&) override; + void querySourceFeatures(std::vector& result, + const SourceQueryOptions&, + const GlobalStateMap* globalState) override; private: std::shared_ptr data; diff --git a/src/mln/tile/geometry_tile.cpp b/src/mln/tile/geometry_tile.cpp index 0aab2f0b96cc..7f5b3953ee5d 100644 --- a/src/mln/tile/geometry_tile.cpp +++ b/src/mln/tile/geometry_tile.cpp @@ -513,6 +513,7 @@ void GeometryTile::queryRenderedFeatures(std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix, const SourceFeatureState& featureState) { MLN_TRACE_FUNC(); @@ -532,13 +533,16 @@ void GeometryTile::queryRenderedFeatures(std::unordered_map& result, const SourceQueryOptions& options) { +void GeometryTile::querySourceFeatures(std::vector& result, + const SourceQueryOptions& options, + const GlobalStateMap* globalState) { MLN_TRACE_FUNC(); // Data not yet available, or tile is empty @@ -565,7 +569,7 @@ void GeometryTile::querySourceFeatures(std::vector& result, const Sourc // Apply filter, if any if (options.filter && !(*options.filter)(style::expression::EvaluationContext{ static_cast(this->id.overscaledZ), feature.get()} - .withGlobalState(options.globalState.get()))) { + .withGlobalState(globalState))) { continue; } diff --git a/src/mln/tile/geometry_tile.hpp b/src/mln/tile/geometry_tile.hpp index 5e17b73843e2..5c6562f061d9 100644 --- a/src/mln/tile/geometry_tile.hpp +++ b/src/mln/tile/geometry_tile.hpp @@ -58,10 +58,13 @@ class GeometryTile : public Tile, public GlyphRequestor, public ImageRequestor { const TransformState&, const std::unordered_map& layers, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix, const SourceFeatureState& featureState) override; - void querySourceFeatures(std::vector& result, const SourceQueryOptions&) override; + void querySourceFeatures(std::vector& result, + const SourceQueryOptions&, + const GlobalStateMap* globalState) override; float getQueryPadding(const std::unordered_map&) override; diff --git a/src/mln/tile/tile.cpp b/src/mln/tile/tile.cpp index 663483cc528d..8a6fc7e9ad9e 100644 --- a/src/mln/tile/tile.cpp +++ b/src/mln/tile/tile.cpp @@ -56,6 +56,7 @@ void Tile::queryRenderedFeatures(std::unordered_map&, const RenderedQueryOptions&, + const GlobalStateMap*, const mat4&, const SourceFeatureState&) {} @@ -63,7 +64,7 @@ float Tile::getQueryPadding(const std::unordered_map&, const SourceQueryOptions&) {} +void Tile::querySourceFeatures(std::vector&, const SourceQueryOptions&, const GlobalStateMap*) {} void Tile::onTileAction(TileOperation op) { observer->onTileAction(id, sourceID, op); diff --git a/src/mln/tile/tile.hpp b/src/mln/tile/tile.hpp index 5c02ce57aca3..f21ee089d369 100644 --- a/src/mln/tile/tile.hpp +++ b/src/mln/tile/tile.hpp @@ -89,10 +89,13 @@ class Tile : public TileLoaderObserver { const TransformState&, const std::unordered_map&, const RenderedQueryOptions& options, + const GlobalStateMap* globalState, const mat4& projMatrix, const SourceFeatureState& featureState); - virtual void querySourceFeatures(std::vector& result, const SourceQueryOptions&); + virtual void querySourceFeatures(std::vector& result, + const SourceQueryOptions&, + const GlobalStateMap* globalState); virtual float getQueryPadding(const std::unordered_map&); diff --git a/test/tile/vector_tile.test.cpp b/test/tile/vector_tile.test.cpp index 0b00ee8c2639..ecf39a03e4ef 100644 --- a/test/tile/vector_tile.test.cpp +++ b/test/tile/vector_tile.test.cpp @@ -49,7 +49,7 @@ TEST(VectorTile, Issue8542) { // Query before data is set std::vector result; - tile.querySourceFeatures(result, {{{"layer"}}, {}}); + tile.querySourceFeatures(result, {{{"layer"}}, {}}, nullptr); } TEST(VectorTileData, ParseResults) { From b300d1c646919892eb7ba89b5b41409a57f9a484 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 7 Sep 2026 00:45:22 +0900 Subject: [PATCH 30/32] fix(core): reject global-state updates before style load --- CHANGELOG.md | 3 ++- include/mln/style/style.hpp | 1 + .../android/snapshotter/MapSnapshotter.kt | 9 ++++--- platform/node/index.d.ts | 1 + platform/node/test/js/global_state.test.js | 11 +++++++++ src/mln/style/style.cpp | 5 ++++ test/style/style.test.cpp | 24 +++++++++++++++++++ 7 files changed, 48 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f979e06b72cf..abfa3f45c522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### ✨ New features - *...Add new stuff here...* -- [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property in filters, layout properties, and paint properties, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState` ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). +- [core] Add support for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression and the root [`state`](https://maplibre.org/maplibre-style-spec/root/#state) style property in filters, layout properties, and paint properties, including the runtime API `style::Style::setGlobalStateProperty` / `style::Style::getGlobalState`. Calls to `setGlobalStateProperty` before the style has loaded are rejected, matching MapLibre GL JS ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). +- [core] Add `style::Style::isLoaded` to report whether style JSON has finished loading and parsing without waiting for source or sprite resources ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [android] Add `Style#setGlobalStateProperty` / `Style#getGlobalState` (also available on `MapSnapshotter`) and the `Expression.globalState` builder for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [ios] Add `MLNStyle.globalState` / `-[MLNStyle setGlobalStateValue:forProperty:]` and `NSExpression` support (`mgl_globalState:`) for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). - [node] Add `map.setGlobalStateProperty` / `map.getGlobalState` for the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression ([#3302](https://github.com/maplibre/maplibre-native/issues/3302)). diff --git a/include/mln/style/style.hpp b/include/mln/style/style.hpp index 76206b2a24ae..9a24fdab6e2d 100644 --- a/include/mln/style/style.hpp +++ b/include/mln/style/style.hpp @@ -54,6 +54,7 @@ class Style { // Setting a null value resets the property to the default defined in the // style's root "state" property (or null if there is none). GlobalStateMap getGlobalState() const; + /// Throws if the style has not finished loading. void setGlobalStateProperty(const std::string& name, const Value& value); // Images diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt index 2ada1dd45939..83c9c4fa3753 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/snapshotter/MapSnapshotter.kt @@ -758,17 +758,16 @@ open class MapSnapshotter(context: Context, options: Options) { * Setting a null value (or [com.google.gson.JsonNull]) resets the property to the default * defined in the style's root `state` property, or to null if there is none. * - * Only takes effect once the style is loaded; when the style is set by URL, call this - * from [Observer.onDidFinishLoadingStyle]. + * When the style is set by URL, call this from [Observer.onDidFinishLoadingStyle]. * * @param name the name of the state property * @param value the new value of the state property + * @throws IllegalStateException if the style has not finished loading */ fun setGlobalStateProperty(name: String, value: JsonElement?) { checkThread() - if (styleLoaded) { - nativeSetGlobalStateProperty(name, value) - } + check(styleLoaded) { "Style is not done loading." } + nativeSetGlobalStateProperty(name, value) } /** diff --git a/platform/node/index.d.ts b/platform/node/index.d.ts index 348eee862db8..304b8dcabd0a 100644 --- a/platform/node/index.d.ts +++ b/platform/node/index.d.ts @@ -184,6 +184,7 @@ declare module '@maplibre/maplibre-gl-native' { * Set a property of the map's global state, used by the `global-state` expression. * A `null` value resets the property to the default defined in the style's * root `state` property, or to null if there is none. + * @throws If the style has not finished loading. */ setGlobalStateProperty: (name: string, value: unknown) => void; diff --git a/platform/node/test/js/global_state.test.js b/platform/node/test/js/global_state.test.js index 3d2c90fdeabb..b8f77100ad33 100644 --- a/platform/node/test/js/global_state.test.js +++ b/platform/node/test/js/global_state.test.js @@ -14,6 +14,17 @@ var style = { }; test('Map#globalState', function(t) { + t.test('rejects updates before a style is loaded', function(t) { + var map = new mbgl.Map({request: function() {}}); + + t.throws(function() { + map.setGlobalStateProperty('showLabels', false); + }, /Style is not done loading/); + + map.release(); + t.end(); + }); + t.test('applies defaults from the style root state property', function(t) { var map = new mbgl.Map({request: function() {}}); map.load(style); diff --git a/src/mln/style/style.cpp b/src/mln/style/style.cpp index 9c3193484e2b..d14204c1669d 100644 --- a/src/mln/style/style.cpp +++ b/src/mln/style/style.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace mln { @@ -80,6 +81,10 @@ GlobalStateMap Style::getGlobalState() const { } void Style::setGlobalStateProperty(const std::string& name, const Value& value) { + if (!impl->loaded) { + throw util::MisuseException("Style is not done loading."); + } + impl->mutated = true; impl->setGlobalStateProperty(name, value); } diff --git a/test/style/style.test.cpp b/test/style/style.test.cpp index 8d0fb739696c..374109a281f0 100644 --- a/test/style/style.test.cpp +++ b/test/style/style.test.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -394,6 +395,29 @@ TEST(Style, PublicLoadStateDoesNotWaitForResources) { EXPECT_FALSE(style.isLoaded()); } +TEST(Style, GlobalStatePropertyRequiresLoadedStyle) { + util::RunLoop loop; + + auto fileSource = std::make_shared<::StubFileSource>(StubFileSource::ResponseType::Synchronous); + style::Style style{fileSource, 1.0, {Scheduler::GetBackground(), {}}}; + + ASSERT_FALSE(style.isLoaded()); + EXPECT_THROW(style.setGlobalStateProperty("showLabels", false), util::MisuseException); + EXPECT_TRUE(style.getGlobalState().empty()); + + style.loadJSON(R"STYLE({ + "version": 8, + "state": {"showLabels": {"default": true}}, + "sources": {}, + "layers": [] + })STYLE"); + + ASSERT_TRUE(style.isLoaded()); + EXPECT_EQ(Value(true), style.getGlobalState().at("showLabels")); + EXPECT_NO_THROW(style.setGlobalStateProperty("showLabels", false)); + EXPECT_EQ(Value(false), style.getGlobalState().at("showLabels")); +} + TEST(Style, SourceImplsOrder) { util::RunLoop loop; auto fileSource = std::make_shared(); From b8969dd17496acf914832931dd79a61e653d05b4 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 7 Sep 2026 00:48:24 +0900 Subject: [PATCH 31/32] chore: restore expected-lite submodule revision --- vendor/expected-lite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/expected-lite b/vendor/expected-lite index 95b9cb015fa1..3583e95bad46 160000 --- a/vendor/expected-lite +++ b/vendor/expected-lite @@ -1 +1 @@ -Subproject commit 95b9cb015fa17baa749c2b396b335906e1596a9e +Subproject commit 3583e95bad46f5af7d8a826632bc68a6f7281551 From 8905d43f6428ab7b0ad78f6f5409d7faa5f54f85 Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Tue, 8 Sep 2026 01:21:07 +0900 Subject: [PATCH 32/32] fix(darwin): convert global-state errors to NSException --- platform/darwin/src/MLNStyle.h | 2 ++ platform/darwin/src/MLNStyle.mm | 6 +++++- platform/darwin/test/MLNMapSnapshotterTests.m | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/platform/darwin/src/MLNStyle.h b/platform/darwin/src/MLNStyle.h index 6d288aaa6789..7d56e477dd05 100644 --- a/platform/darwin/src/MLNStyle.h +++ b/platform/darwin/src/MLNStyle.h @@ -120,6 +120,8 @@ MLN_EXPORT Setting a value of `nil` or `NSNull` resets the property to the default defined in the style's root `state` property, or to null if there is none. + @throws NSInternalInconsistencyException if the style has not finished loading. + @param value The new value of the state property. May be a Foundation instance corresponding to a JSON value: `NSString`, `NSNumber`, `NSArray`, `NSDictionary`, or `NSNull`. diff --git a/platform/darwin/src/MLNStyle.mm b/platform/darwin/src/MLNStyle.mm index 2dcc13c620b4..c32af96fd9dc 100644 --- a/platform/darwin/src/MLNStyle.mm +++ b/platform/darwin/src/MLNStyle.mm @@ -547,7 +547,11 @@ - (MLNImage *)imageForName:(NSString *)name { - (void)setGlobalStateValue:(id)value forProperty:(NSString *)propertyName { MLNLogDebug(@"Setting global state property: %@", propertyName); - self.rawStyle->setGlobalStateProperty(propertyName.UTF8String, MLNValueFromJSONObject(value)); + try { + self.rawStyle->setGlobalStateProperty(propertyName.UTF8String, MLNValueFromJSONObject(value)); + } catch (const std::runtime_error &err) { + [NSException raise:NSInternalInconsistencyException format:@"%s", err.what()]; + } } // MARK: Style transitions diff --git a/platform/darwin/test/MLNMapSnapshotterTests.m b/platform/darwin/test/MLNMapSnapshotterTests.m index 5352220bc228..c96da27f8a08 100644 --- a/platform/darwin/test/MLNMapSnapshotterTests.m +++ b/platform/darwin/test/MLNMapSnapshotterTests.m @@ -74,6 +74,26 @@ - (void)tearDown { [super tearDown]; } +- (void)testSettingGlobalStateBeforeStyleLoadsRaisesException { + NSURL *styleURL = [NSURL URLWithString:@"https://example.invalid/style.json"]; + MLNMapSnapshotOptions *options = [[MLNMapSnapshotOptions alloc] + initWithStyleURL:styleURL + camera:[MLNMapCamera camera] + size:CGSizeMake(64, 64)]; + MLNMapSnapshotter *snapshotter = [[MLNMapSnapshotter alloc] initWithOptions:options]; + + [snapshotter startWithCompletionHandler:^(__unused MLNMapSnapshot *snapshot, + __unused NSError *error) { + }]; + + MLNStyle *style = snapshotter.style; + XCTAssertNotNil(style); + XCTAssertThrowsSpecificNamed([style setGlobalStateValue:@YES forProperty:@"showLabels"], + NSException, NSInternalInconsistencyException); + + [snapshotter cancel]; +} + - (void)testOverlayHandler { XCTSkip(@"Snapshotter not implemented yet for Metal. See https://github.com/maplibre/maplibre-native/issues/1862"); self.styleLoadingExpectation = [self expectationWithDescription:@"Style should finish loading."];