Skip to content

Design Proposal: Global map state #886

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
hiddewie opened this issue Nov 4, 2024 · 13 comments
Open

Design Proposal: Global map state #886

hiddewie opened this issue Nov 4, 2024 · 13 comments
Labels
enhancement New feature or request

Comments

@hiddewie
Copy link

hiddewie commented Nov 4, 2024

Design Proposal: Global map state

Motivation

Split off from #516 (comment), and originally posted / requested in maplibre/maplibre-gl-js#4964.

The goal is to store some global map state on the Map object, which can be used in the style. This makes it easier to let the user configure the map for certain preferences, which can then dynamically be used in the style to modify the appearance of the map.

My concrete use case is supporting a map theme, where the user selects a theme and this impacts the look and feel of the web application, in particular the features on the map.

Proposed Change

Introduce a function map-state which takes a single string argument which is the key.

The return value is the value in the global map state, if present, otherwise null.

Examples:

paint: {
  'text-color': ['map-state', 'color'],
}

Handling unset key in the map state

paint: {
  'text-color': ['coalesce', ['map-state', 'color'], 'red']
}

The map state will function as a key-value dictionary, so the values in the map state contains string keys and string, numeric or boolean values. We could choose to support e.g. arrays as values as well.

The map-state function should have the same semantics as feature-state which uses data set on a feature.

API Modifications

The expression map-state will be supported.

Migration Plan and Compatibility

No migration needed, this is new functionality.

The feature should not be complex to implement in the specification and client libraries because:

  • the function feature-state already exists, and this function is similar although it takes the state from a different place
  • the map state is a new feature, so this will not conflict with existing features.

Probably the biggest complexity is managing the global map state changing its value, and recomputing the style for the impacted features on the map.

Rejected Alternatives

  • Support global map state maplibre-gl-js#4964 (comment): Changing the style dynamically during loading of the map: this makes the map style highly coupled with the client using the style. In particular, the client application has to walk every style expression, and match the expression against the replacements with the configured global values from the user configuration.
  • Support global map state maplibre-gl-js#4964 (comment): using let expressions and dynamically transforming the style. This still couples the client with the map style, and still requires walking the expressions in the map style to dynamically replace the let values with the configured values.
  • Proposal: Support registering external functions to be used in expressions in the style #516: Design proposal to add arbitrary functions to the map. That proposal would replace this proposal, but the scope is much bigger and more complex to implement than this feature.
  • Proposal: Support registering external functions to be used in expressions in the style #516 (comment): Generate a style per value of the user configuration. This is possible for global map state with a finite set of values. Once the values become non-enumerable (like numbers between 0 and 1), or many properties which can all be combined, the number of JSON styles to generate explodes. It is possible to create a server to dynamically create a style on request, taking the user parameters into account, but this requires a server creating the styles on demand, instead of putting static files in a web file server.
@sjg-wdw
Copy link

sjg-wdw commented Nov 4, 2024

This is a really interesting idea and I'd like to weigh in from the implementation side.

Having overseen the MapLibre Native upgrade this year and last, I can say that the flexibility in the style spec causes a lot of extra work at the implementation level.

The difference in perspective between the style sheets and the GPU focused real time rendering is vast. That gulf has to be bridged by the renderer implementations each time they see a style sheet and a vector tile.

The MapLibre toolkits already do this work, so that's good for MapLibre. It bad for anyone else who might want to write a new renderer and it does use a lot of extra power on mobile devices for flexibility that's rarely used.

At the root, the problem is that style sheets are meant to be flexible enough for editing and be simple enough to be parsed by the map renderer. Those two goals pull in opposite directions.

Rather than adding more flexibility into the style spec itself, I wonder if it might be time to break things apart. Add more flexibility in a high level version of the style sheet and remove flexibility from the low level.

In your example, perhaps add meta-tags to the style sheet that denote states and then process that into multiple actual style sheets. To make switching easier, name things the same, provide unique IDs, perhaps. That sort of thing.

Maybe follow the logic all the way into generating style sheets the same way we do static HTML sites these days. It would be kind of nice to just dump a block of Javascript in the middle of a style sheet to do some logic at generation time.

On the compiled side, you could imagine a header block to tell us what's in the style sheet. There's all sorts of stuff we could turn off if we didn't have to expect it and new renderers could have some idea of what they need to support to get most of the way there.

A bit more than you're looking for, I'm sure, so the short version is... maybe go in this other direction?

@hiddewie
Copy link
Author

hiddewie commented Nov 6, 2024

Thanks for the comment, interesting perspective.

We could also describe this same design proposal as a set of style input parameters, which are used to generate a concrete style. I don't have a concrete proposal of how this would look like from the API and implementation perspective.

I do agree that it seems valuable to move as much processing away from parsing the style and low level dynamic style elements, into a simple style specification that can be fed to a GPU for rendering.

@BTolputt
Copy link

Worth noting that I found this discussion whilst looking for a means by which I could dynamically alter the "text-size" & "line-width" properties (be they constant or expression) due to high vs low DPI screens in the Win32 world.

At the moment, I'm having to pre-process the style JSON and replace wildcards with the calculated DPI at style load time (requiring a style reload when the window is moved between monitors). Not great, but OK for styles I code personally, however not a reasonable option for client created styles.

