Skip to main content

maplibre_expr/
convert.rs

1//! Converting legacy MapLibre *function objects* into modern expressions.
2//!
3//! Before expressions existed, data- and zoom-driven styling was expressed with
4//! *function objects* — `{ "type": "exponential", "property": "x", "stops":
5//! [...] }` and friends. MapLibre still accepts them, converting each to the
6//! equivalent modern expression (`interpolate` / `step` / `match` / `case` / …)
7//! before parsing. This module is a port of maplibre-style-spec's
8//! `src/function/convert.ts`, so the produced expressions match the reference
9//! implementation.
10//!
11//! [`convert_function`] does the conversion given the function object and its
12//! *property spec* (the style-spec entry for the property being styled). The
13//! spec supplies information the object alone lacks — whether the property is
14//! interpolatable (which picks `exponential` vs `interval` when `type` is
15//! omitted), whether `{token}` strings expand to `["get", …]`, and the item
16//! type for identity `array`/`enum`/`color` properties. Pass `&Value::Null`
17//! (or an empty object) when no spec is available: conversion then relies only
18//! on the object's own `type`/`base`/`default`/`stops`/`property` fields, which
19//! covers the common cases.
20//!
21//! By default [`parse`](crate::parse) applies this transparently: hand it either
22//! a modern expression or a legacy function object and it does the right thing.
23//! Disable that with [`Options::convert_legacy`](crate::Options::convert_legacy)
24//! when you want bare objects to be rejected instead.
25
26use serde_json::{json, Value as Json};
27
28/// Whether `value` looks like a legacy function object — i.e. a JSON object
29/// carrying `stops` (interval/exponential/categorical) or `property` (an
30/// identity function). Other objects are not functions and remain parse errors.
31pub fn is_function(value: &Json) -> bool {
32    value
33        .as_object()
34        .is_some_and(|o| o.contains_key("stops") || o.contains_key("property"))
35}
36
37/// Convert a legacy function object to the equivalent modern expression.
38///
39/// `params` is the function object; `spec` is the property's style-spec entry
40/// (pass `&Value::Null` when unavailable). The result is a modern expression as
41/// raw JSON, ready for [`parse`](crate::parse).
42pub fn convert_function(params: &Json, spec: &Json) -> Json {
43    let Some(raw_stops) = params.get("stops").and_then(Json::as_array) else {
44        return convert_identity(params, spec);
45    };
46    let zoom_and_feature = raw_stops
47        .first()
48        .and_then(Json::as_array)
49        .and_then(|s| s.first())
50        .map(Json::is_object)
51        .unwrap_or(false);
52    let feature_dependent = zoom_and_feature || params.get("property").is_some();
53    let zoom_dependent = zoom_and_feature || !feature_dependent;
54    let tokens = spec.get("tokens").and_then(Json::as_bool).unwrap_or(false);
55
56    let stops: Vec<(Json, Json)> = raw_stops
57        .iter()
58        .filter_map(Json::as_array)
59        .filter(|s| s.len() >= 2)
60        .map(|s| {
61            let output = if !feature_dependent && tokens && s[1].is_string() {
62                convert_token_string(s[1].as_str().unwrap())
63            } else {
64                convert_literal(&s[1])
65            };
66            (s[0].clone(), output)
67        })
68        .collect();
69
70    if zoom_and_feature {
71        convert_zoom_and_property(params, spec, &stops)
72    } else if zoom_dependent {
73        convert_zoom(params, spec, &stops, json!(["zoom"]))
74    } else {
75        convert_property(params, spec, &stops)
76    }
77}
78
79fn convert_literal(v: &Json) -> Json {
80    if v.is_object() || v.is_array() {
81        json!(["literal", v])
82    } else {
83        v.clone()
84    }
85}
86
87fn function_type(params: &Json, spec: &Json) -> String {
88    if let Some(t) = params.get("type").and_then(Json::as_str) {
89        return t.to_string();
90    }
91    let interpolated = spec
92        .get("expression")
93        .and_then(|e| e.get("interpolated"))
94        .and_then(Json::as_bool)
95        .unwrap_or(false);
96    if interpolated {
97        "exponential"
98    } else {
99        "interval"
100    }
101    .to_string()
102}
103
104fn interpolate_operator(params: &Json) -> &'static str {
105    match params.get("colorSpace").and_then(Json::as_str) {
106        Some("hcl") => "interpolate-hcl",
107        Some("lab") => "interpolate-lab",
108        _ => "interpolate",
109    }
110}
111
112fn get_fallback(params: &Json, spec: &Json) -> Json {
113    let d = params
114        .get("default")
115        .or_else(|| spec.get("default"))
116        .cloned();
117    match d {
118        Some(v) => convert_literal(&v),
119        None => Json::Null,
120    }
121}
122
123fn append_stop_pair(curve: &mut Vec<Json>, input: Json, output: Json, is_step: bool) {
124    if curve.len() > 3 && curve.get(curve.len() - 2) == Some(&input) {
125        return;
126    }
127    if !(is_step && curve.len() == 2) {
128        curve.push(input);
129    }
130    curve.push(output);
131}
132
133fn fixup_degenerate_step(curve: &mut Vec<Json>) {
134    if curve.first().and_then(Json::as_str) == Some("step") && curve.len() == 3 {
135        let out = curve[2].clone();
136        curve.push(json!(0));
137        curve.push(out);
138    }
139}
140
141fn convert_token_string(s: &str) -> Json {
142    // Replace `{tokens}` with `["get", token]`, concatenating literal spans.
143    let mut result: Vec<Json> = vec![json!("concat")];
144    let bytes = s.as_bytes();
145    let mut pos = 0;
146    let mut i = 0;
147    while i < bytes.len() {
148        if bytes[i] == b'{' {
149            if let Some(end) = s[i..].find('}') {
150                let close = i + end;
151                if i > pos {
152                    result.push(json!(&s[pos..i]));
153                }
154                result.push(json!(["get", &s[i + 1..close]]));
155                i = close + 1;
156                pos = i;
157                continue;
158            }
159        }
160        i += 1;
161    }
162    if result.len() == 1 {
163        return json!(s);
164    }
165    if pos < s.len() {
166        result.push(json!(&s[pos..]));
167    } else if result.len() == 2 {
168        return json!(["to-string", result[1]]);
169    }
170    Json::Array(result)
171}
172
173fn convert_identity(params: &Json, spec: &Json) -> Json {
174    let get = json!(["get", params.get("property")]);
175    let spec_type = spec.get("type").and_then(Json::as_str).unwrap_or("");
176    if params.get("default").is_none() {
177        return if spec_type == "string" {
178            json!(["string", get])
179        } else {
180            get
181        };
182    }
183    if spec_type == "enum" {
184        let keys: Vec<Json> = spec
185            .get("values")
186            .and_then(Json::as_object)
187            .map(|o| o.keys().map(|k| json!(k)).collect())
188            .unwrap_or_default();
189        return json!(["match", get, keys, get, params.get("default")]);
190    }
191    let op = if spec_type == "color" {
192        "to-color"
193    } else {
194        spec_type
195    };
196    let mut expr = vec![json!(op)];
197    if spec_type == "array" {
198        expr.push(spec.get("value").cloned().unwrap_or(Json::Null));
199        expr.push(spec.get("length").cloned().unwrap_or(Json::Null));
200    }
201    expr.push(get);
202    expr.push(convert_literal(params.get("default").unwrap()));
203    Json::Array(expr)
204}
205
206fn convert_property(params: &Json, spec: &Json, stops: &[(Json, Json)]) -> Json {
207    let ty = function_type(params, spec);
208    let get = json!(["get", params.get("property")]);
209    match ty.as_str() {
210        "categorical" if stops.first().map(|s| s.0.is_boolean()).unwrap_or(false) => {
211            let mut expr = vec![json!("case")];
212            for (input, output) in stops {
213                expr.push(json!(["==", get, input]));
214                expr.push(output.clone());
215            }
216            expr.push(get_fallback(params, spec));
217            Json::Array(expr)
218        }
219        "categorical" => {
220            let mut expr = vec![json!("match"), get];
221            for (input, output) in stops {
222                append_stop_pair(&mut expr, input.clone(), output.clone(), false);
223            }
224            expr.push(get_fallback(params, spec));
225            Json::Array(expr)
226        }
227        "interval" => {
228            let mut expr = vec![json!("step"), json!(["number", get])];
229            for (input, output) in stops {
230                append_stop_pair(&mut expr, input.clone(), output.clone(), true);
231            }
232            fixup_degenerate_step(&mut expr);
233            wrap_default(params, Json::Array(expr), &get)
234        }
235        _ => {
236            // exponential
237            let base = params.get("base").and_then(Json::as_f64).unwrap_or(1.0);
238            let interp = if base == 1.0 {
239                json!(["linear"])
240            } else {
241                json!(["exponential", base])
242            };
243            let mut expr = vec![
244                json!(interpolate_operator(params)),
245                interp,
246                json!(["number", get]),
247            ];
248            for (input, output) in stops {
249                append_stop_pair(&mut expr, input.clone(), output.clone(), false);
250            }
251            wrap_default(params, Json::Array(expr), &get)
252        }
253    }
254}
255
256/// Wrap an interval/exponential property function in a `case` that falls back
257/// to the default when the property is not a number.
258fn wrap_default(params: &Json, expr: Json, get: &Json) -> Json {
259    match params.get("default") {
260        None => expr,
261        Some(default) => json!([
262            "case",
263            ["==", ["typeof", get], "number"],
264            expr,
265            convert_literal(default)
266        ]),
267    }
268}
269
270fn convert_zoom(params: &Json, spec: &Json, stops: &[(Json, Json)], input: Json) -> Json {
271    let ty = function_type(params, spec);
272    let (mut expr, is_step) = if ty == "interval" {
273        (vec![json!("step"), input], true)
274    } else {
275        let base = params.get("base").and_then(Json::as_f64).unwrap_or(1.0);
276        let interp = if base == 1.0 {
277            json!(["linear"])
278        } else {
279            json!(["exponential", base])
280        };
281        (
282            vec![json!(interpolate_operator(params)), interp, input],
283            false,
284        )
285    };
286    for (i, o) in stops {
287        append_stop_pair(&mut expr, i.clone(), o.clone(), is_step);
288    }
289    fixup_degenerate_step(&mut expr);
290    Json::Array(expr)
291}
292
293fn convert_zoom_and_property(params: &Json, spec: &Json, stops: &[(Json, Json)]) -> Json {
294    // Group stops by zoom level, preserving encounter order.
295    let mut zooms: Vec<f64> = Vec::new();
296    let mut grouped: Vec<Vec<(Json, Json)>> = Vec::new();
297    for (key, output) in stops {
298        let zoom = key.get("zoom").and_then(Json::as_f64).unwrap_or(0.0);
299        let value = key.get("value").cloned().unwrap_or(Json::Null);
300        match zooms.iter().position(|z| *z == zoom) {
301            Some(idx) => grouped[idx].push((value, output.clone())),
302            None => {
303                zooms.push(zoom);
304                grouped.push(vec![(value, output.clone())]);
305            }
306        }
307    }
308    let feature_params = |zoom: f64| {
309        let mut m = serde_json::Map::new();
310        m.insert("zoom".into(), json!(zoom));
311        for key in ["type", "property", "default"] {
312            if let Some(v) = params.get(key) {
313                m.insert(key.into(), v.clone());
314            }
315        }
316        Json::Object(m)
317    };
318    let ty = function_type(&json!({}), spec);
319    if ty == "exponential" {
320        let mut expr = vec![
321            json!(interpolate_operator(params)),
322            json!(["linear"]),
323            json!(["zoom"]),
324        ];
325        for (i, z) in zooms.iter().enumerate() {
326            let output = convert_property(&feature_params(*z), spec, &grouped[i]);
327            append_stop_pair(&mut expr, json!(z), output, false);
328        }
329        Json::Array(expr)
330    } else {
331        let mut expr = vec![json!("step"), json!(["zoom"])];
332        for (i, z) in zooms.iter().enumerate() {
333            let output = convert_property(&feature_params(*z), spec, &grouped[i]);
334            append_stop_pair(&mut expr, json!(z), output, true);
335        }
336        fixup_degenerate_step(&mut expr);
337        Json::Array(expr)
338    }
339}