maplibre / maplibre-style-spec

MapLibre Style Specification & Utilities
https://maplibre.org/maplibre-style-spec/
Other
86 stars 65 forks source link

Proposal: Support registering external functions to be used in expressions in the style #516

Open MikkoSteerpath opened 9 months ago

MikkoSteerpath commented 9 months ago

Design Proposal: External style functions

Motivation

Some things, such as converting unix timestamps to be shown in a human readable way in for the end users locale can be extremely complex to accomplish using style expressions. Another case is implementing for example thousands separator for numbers. These things depend on the app user's locale settings and potentially even timezones.

While it could be theoretically possible to extend the style specification with every helper function app developers would need, or write extremely long and complex expressions, in many cases it would be much more convenient for everyone if there was a standard way for applications to register their own utility functions that could be used from the style.

Another example in the past was that I would have liked to use regular expressions in the string handling in the style. This proposal and js implementation were never accepted, because regular expression engines are different on different platforms, and there was a risk of introducing cross platform incompatibilities in the spec (iirc). Meanwhile, as a developer, I had no reasonable route forward (except forking the entire project, on 3 platforms), even if I was willing to accept minor differences between platforms.

The feature could also be used for prototyping new functionality and showcasing possibilities of functionality that could be included in the specification. For example one could hack together a function which returns the icon-offset based on the camera transformation matrix, to correctly place the POI in 3d space, even though this feature is not yet available.

If the value of the parameter could be updated based on animation frames, it would even be possible to make icons bounce by returning a different value for icon-offset each frame.

Proposed Change

Extend the style specification with a new expressions exec which takes two parameters, a string type parameter for the name of the external function to execute, and an array type parameter with the the parameters passed to the external function.

Examples: ["exec", "formatDate", [ ["get", "timestamp"], ["get", "poi_timezone"]] ]

API Modifications

Define a new type that is a catch all for all exec functions.

Add a new API function to the maplibre programmatic api to set and unset the catch all handler.

The new types and API could be:

type ExecStyleFunctionResult<T> = {
  cacheHint: 'until-animation-frame' | 'until-camera-move' | 'until-style-update' | 'forever';
  value: T;
}

type ExecStyleFunctionHandler = (fnName: string, params: unknown[]) => ExecStyleFunctionResult<unknown>;

function setExecStyleFunctionHandler(cb: ExternalStyleFunctionHandler | null);

Internally the engine would keep a map of the exec functions used, the previous value and

Migration Plan and Compatibility

This does not replace existing functionality but allows easy prototyping of new style expressions prior to rolling them into the spec.

Rejected Alternatives

The reason for a catch all function rather than registering individual handlers are:

The reason for passing params in a single array rather than passing params indivdually:

The reason why the catch all cannot just return the value:

sjg-wdw commented 9 months ago

I see the need, but I wonder if just catching the data as it's being parsed might be simpler. Extending the style spec to incorporate out of band functions is probably more logical in Javascript than it is on mobile. Where messing with the data after it's been parsed probably meshes better on both major toolkits.

wipfli commented 9 months ago

I agree with @sjg-wdw that it would be better to add way for the user to get all attributes of a point/line/polygon after parsing, and then being able to freely modify the attributes with a user-defined function. This means one can update/add/delete properties...

MikkoSteerpath commented 9 months ago

I can see that modifying the attribute data could be quite interesting as well, it has some challenges such as how to define which objects to modify, how to update the previous modification when the users locale changes and how to track when the data with the relevant properties is loaded.

In particular considering clusters, where the data is generated on the fly using the style rules, I don't think this approach will be easy to implement but feel free to prove me wrong.

As for this approach being less logical on natives I would like to give some counter arguments:

In summary, the evaluation inside maplibre is already dynamic and takes into account many global and runtime variables, the handler for custom code would just be another one. I would also argue that from a developer/library user point of view, setting a handler/delegate/provider is a common pattern on all platform.

HarelM commented 9 months ago

I guess the next question, assuming this is possible, might be the API signature. You mentioned the javascript API, that can be very "loose", what would be the API for the other platforms, given that they are more strict in terms of types etc? Would sending Object[] as the parameters in Android makes sense?

sjg-wdw commented 9 months ago

For Native I'd run this by the team and see what they think. Might be something really easy to do, but at least they're familiar with that part of the system.

neodescis commented 9 months ago

I agree with @sjg-wdw that it would be better to add way for the user to get all attributes of a point/line/polygon after parsing, and then being able to freely modify the attributes with a user-defined function. This means one can update/add/delete properties...

It would be great if we could modify the GeoJSON shapes too, in addition to properties.

This got me thinking about a potentially related use case that I am going to have to support soon: creating ellipses on the fly. Basically I have a set of points with semi-major and semi-minor axes, but I need to render them as ellipses (or polygonal approximations of them, given GeoJSON constraints). These will be delivered to the front-end as features in vector tiles. I have control over the vector tiles, but the size of the (potentially many) features coming across the wire would dramatically increase if I have to create the polygon approximations on the server. It would be ideal if I could handle this transformation client-side.

MikkoSteerpath commented 9 months ago

This idea has been explored before in https://github.com/maplibre/maplibre-gl-js/issues/1295 and there is a bit of a summary in a comment about the challenges.

wipfli commented 9 months ago

@neodescis if we allow geometry-on-the-fly we could also do bezier curves for rounded lines...

hiddewie commented 1 week ago

I was referenced here to discuss a feature request for MapLibre GL JS, written in https://github.com/maplibre/maplibre-gl-js/issues/4964.

In summary, I would like to have a way to specify (global) map state that can somehow be used in the style. The use case is to define a map theme, and allow the style to dynamically change the rendering of the style based on the chosen user theme.

My feature request would be satisfied if I could define custom functions to be used in the style specification, such that I can make some values in the style dynamic based on the map configuration in the client application.

If it makes it easier to discuss, I could also write a separate proposal just for the MapLibre style specification to allow defining and using map state in a style. There is a proposal for syntax in https://github.com/maplibre/maplibre-gl-js/issues/4964, similar to how feature-state can be used.

Seeing that this proposal has been open for more than 10 months, what is the best way to move this forward?

HarelM commented 1 week ago

I'm pretty sure I've seen something similar related to dark theme and light theme that can benefit from "global" variables, but I can't find it... :-( You can open a separate design proposal for the spec for something that you think can facilitate for this. Other approach that I've seen people do it generate the style.json from parameters instead of using a style.json file, this obviously has all the flexibility of any programming language you decide to use. Generating the json is fairly straightforward.

hiddewie commented 6 days ago

Thanks, I split off this proposal into https://github.com/maplibre/maplibre-style-spec/issues/886 where it can be discussed in depth.