Being able to access global variable representing the DPI scale factor for this in expressions would make creating these styles far more manageable.

@zbigniewmatysek-tomtom
Copy link
Contributor

At TomTom, we see great potential in having this feature for map personalization purposes.

Some of the use cases it enables for navigation applications are:

  • Filtering EV POIs depending on the charger type, charging speed, etc.
  • Prioritizing different POIs for navigation mode and different ones for search mode.

To address other options:

  • Existing API allows such filtering by modifying the expression, but the logic needs to be duplicated in each application consuming the style (we have many apps built on different stacks), and every style update can easily break this logic.

  • We have very complex expressions for layer filters. Those filters change frequently with style updates. This implies that the fragile logic that modifies these filters needs to be updated frequently too. We want to achieve the opposite - making style updates transparent to the apps that consume the style.

Extending style spec seems the most straightforward and viable solution for fast adoption of this feature. All filtering logic is still in in one place, changing it does not require code modification which reduces implementation and maintenance cost, and cycle time to deliver new features.

@j-osephlong
Copy link

Excited to see this become reality - this can greatly simplify code bases needing to vary styling. My use case is I have "sets" of fieldwork symbols, and I'd like to emphasis the currently selected set whilst fading all other points. With a global state I could simply pass the selected fieldwork id!

@zbigniewmatysek-tomtom
Copy link
Contributor

To facilitate the discussion I created implementation (style-spec, maplibre-gl-js) and sandbox, so that everyone can play with the proposed functionality:
https://codesandbox.io/p/sandbox/4gzhtc

For the sake of the PoC I keep things simple - the expression is not compound (you cannot use another expression as a value) and setting global state always reloads sources because expression may be used inside layer.filter.

@zbigniewmatysek-tomtom
Copy link
Contributor

I have some concerns regarding the style validation and integration.

  1. By using the global-state expression, we retrieve data from the state that may or may not be set. When the state is not set, the expression will evaluate to null. If this is not handled properly (for example, with coalesce as suggested in the initial post), then the style may fail during evaluation. This property is dynamic, so we cannot verify its correctness during validation or parse time.
  2. Integrators may have difficulties identifying which global state properties are supported by the style.

To address this, we could specify the defaults and types in a new style property. Let's refer to this property as state:

"state": {
  "minChargerSpeedKWh": 200
  "poiPreferences": {
      "promotedCategories" ['restaurant', 'leisure']
   }
}

Having such property in place:

  • Adjust validation to require it's presence if global-state expression is used and there is always a fallback if the state is not set in the runtime.
  • State setting method (think map.setGlobalState(property, value)) could use the information in the state property to perform validations: check whether the property user is trying to set exists and that the value type matches the default type.

What do you think of this approach?

@HarelM
Copy link
Collaborator

HarelM commented Mar 12, 2025

How is it different than missing values in a property of a feature (["get", "not-existing-property"])?

@zbigniewmatysek-tomtom
Copy link
Contributor

zbigniewmatysek-tomtom commented Mar 12, 2025

That's a good example. get can also fail in a runtime and then style property will fallback to the default value, which may be unexpected. Validation is passing without that check. With this new property, we can make it better and introduce a guarantee that style will render correctly if those state properties are not set, minimizing the impact of errors in style integration in the consuming applications.

Building further on the integration problem. Let's say I am developing a navigation SDK that relies on the minChargerSpeedKWh and exposes a method setMinChargerSpeedKWh(). SDK comes with a set of default styles that are released frequently. When new version is released, it's tested and integrated in the SDK. How can I automatically test that new style version contains state values that the SDK relies on? If we have just the expression, then I would need to parse the expressions, which is quite complicated and requires additional library just for this purpose.

@HarelM
Copy link
Collaborator

HarelM commented Mar 12, 2025

I'm not sure I understand the last part, but I'm not against default values, I'm just saying that it might be easier if they were optional.
Default values are a good descriptive way to let the style reader (a human or a machine) know what can be defined as part of this global state.

@1ec5
Copy link
Contributor

1ec5 commented Mar 12, 2025

The ability to set style-wide state and refer to it in individual expressions would be extremely useful. Lots of client code is forced to iterate over all the layers in the style and apply the same transformation to the filter or paint or layout properties. Examples include mapbox-gl-language, maplibre-gl-dates (which I maintain), OpenStreetMap Americana, and code built right into MapLibre Native for iOS.

To guard against repeated applications of the same transformation, most of these plugins have code that wraps each expression in a let expression and modifies one of the variables if it sees a let expression the next time around. This is one of the rejected alternatives, and I agree with the rejection, because it can be quite a challenging workaround if multiple plugins are involved in the same style.

I don’t know if the current implementation of this proposal would be more performant, but it would certainly be more manageable than the current approach. For performance testing, this OpenHistoricalMap animation demonstrates the iterative approach in a fairly hot loop.

@zbigniewmatysek-tomtom
Copy link
Contributor

For people following this thread, the discussion continues in: #1044

@hiddewie
Copy link
Author

Seeing #1044 is merged, I think this issue can be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants