Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b7d07e6
Add support for the global-state expression and state style property
NEKOYASAN Aug 23, 2026
738b3b7
perf(core): track global-state refs per key to limit re-evaluation
NEKOYASAN Aug 23, 2026
a1d9a17
refactor(core): remove redundant comments from global-state
NEKOYASAN Aug 23, 2026
0d1c9cc
feat(android): add global-state runtime API and expression builder
NEKOYASAN Aug 24, 2026
2a3353a
feat(ios): add global-state runtime API and NSExpression support
NEKOYASAN Aug 24, 2026
89e3090
docs: remake changelog texts and document mgl_globalState
NEKOYASAN Aug 24, 2026
fa43402
feat(android): add global-state API to MapSnapshotter
NEKOYASAN Aug 24, 2026
e0c2812
feat(node): add global-state runtime API
NEKOYASAN Aug 24, 2026
d818194
fix(core): evaluate runtime-set visibility expressions against current
NEKOYASAN Aug 24, 2026
8b4fb76
fix(core): keep global-state dependency through format section overrides
NEKOYASAN Aug 24, 2026
9283658
fix(android): query the style's load state instead of tracking it in
NEKOYASAN Aug 24, 2026
a7a0546
feat(android): allow setting layer visibility as an expression
NEKOYASAN Aug 24, 2026
da94ac5
feat(ios): allow setting layer visibility as an expression
NEKOYASAN Aug 24, 2026
5999786
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 24, 2026
e42efbe
feat(android): add Layer#getVisibilityExpression
NEKOYASAN Aug 24, 2026
cb3351e
fix(core): address global-state CI build failures
NEKOYASAN Aug 24, 2026
b388d5d
update: v8.json from @maplibre/maplibre-gl-style-spec
NEKOYASAN Aug 24, 2026
56fe296
chore: port integration test from gl-js
NEKOYASAN Aug 24, 2026
0bfffc5
fix(core): use property defaults when expression evaluation fails
NEKOYASAN Aug 24, 2026
45aedbe
Revert "update: v8.json from @maplibre/maplibre-gl-style-spec"
NEKOYASAN Aug 25, 2026
72433f6
Reapply update v8.json from @maplibre/maplibre-gl-style-spec without
NEKOYASAN Aug 25, 2026
ee02997
test: port global-state render tests from gl-js
NEKOYASAN Aug 26, 2026
5b07b3b
fix(core): defer visibility evaluation until global state is available
NEKOYASAN Aug 26, 2026
89dcf1e
fix(core): align global-state null semantics with gl-js
NEKOYASAN Aug 26, 2026
a52a206
fix(android): allow snapshotter style access after style load
NEKOYASAN Aug 26, 2026
a75c0e9
test(core): add global-state update regression coverage
NEKOYASAN Aug 26, 2026
9441917
test(darwin): ignore fill-extrusion-pattern/global-state test
NEKOYASAN Aug 30, 2026
86ab6e6
refactor(core): pass global state through Tile::setLayers
NEKOYASAN Sep 6, 2026
9cedafc
refactor(core): pass global state separately in query paths
NEKOYASAN Sep 6, 2026
b300d1c
fix(core): reject global-state updates before style load
NEKOYASAN Sep 6, 2026
b8969dd
chore: restore expected-lite submodule revision
NEKOYASAN Sep 6, 2026
8905d43
fix(darwin): convert global-state errors to NSException
NEKOYASAN Sep 7, 2026
dc75a57
Merge branch 'main' into feat/js-compat-global-state
NEKOYASAN Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
### ✨ 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`. 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)).
- [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)
Expand Down
24 changes: 19 additions & 5 deletions include/mln/style/color_ramp_property_value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace style {
class ColorRampPropertyValue {
private:
std::shared_ptr<expression::Expression> value;
std::shared_ptr<const std::set<std::string>> globalStateRefs_;

friend bool operator==(const ColorRampPropertyValue& lhs, const ColorRampPropertyValue& rhs) noexcept {
return (lhs.isUndefined() && rhs.isUndefined()) || (lhs.value && rhs.value && *(lhs.value) == *(rhs.value));
Expand All @@ -25,8 +26,14 @@ class ColorRampPropertyValue {

public:
ColorRampPropertyValue() noexcept = default;
ColorRampPropertyValue(std::shared_ptr<expression::Expression> value_) noexcept
: value(std::move(value_)) {}
ColorRampPropertyValue(std::shared_ptr<expression::Expression> value_)
: value(std::move(value_)) {
if (value && value->has(expression::Dependency::GlobalState)) {
auto refs = std::make_shared<std::set<std::string>>();
expression::collectGlobalStateRefs(*value, *refs);
globalStateRefs_ = std::move(refs);
}
}

bool isUndefined() const noexcept { return value == nullptr; }

Expand All @@ -36,9 +43,14 @@ class ColorRampPropertyValue {
return {};
}

Color evaluate(double rampEvaluationParameter) const {
const auto result = value->evaluate(expression::EvaluationContext({}, nullptr, {rampEvaluationParameter}));
return *expression::fromExpressionValue<Color>(*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<Color>(*result);
return color ? *color : Color{};
}

bool isDataDriven() const noexcept { return false; }
Expand All @@ -48,6 +60,8 @@ class ColorRampPropertyValue {

using Dependency = style::expression::Dependency;
Dependency getDependencies() const noexcept { return value ? value->dependencies : Dependency::None; }

const std::set<std::string>* getGlobalStateRefs() const noexcept { return globalStateRefs_.get(); }
};

} // namespace style
Expand Down
42 changes: 33 additions & 9 deletions include/mln/style/expression/expression.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
#include <mln/util/traits.hpp>
#include <mln/util/variant.hpp>

#include <algorithm>
#include <array>
#include <memory>
#include <numeric>
#include <optional>
#include <set>
#include <string>
#include <vector>

namespace mln {
Expand Down Expand Up @@ -85,6 +88,11 @@ class EvaluationContext {
return *this;
};

EvaluationContext& withGlobalState(const GlobalStateMap* globalState_) noexcept {
globalState = globalState_;
return *this;
};

std::optional<float> zoom;
std::optional<mln::Value> accumulated;
GeometryTileFeature const* feature = nullptr;
Expand All @@ -95,6 +103,7 @@ class EvaluationContext {
const FeatureState* featureState = nullptr;
const std::set<std::string>* availableImages = nullptr;
const mln::CanonicalTileID* canonical = nullptr;
const GlobalStateMap* globalState = nullptr;
};

template <typename T>
Expand Down Expand Up @@ -213,15 +222,16 @@ 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
Override = 1 << 6, // Property override
Elevation = 1 << 7, // Elevation from DEM
MaskCount = 8,
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")
MaskCount = 9,
All = (1 << MaskCount) - 1,
};

Expand Down Expand Up @@ -376,6 +386,20 @@ 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<std::string>& refs);

/// Whether any of the global-state keys referenced by an expression is among
/// the changed keys. A null set on either side means "unknown" and is
/// conservatively treated as a match.
inline bool globalStateRefsIntersect(const std::set<std::string>* refs, const std::set<std::string>* 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

Expand Down
11 changes: 10 additions & 1 deletion include/mln/style/expression/format_section_override.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ class FormatSectionOverride final : public Expression {
}

return defaultValue.match(
[&context](const style::PropertyExpression<T>& e) { return e.getExpression().evaluate(context); },
[&context](const style::PropertyExpression<T>& e) {
// 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();
return e.getExpression().evaluate(contextWithState);
}
return e.getExpression().evaluate(context);
},
[](const T& t) -> EvaluationResult { return t; });
}

Expand Down
9 changes: 9 additions & 0 deletions include/mln/style/layer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <mln/style/conversion.hpp>
#include <mln/style/style_property.hpp>
#include <mln/style/types.hpp>
#include <mln/util/feature.hpp>
#include <mln/util/immutable.hpp>

#include <mapbox/std/weak.hpp>
Expand Down Expand Up @@ -133,6 +134,14 @@ 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&);

/// 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;
Expand Down
12 changes: 12 additions & 0 deletions include/mln/style/layer_properties.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#pragma once
#include <mln/style/layer_impl.hpp>

#include <set>
#include <string>

namespace mln {

namespace style {
Expand All @@ -23,6 +26,15 @@ 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; }

/// Collect the global-state properties referenced by the expressions
/// retained in the evaluated properties, mirroring getEvaluatedDependencies().
virtual void collectEvaluatedGlobalStateRefs(std::set<std::string>&) const {}

protected:
LayerProperties(Immutable<Layer::Impl> impl) noexcept
: baseImpl(std::move(impl)) {}
Expand Down
45 changes: 37 additions & 8 deletions include/mln/style/property_expression.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ 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<const GlobalStateMap> globalState) {
if (expression && expression->has(Dependency::GlobalState)) {
capturedGlobalState_ = std::move(globalState);
}
}
const std::shared_ptr<const GlobalStateMap>& getCapturedGlobalState() const noexcept {
return capturedGlobalState_;
}

/// Null if the expression does not reference the global state.
const std::set<std::string>* 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<const Expression> getSharedExpression() const noexcept;
Expand All @@ -59,6 +75,10 @@ class PropertyExpressionBase {
protected:
std::shared_ptr<const Expression> expression;

std::shared_ptr<const GlobalStateMap> capturedGlobalState_;

std::shared_ptr<const std::set<std::string>> globalStateRefs_;

ZoomCurvePtr zoomCurve;

bool useIntegerZoom_ = false;
Expand All @@ -82,18 +102,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<T> typed = expression::fromExpressionValue<T>(*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));
}
Expand Down Expand Up @@ -172,6 +190,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<T> typed = expression::fromExpressionValue<T>(*result);
if (typed) {
return *typed;
}
}
return defaultValue ? *defaultValue : finalDefaultValue;
}

std::optional<T> defaultValue;
};

Expand Down
6 changes: 6 additions & 0 deletions include/mln/style/property_value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ class PropertyValue {
[](const T&) { return Dependency::None; },
[](const PropertyExpression<T>& ex) { return ex.getDependencies(); });
}

const std::set<std::string>* getGlobalStateRefs() const noexcept {
return value.match([](const Undefined&) -> const std::set<std::string>* { return nullptr; },
[](const T&) -> const std::set<std::string>* { return nullptr; },
[](const PropertyExpression<T>& ex) { return ex.getGlobalStateRefs(); });
}
};

} // namespace style
Expand Down
11 changes: 11 additions & 0 deletions include/mln/style/style.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <mln/map/camera.hpp>
#include <mln/style/image.hpp>
#include <mln/style/transition_options.hpp>
#include <mln/util/feature.hpp>
#include <mln/util/geo.hpp>
#include <mln/util/immutable.hpp>

Expand Down Expand Up @@ -32,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;
Expand All @@ -46,6 +50,13 @@ class Style {

void setLight(std::unique_ptr<Light>);

// 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;
/// Throws if the style has not finished loading.
void setGlobalStateProperty(const std::string& name, const Value& value);

// Images
std::optional<Image> getImage(const std::string&) const;
void addImage(std::unique_ptr<Image>);
Expand Down
1 change: 1 addition & 0 deletions include/mln/util/feature.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ using GeoJSONFeature = mapbox::feature::feature<double>;
using FeatureState = mapbox::base::ValueObject;
using FeatureStates = std::unordered_map<std::string, FeatureState>; // <featureID, FeatureState>
using LayerFeatureStates = std::unordered_map<std::string, FeatureStates>; // <sourceLayer, FeatureStates>
using GlobalStateMap = mapbox::base::ValueObject; // <property name, value>

class Feature : public GeoJSONFeature {
public:
Expand Down
1 change: 1 addition & 0 deletions metrics/ignores/platform-macos-webgpu.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading