Skip to main content

mapterm/
expr.rs

1//! MapLibre / Mapbox style expressions, the subset that makes sense in a terminal:
2//! `get`, `has`, `zoom`, `geometry-type`, comparisons, `all`/`any`/`!`, `in`, `match`, `case`,
3//! `coalesce`, `interpolate` (linear and exponential), `step`, string and number helpers, plus
4//! the legacy filter syntax (`["==", "class", "park"]`, `["in", "class", ...]`, `["has", k]`)
5//! and legacy zoom functions (`{"stops": [[5, 1], [10, 4]], "base": 1.2}`).
6//!
7//! Anything else is rejected with an error, so a style can fall back to the property's default
8//! (or hide a layer whose filter it can't understand) instead of drawing the wrong thing.
9//! Spec: <https://maplibre.org/maplibre-style-spec/expressions/>
10
11use anyhow::{Result, anyhow, bail};
12use serde_json::Value as Json;
13
14use crate::mvt::{GeomType, Value};
15
16/// An RGBA color, channels 0..1.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct Rgba {
19    /// Red.
20    pub r: f64,
21    /// Green.
22    pub g: f64,
23    /// Blue.
24    pub b: f64,
25    /// Alpha (opacity).
26    pub a: f64,
27}
28
29impl Rgba {
30    /// An opaque color from 8-bit channels.
31    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
32        Self { r: r as f64 / 255.0, g: g as f64 / 255.0, b: b as f64 / 255.0, a: 1.0 }
33    }
34
35    /// The color as 8-bit RGB (alpha ignored).
36    pub fn to_u8(self) -> (u8, u8, u8) {
37        let c = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
38        (c(self.r), c(self.g), c(self.b))
39    }
40
41    /// Mix `self` over `under` with the given alpha (0 = `under`, 1 = `self`).
42    pub fn over(self, under: Rgba, alpha: f64) -> Rgba {
43        let a = alpha.clamp(0.0, 1.0);
44        let m = |x: f64, y: f64| x * a + y * (1.0 - a);
45        Rgba { r: m(self.r, under.r), g: m(self.g, under.g), b: m(self.b, under.b), a: 1.0 }
46    }
47
48    fn lerp(self, o: Rgba, t: f64) -> Rgba {
49        let m = |x: f64, y: f64| x + (y - x) * t;
50        Rgba { r: m(self.r, o.r), g: m(self.g, o.g), b: m(self.b, o.b), a: m(self.a, o.a) }
51    }
52}
53
54/// A CSS color: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, `hsl()`, `hsla()`
55/// or a common name.
56pub fn parse_color(s: &str) -> Option<Rgba> {
57    let s = s.trim().to_ascii_lowercase();
58    if let Some(hex) = s.strip_prefix('#') {
59        let d: Vec<u8> = hex.chars().map(|c| c.to_digit(16).map(|v| v as u8)).collect::<Option<_>>()?;
60        let (r, g, b, a) = match d.len() {
61            3 => (d[0] * 17, d[1] * 17, d[2] * 17, 255),
62            4 => (d[0] * 17, d[1] * 17, d[2] * 17, d[3] * 17),
63            6 => (d[0] * 16 + d[1], d[2] * 16 + d[3], d[4] * 16 + d[5], 255),
64            8 => (d[0] * 16 + d[1], d[2] * 16 + d[3], d[4] * 16 + d[5], d[6] * 16 + d[7]),
65            _ => return None,
66        };
67        let mut c = Rgba::rgb(r, g, b);
68        c.a = a as f64 / 255.0;
69        return Some(c);
70    }
71    if let Some((func, rest)) = s.split_once('(') {
72        let args: Vec<&str> = rest.trim_end_matches(')').split([',', ' ', '/']).filter(|a| !a.is_empty()).collect();
73        let num = |a: &str| -> Option<f64> {
74            match a.strip_suffix('%') {
75                Some(p) => p.parse::<f64>().ok().map(|v| v / 100.0),
76                None => a.trim_end_matches("deg").parse().ok(),
77            }
78        };
79        let alpha = args.get(3).map_or(Some(1.0), |a| num(a))?;
80        return match func.trim() {
81            "rgb" | "rgba" if args.len() >= 3 => {
82                let ch = |a: &str| -> Option<f64> {
83                    if a.ends_with('%') { num(a) } else { num(a).map(|v| v / 255.0) }
84                };
85                Some(Rgba { r: ch(args[0])?, g: ch(args[1])?, b: ch(args[2])?, a: alpha })
86            }
87            "hsl" | "hsla" if args.len() >= 3 => {
88                let (h, sat, l) = (num(args[0])?.rem_euclid(360.0) / 360.0, num(args[1])?, num(args[2])?);
89                let q = if l < 0.5 { l * (1.0 + sat) } else { l + sat - l * sat };
90                let p = 2.0 * l - q;
91                let hue = |mut t: f64| {
92                    t = t.rem_euclid(1.0);
93                    if t < 1.0 / 6.0 {
94                        p + (q - p) * 6.0 * t
95                    } else if t < 0.5 {
96                        q
97                    } else if t < 2.0 / 3.0 {
98                        p + (q - p) * (2.0 / 3.0 - t) * 6.0
99                    } else {
100                        p
101                    }
102                };
103                Some(Rgba { r: hue(h + 1.0 / 3.0), g: hue(h), b: hue(h - 1.0 / 3.0), a: alpha })
104            }
105            _ => None,
106        };
107    }
108    let (r, g, b) = match s.as_str() {
109        "transparent" => return Some(Rgba { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
110        "black" => (0, 0, 0),
111        "white" => (255, 255, 255),
112        "red" => (255, 0, 0),
113        "green" => (0, 128, 0),
114        "lime" => (0, 255, 0),
115        "blue" => (0, 0, 255),
116        "yellow" => (255, 255, 0),
117        "cyan" | "aqua" => (0, 255, 255),
118        "magenta" | "fuchsia" => (255, 0, 255),
119        "orange" => (255, 165, 0),
120        "purple" => (128, 0, 128),
121        "gray" | "grey" => (128, 128, 128),
122        "silver" => (192, 192, 192),
123        "navy" => (0, 0, 128),
124        "teal" => (0, 128, 128),
125        "maroon" => (128, 0, 0),
126        "olive" => (128, 128, 0),
127        "brown" => (165, 42, 42),
128        "pink" => (255, 192, 203),
129        _ => return None,
130    };
131    Some(Rgba::rgb(r, g, b))
132}
133
134/// A value an expression produces.
135#[derive(Clone, Debug, PartialEq)]
136pub enum Val {
137    /// No value (a missing property, say).
138    Null,
139    /// true or false.
140    Bool(bool),
141    /// A number.
142    Num(f64),
143    /// Text.
144    Str(String),
145    /// A color.
146    Color(Rgba),
147    /// A list.
148    Array(Vec<Val>),
149}
150
151impl Val {
152    fn from_json(j: &Json) -> Val {
153        match j {
154            Json::Null => Val::Null,
155            Json::Bool(b) => Val::Bool(*b),
156            Json::Number(n) => n.as_f64().map_or(Val::Null, Val::Num),
157            Json::String(s) => Val::Str(s.clone()),
158            Json::Array(a) => Val::Array(a.iter().map(Val::from_json).collect()),
159            Json::Object(_) => Val::Null,
160        }
161    }
162
163    fn from_prop(v: &Value) -> Val {
164        match v {
165            Value::Str(s) => Val::Str(s.clone()),
166            Value::Num(n) => Val::Num(*n),
167            Value::Bool(b) => Val::Bool(*b),
168        }
169    }
170
171    /// The number, if it is one.
172    pub fn as_num(&self) -> Option<f64> {
173        match self {
174            Val::Num(n) => Some(*n),
175            _ => None,
176        }
177    }
178
179    /// Whether it is `true`.
180    pub fn as_bool(&self) -> bool {
181        matches!(self, Val::Bool(true))
182    }
183
184    /// Colors, and strings that parse as colors.
185    pub fn as_color(&self) -> Option<Rgba> {
186        match self {
187            Val::Color(c) => Some(*c),
188            Val::Str(s) => parse_color(s),
189            _ => None,
190        }
191    }
192
193    /// The value as display text (`to-string` rules).
194    pub fn to_text(&self) -> String {
195        match self {
196            Val::Null => String::new(),
197            Val::Bool(b) => b.to_string(),
198            Val::Num(n) => Value::Num(*n).to_string(),
199            Val::Str(s) => s.clone(),
200            Val::Color(c) => {
201                let (r, g, b) = c.to_u8();
202                format!("rgba({r},{g},{b},{})", c.a)
203            }
204            Val::Array(a) => format!("[{}]", a.iter().map(Val::to_text).collect::<Vec<_>>().join(",")),
205        }
206    }
207
208    fn truthy(&self) -> bool {
209        !matches!(self, Val::Null | Val::Bool(false))
210    }
211}
212
213/// What an expression is evaluated against: one feature at one zoom, with its feature state
214/// (see [`crate::Map::set_feature_state`]).
215pub struct Ctx<'a> {
216    /// The zoom level.
217    pub zoom: f64,
218    /// The feature's geometry type.
219    pub geom: GeomType,
220    /// The feature's properties.
221    pub props: &'a [(String, Value)],
222    /// The feature's state (empty when none is set).
223    pub state: &'a [(String, Val)],
224}
225
226impl Ctx<'_> {
227    fn prop(&self, k: &str) -> Option<&Value> {
228        self.props.iter().find(|(key, _)| key == k).map(|(_, v)| v)
229    }
230}
231
232#[derive(Clone, Copy, Debug, PartialEq)]
233/// A comparison operator.
234pub enum Cmp {
235    /// `==`
236    Eq,
237    /// `!=`
238    Ne,
239    /// `<`
240    Lt,
241    /// `<=`
242    Le,
243    /// `>`
244    Gt,
245    /// `>=`
246    Ge,
247}
248
249#[derive(Clone, Copy, Debug, PartialEq)]
250/// An arithmetic operator.
251pub enum Math {
252    /// `+` (sum of all)
253    Add,
254    /// `-` (negation with one argument)
255    Sub,
256    /// `*`
257    Mul,
258    /// `/`
259    Div,
260    /// `%`
261    Rem,
262    /// `min`
263    Min,
264    /// `max`
265    Max,
266}
267
268#[derive(Clone, Copy, Debug, PartialEq)]
269/// The type `string`, `number` and `boolean` assert.
270pub enum Assert {
271    /// `string`
272    Str,
273    /// `number`
274    Num,
275    /// `boolean`
276    Bool,
277}
278
279/// A compiled expression. Make one with [`Expr::parse`] (or [`Expr::parse_text`] for
280/// `text-field`); each variant is the expression of the same name in the MapLibre spec.
281#[derive(Clone, Debug, PartialEq)]
282pub enum Expr {
283    /// A constant (`["literal", ...]` or a plain value).
284    Lit(Val),
285    /// `["get", key]`: a property.
286    Get(String),
287    /// `["has", key]`: whether the property exists.
288    Has(String),
289    /// `["zoom"]`.
290    Zoom,
291    /// `["geometry-type"]`: `Point`, `LineString` or `Polygon`.
292    GeomType,
293    /// A comparison (`==`, `!=`, `<`, `<=`, `>`, `>=`).
294    Cmp(Cmp, Box<Expr>, Box<Expr>),
295    /// `["all", ...]`: every condition holds.
296    All(Vec<Expr>),
297    /// `["any", ...]`: some condition holds.
298    Any(Vec<Expr>),
299    /// `["!", x]` (or legacy `none`).
300    Not(Box<Expr>),
301    /// Needle in a list of values (legacy `in`, or modern `in` with a literal list).
302    InList(Box<Expr>, Vec<Val>),
303    /// Modern `in` with a computed haystack (string or array).
304    In(Box<Expr>, Box<Expr>),
305    /// `["match", input, labels, output, ..., default]`.
306    Match {
307        /// The value matched.
308        input: Box<Expr>,
309        /// Labels (one or a list) and the output for them.
310        arms: Vec<(Vec<Val>, Expr)>,
311        /// The output when no label matches.
312        default: Box<Expr>,
313    },
314    /// `["case", condition, output, ..., default]`.
315    Case {
316        /// Conditions and their outputs, tried in order.
317        arms: Vec<(Expr, Expr)>,
318        /// The output when no condition holds.
319        default: Box<Expr>,
320    },
321    /// `["coalesce", ...]`: the first value that isn't null.
322    Coalesce(Vec<Expr>),
323    /// `["interpolate", curve, input, stop, output, ...]` (numbers and colors).
324    Interpolate {
325        /// Exponential base; 1 is linear.
326        base: f64,
327        /// The value interpolated over (usually `["zoom"]`).
328        input: Box<Expr>,
329        /// Stop inputs and their outputs, ascending.
330        stops: Vec<(f64, Expr)>,
331    },
332    /// `["step", input, default, stop, output, ...]`.
333    Step {
334        /// The value stepped over.
335        input: Box<Expr>,
336        /// The output below the first stop.
337        default: Box<Expr>,
338        /// Stop inputs and the outputs from them on, ascending.
339        stops: Vec<(f64, Expr)>,
340    },
341    /// Arithmetic (`+`, `-`, `*`, `/`, `%`, `min`, `max`).
342    Math(Math, Vec<Expr>),
343    /// `["concat", ...]` (and `format`, as plain text).
344    Concat(Vec<Expr>),
345    /// `["upcase", x]`.
346    Upcase(Box<Expr>),
347    /// `["downcase", x]`.
348    Downcase(Box<Expr>),
349    /// `["to-string", x]`.
350    ToStr(Box<Expr>),
351    /// `["to-number", x, fallback...]`.
352    ToNum(Vec<Expr>),
353    /// `["to-boolean", x]`.
354    ToBool(Box<Expr>),
355    /// `["to-color", x, fallback...]`.
356    ToColor(Vec<Expr>),
357    /// `["length", x]`: characters of a string or items of an array.
358    Length(Box<Expr>),
359    /// `["typeof", x]`.
360    TypeOf(Box<Expr>),
361    /// `["index-of", keyword, input, from?]`: character position, or -1.
362    IndexOf(Vec<Expr>),
363    /// `["slice", input, start, end?]`: characters (or array items) from start to end.
364    Slice(Vec<Expr>),
365    /// `["string", ...]`, `["number", ...]`, `["boolean", ...]`: the first value of that type.
366    Assert(Assert, Vec<Expr>),
367    /// `["feature-state", key]`: a value the app set on the feature.
368    FeatureState(String),
369    /// A legacy `"{name}"` text template.
370    Template(Vec<Part>),
371}
372
373/// A piece of a legacy text template.
374#[derive(Clone, Debug, PartialEq)]
375pub enum Part {
376    /// Literal text.
377    Text(String),
378    /// A property's value (`{name}`).
379    Prop(String),
380}
381
382fn key_expr(k: &str) -> Expr {
383    match k {
384        "$type" => Expr::GeomType,
385        _ => Expr::Get(k.to_string()),
386    }
387}
388
389fn args(a: &[Json]) -> Result<Vec<Expr>> {
390    a.iter().map(Expr::parse).collect()
391}
392
393fn one(a: &[Json], op: &str) -> Result<Box<Expr>> {
394    match a {
395        [x] => Ok(Box::new(Expr::parse(x)?)),
396        _ => bail!("{op}: expected 1 argument"),
397    }
398}
399
400/// `["literal", [..]]` or a bare array, as values.
401fn literal_list(j: &Json) -> Option<Vec<Val>> {
402    match j {
403        Json::Array(a) if a.first().and_then(Json::as_str) == Some("literal") => {
404            a.get(1).and_then(Json::as_array).map(|l| l.iter().map(Val::from_json).collect())
405        }
406        _ => None,
407    }
408}
409
410fn stops(a: &[Json], op: &str) -> Result<Vec<(f64, Expr)>> {
411    if !a.len().is_multiple_of(2) {
412        bail!("{op}: stops must come in pairs");
413    }
414    let mut out = Vec::new();
415    for pair in a.chunks(2) {
416        let x = pair[0].as_f64().ok_or_else(|| anyhow!("{op}: stop input must be a number"))?;
417        out.push((x, Expr::parse(&pair[1])?));
418    }
419    Ok(out)
420}
421
422impl Expr {
423    /// A constant.
424    pub fn lit(v: Val) -> Expr {
425        Expr::Lit(v)
426    }
427
428    /// Compile a JSON value: an expression array, a legacy filter, a legacy function object or
429    /// a literal.
430    pub fn parse(j: &Json) -> Result<Expr> {
431        match j {
432            Json::Array(a) => match a.first() {
433                Some(Json::String(op)) => Self::parse_op(op, &a[1..]),
434                _ => Ok(Expr::Lit(Val::from_json(j))),
435            },
436            Json::Object(o) if o.contains_key("stops") => Self::parse_function(j),
437            Json::Object(_) => bail!("objects are not expressions"),
438            _ => Ok(Expr::Lit(Val::from_json(j))),
439        }
440    }
441
442    /// Like [`Expr::parse`] for `text-field`, where plain strings may hold `{prop}` tokens.
443    pub fn parse_text(j: &Json) -> Result<Expr> {
444        match j.as_str() {
445            Some(s) if s.contains('{') => {
446                let mut parts = Vec::new();
447                let mut rest = s;
448                while let Some(open) = rest.find('{') {
449                    let Some(close) = rest[open..].find('}') else { break };
450                    if open > 0 {
451                        parts.push(Part::Text(rest[..open].to_string()));
452                    }
453                    parts.push(Part::Prop(rest[open + 1..open + close].to_string()));
454                    rest = &rest[open + close + 1..];
455                }
456                if !rest.is_empty() {
457                    parts.push(Part::Text(rest.to_string()));
458                }
459                Ok(Expr::Template(parts))
460            }
461            _ => Self::parse(j),
462        }
463    }
464
465    /// Legacy `{"stops": ..}` functions: zoom functions, property functions, categorical.
466    fn parse_function(j: &Json) -> Result<Expr> {
467        let input = match j.get("property").and_then(Json::as_str) {
468            Some(p) => Box::new(key_expr(p)),
469            None => Box::new(Expr::Zoom),
470        };
471        let raw = j["stops"].as_array().ok_or_else(|| anyhow!("stops must be an array"))?;
472        let mut st = Vec::new();
473        for s in raw {
474            let (Some(k), Some(v)) = (s.get(0), s.get(1)) else { bail!("a stop must be [input, output]") };
475            if k.is_object() {
476                bail!("zoom-and-property functions are not supported");
477            }
478            st.push((k.clone(), Val::from_json(v)));
479        }
480        let kind = j.get("type").and_then(Json::as_str);
481        let default = Box::new(Expr::Lit(j.get("default").map_or(Val::Null, Val::from_json)));
482        let interpolable = st.first().is_some_and(|(_, v)| matches!(v, Val::Num(_)) || v.as_color().is_some());
483        match kind {
484            Some("categorical") => {
485                Ok(Expr::Match { input, arms: st.into_iter().map(|(k, v)| (vec![Val::from_json(&k)], Expr::Lit(v))).collect(), default })
486            }
487            Some("identity") => Ok(*input),
488            _ => {
489                let nums: Vec<(f64, Expr)> = st
490                    .into_iter()
491                    .map(|(k, v)| k.as_f64().map(|k| (k, Expr::Lit(v))).ok_or_else(|| anyhow!("stop inputs must be numbers")))
492                    .collect::<Result<_>>()?;
493                if nums.is_empty() {
494                    bail!("no stops");
495                }
496                if kind == Some("interval") || (kind.is_none() && !interpolable) {
497                    let first = nums[0].1.clone();
498                    Ok(Expr::Step { input, default: Box::new(first), stops: nums })
499                } else {
500                    let base = j.get("base").and_then(Json::as_f64).unwrap_or(1.0);
501                    Ok(Expr::Interpolate { base, input, stops: nums })
502                }
503            }
504        }
505    }
506
507    fn parse_op(op: &str, a: &[Json]) -> Result<Expr> {
508        let legacy_key = a.first().and_then(Json::as_str);
509        // Legacy filters name the property as a bare string and use only literal operands.
510        let legacy = legacy_key.is_some() && a[1..].iter().all(|v| !v.is_array());
511        let cmp = |c: Cmp| -> Result<Expr> {
512            match a {
513                [_, r] if legacy => Ok(Expr::Cmp(c, Box::new(key_expr(legacy_key.unwrap_or_default())), Box::new(Expr::Lit(Val::from_json(r))))),
514                [l, r] => Ok(Expr::Cmp(c, Box::new(Expr::parse(l)?), Box::new(Expr::parse(r)?))),
515                [l, r, _collator] => Ok(Expr::Cmp(c, Box::new(Expr::parse(l)?), Box::new(Expr::parse(r)?))),
516                _ => bail!("{op}: expected 2 arguments"),
517            }
518        };
519        Ok(match op {
520            "literal" => Expr::Lit(a.first().map_or(Val::Null, Val::from_json)),
521            "get" => match a {
522                [Json::String(k)] => Expr::Get(k.clone()),
523                _ => bail!("get: only [\"get\", \"name\"] is supported"),
524            },
525            "has" => match a {
526                [Json::String(k)] => Expr::Has(k.clone()),
527                _ => bail!("has: only [\"has\", \"name\"] is supported"),
528            },
529            "!has" => match a {
530                [Json::String(k)] => Expr::Not(Box::new(Expr::Has(k.clone()))),
531                _ => bail!("!has: expected a property name"),
532            },
533            "zoom" => Expr::Zoom,
534            "feature-state" => match a {
535                [Json::String(k)] => Expr::FeatureState(k.clone()),
536                _ => bail!("feature-state: only [\"feature-state\", \"key\"] is supported"),
537            },
538            "geometry-type" => Expr::GeomType,
539            "==" => cmp(Cmp::Eq)?,
540            "!=" => cmp(Cmp::Ne)?,
541            "<" => cmp(Cmp::Lt)?,
542            "<=" => cmp(Cmp::Le)?,
543            ">" => cmp(Cmp::Gt)?,
544            ">=" => cmp(Cmp::Ge)?,
545            "all" => Expr::All(args(a)?),
546            "any" => Expr::Any(args(a)?),
547            "none" => Expr::Not(Box::new(Expr::Any(args(a)?))),
548            "!" => Expr::Not(one(a, op)?),
549            "in" | "!in" => {
550                let e = if legacy {
551                    Expr::InList(Box::new(key_expr(legacy_key.unwrap_or_default())), a[1..].iter().map(Val::from_json).collect())
552                } else {
553                    match a {
554                        [needle, hay] => match literal_list(hay) {
555                            Some(list) => Expr::InList(Box::new(Expr::parse(needle)?), list),
556                            None => Expr::In(Box::new(Expr::parse(needle)?), Box::new(Expr::parse(hay)?)),
557                        },
558                        _ => bail!("in: expected 2 arguments"),
559                    }
560                };
561                if op == "!in" { Expr::Not(Box::new(e)) } else { e }
562            }
563            "match" => {
564                if a.len() < 2 || !a.len().is_multiple_of(2) {
565                    bail!("match: expected input, label/output pairs and a default");
566                }
567                let input = Box::new(Expr::parse(&a[0])?);
568                let mut arms = Vec::new();
569                for pair in a[1..a.len() - 1].chunks(2) {
570                    let labels = match &pair[0] {
571                        Json::Array(l) => l.iter().map(Val::from_json).collect(),
572                        v => vec![Val::from_json(v)],
573                    };
574                    arms.push((labels, Expr::parse(&pair[1])?));
575                }
576                Expr::Match { input, arms, default: Box::new(Expr::parse(&a[a.len() - 1])?) }
577            }
578            "case" => {
579                if a.len() % 2 != 1 {
580                    bail!("case: expected condition/output pairs and a default");
581                }
582                let mut arms = Vec::new();
583                for pair in a[..a.len() - 1].chunks(2) {
584                    arms.push((Expr::parse(&pair[0])?, Expr::parse(&pair[1])?));
585                }
586                Expr::Case { arms, default: Box::new(Expr::parse(&a[a.len() - 1])?) }
587            }
588            "coalesce" => Expr::Coalesce(args(a)?),
589            "interpolate" | "interpolate-hcl" | "interpolate-lab" => {
590                if a.len() < 4 {
591                    bail!("interpolate: expected a curve, an input and stops");
592                }
593                let base = match a[0].get(0).and_then(Json::as_str) {
594                    Some("linear") => 1.0,
595                    Some("exponential") => a[0].get(1).and_then(Json::as_f64).unwrap_or(1.0),
596                    // Close enough in a terminal: a cubic bezier between the same stops.
597                    Some("cubic-bezier") => 1.0,
598                    _ => bail!("interpolate: unknown curve"),
599                };
600                Expr::Interpolate { base, input: Box::new(Expr::parse(&a[1])?), stops: stops(&a[2..], op)? }
601            }
602            "step" => {
603                if a.len() < 2 {
604                    bail!("step: expected an input and a default");
605                }
606                Expr::Step { input: Box::new(Expr::parse(&a[0])?), default: Box::new(Expr::parse(&a[1])?), stops: stops(&a[2..], op)? }
607            }
608            "+" => Expr::Math(Math::Add, args(a)?),
609            "-" => Expr::Math(Math::Sub, args(a)?),
610            "*" => Expr::Math(Math::Mul, args(a)?),
611            "/" => Expr::Math(Math::Div, args(a)?),
612            "%" => Expr::Math(Math::Rem, args(a)?),
613            "min" => Expr::Math(Math::Min, args(a)?),
614            "max" => Expr::Math(Math::Max, args(a)?),
615            "concat" => Expr::Concat(args(a)?),
616            // `format` sections become plain text; their style options don't apply here.
617            "format" => Expr::Concat(a.iter().filter(|v| !v.is_object()).map(Expr::parse).collect::<Result<_>>()?),
618            "upcase" => Expr::Upcase(one(a, op)?),
619            "downcase" => Expr::Downcase(one(a, op)?),
620            "to-string" => Expr::ToStr(one(a, op)?),
621            "to-number" => Expr::ToNum(args(a)?),
622            "to-boolean" => Expr::ToBool(one(a, op)?),
623            "to-color" => Expr::ToColor(args(a)?),
624            "length" => Expr::Length(one(a, op)?),
625            "typeof" => Expr::TypeOf(one(a, op)?),
626            "index-of" if (2..=3).contains(&a.len()) => Expr::IndexOf(args(a)?),
627            "slice" if (2..=3).contains(&a.len()) => Expr::Slice(args(a)?),
628            "string" => Expr::Assert(Assert::Str, args(a)?),
629            "number" => Expr::Assert(Assert::Num, args(a)?),
630            "boolean" => Expr::Assert(Assert::Bool, args(a)?),
631            other => bail!("unsupported expression \"{other}\""),
632        })
633    }
634
635    /// Whether the value is the same for every feature and zoom.
636    pub fn is_constant(&self) -> bool {
637        match self {
638            Expr::Lit(_) => true,
639            Expr::Get(_) | Expr::Has(_) | Expr::Zoom | Expr::GeomType | Expr::Template(_) | Expr::FeatureState(_) => false,
640            Expr::Cmp(_, a, b) | Expr::In(a, b) => a.is_constant() && b.is_constant(),
641            Expr::Not(a) | Expr::Upcase(a) | Expr::Downcase(a) | Expr::ToStr(a) | Expr::ToBool(a) | Expr::Length(a) | Expr::TypeOf(a) => a.is_constant(),
642            Expr::InList(a, _) => a.is_constant(),
643            Expr::All(v) | Expr::Any(v) | Expr::Coalesce(v) | Expr::Math(_, v) | Expr::Concat(v) | Expr::ToNum(v) | Expr::ToColor(v) | Expr::Assert(_, v) | Expr::IndexOf(v) | Expr::Slice(v) => {
644                v.iter().all(Expr::is_constant)
645            }
646            Expr::Match { input, arms, default } => input.is_constant() && default.is_constant() && arms.iter().all(|(_, e)| e.is_constant()),
647            Expr::Case { arms, default } => default.is_constant() && arms.iter().all(|(c, e)| c.is_constant() && e.is_constant()),
648            Expr::Interpolate { input, stops, .. } => input.is_constant() && stops.iter().all(|(_, e)| e.is_constant()),
649            Expr::Step { input, default, stops } => input.is_constant() && default.is_constant() && stops.iter().all(|(_, e)| e.is_constant()),
650        }
651    }
652
653    /// Whether the value can change with the zoom level.
654    pub fn uses_zoom(&self) -> bool {
655        self.any(&|e| matches!(e, Expr::Zoom))
656    }
657
658    /// Whether the value reads feature state.
659    pub fn uses_state(&self) -> bool {
660        self.any(&|e| matches!(e, Expr::FeatureState(_)))
661    }
662
663    /// Whether this expression or any inside it matches.
664    fn any(&self, f: &dyn Fn(&Expr) -> bool) -> bool {
665        if f(self) {
666            return true;
667        }
668        match self {
669            Expr::Lit(_) | Expr::Get(_) | Expr::Has(_) | Expr::Zoom | Expr::GeomType | Expr::Template(_) | Expr::FeatureState(_) => false,
670            Expr::Cmp(_, a, b) | Expr::In(a, b) => a.any(f) || b.any(f),
671            Expr::Not(a) | Expr::Upcase(a) | Expr::Downcase(a) | Expr::ToStr(a) | Expr::ToBool(a) | Expr::Length(a) | Expr::TypeOf(a) | Expr::InList(a, _) => a.any(f),
672            Expr::All(v) | Expr::Any(v) | Expr::Coalesce(v) | Expr::Math(_, v) | Expr::Concat(v) | Expr::ToNum(v) | Expr::ToColor(v) | Expr::Assert(_, v) | Expr::IndexOf(v) | Expr::Slice(v) => {
673                v.iter().any(|e| e.any(f))
674            }
675            Expr::Match { input, arms, default } => input.any(f) || default.any(f) || arms.iter().any(|(_, e)| e.any(f)),
676            Expr::Case { arms, default } => default.any(f) || arms.iter().any(|(c, e)| c.any(f) || e.any(f)),
677            Expr::Interpolate { input, stops, .. } => input.any(f) || stops.iter().any(|(_, e)| e.any(f)),
678            Expr::Step { input, default, stops } => input.any(f) || default.any(f) || stops.iter().any(|(_, e)| e.any(f)),
679        }
680    }
681
682    /// Evaluate for one feature at one zoom.
683    pub fn eval(&self, c: &Ctx) -> Val {
684        match self {
685            Expr::Lit(v) => v.clone(),
686            Expr::Get(k) => c.prop(k).map_or(Val::Null, Val::from_prop),
687            Expr::Has(k) => Val::Bool(c.prop(k).is_some()),
688            Expr::FeatureState(k) => c.state.iter().find(|(key, _)| key == k).map_or(Val::Null, |(_, v)| v.clone()),
689            Expr::Zoom => Val::Num(c.zoom),
690            Expr::GeomType => Val::Str(
691                match c.geom {
692                    GeomType::Point => "Point",
693                    GeomType::Line => "LineString",
694                    GeomType::Polygon => "Polygon",
695                    GeomType::Unknown => "Unknown",
696                }
697                .into(),
698            ),
699            Expr::Cmp(op, l, r) => Val::Bool(compare(*op, &l.eval(c), &r.eval(c))),
700            Expr::All(v) => Val::Bool(v.iter().all(|e| e.eval(c).as_bool())),
701            Expr::Any(v) => Val::Bool(v.iter().any(|e| e.eval(c).as_bool())),
702            Expr::Not(e) => Val::Bool(!e.eval(c).as_bool()),
703            Expr::InList(needle, list) => {
704                let n = needle.eval(c);
705                Val::Bool(list.iter().any(|v| loose_eq(v, &n)))
706            }
707            Expr::In(needle, hay) => {
708                let n = needle.eval(c);
709                Val::Bool(match hay.eval(c) {
710                    Val::Array(a) => a.iter().any(|v| loose_eq(v, &n)),
711                    Val::Str(s) => s.contains(&n.to_text()),
712                    _ => false,
713                })
714            }
715            Expr::Match { input, arms, default } => {
716                let v = input.eval(c);
717                arms.iter().find(|(labels, _)| labels.iter().any(|l| loose_eq(l, &v))).map_or_else(|| default.eval(c), |(_, out)| out.eval(c))
718            }
719            Expr::Case { arms, default } => arms.iter().find(|(cond, _)| cond.eval(c).as_bool()).map_or_else(|| default.eval(c), |(_, out)| out.eval(c)),
720            Expr::Coalesce(v) => v.iter().map(|e| e.eval(c)).find(Val::truthy).unwrap_or(Val::Null),
721            Expr::Interpolate { base, input, stops } => {
722                let Some(x) = input.eval(c).as_num() else { return Val::Null };
723                interpolate(*base, x, stops, c)
724            }
725            Expr::Step { input, default, stops } => {
726                let Some(x) = input.eval(c).as_num() else { return default.eval(c) };
727                stops.iter().rev().find(|(s, _)| x >= *s).map_or_else(|| default.eval(c), |(_, e)| e.eval(c))
728            }
729            Expr::Math(op, v) => {
730                let n: Vec<f64> = v.iter().filter_map(|e| e.eval(c).as_num()).collect();
731                if n.len() != v.len() || n.is_empty() {
732                    return Val::Null;
733                }
734                Val::Num(match op {
735                    Math::Add => n.iter().sum(),
736                    Math::Mul => n.iter().product(),
737                    Math::Sub if n.len() == 1 => -n[0],
738                    Math::Sub => n[0] - n[1..].iter().sum::<f64>(),
739                    Math::Div => n[1..].iter().fold(n[0], |a, b| a / b),
740                    Math::Rem => n[1..].iter().fold(n[0], |a, b| a % b),
741                    Math::Min => n.iter().copied().fold(f64::MAX, f64::min),
742                    Math::Max => n.iter().copied().fold(f64::MIN, f64::max),
743                })
744            }
745            Expr::Concat(v) => Val::Str(v.iter().map(|e| e.eval(c).to_text()).collect()),
746            Expr::Upcase(e) => Val::Str(e.eval(c).to_text().to_uppercase()),
747            Expr::Downcase(e) => Val::Str(e.eval(c).to_text().to_lowercase()),
748            Expr::ToStr(e) => Val::Str(e.eval(c).to_text()),
749            Expr::ToNum(v) => v
750                .iter()
751                .find_map(|e| match e.eval(c) {
752                    Val::Num(n) => Some(n),
753                    Val::Str(s) => s.trim().parse().ok(),
754                    Val::Bool(b) => Some(b as u8 as f64),
755                    _ => None,
756                })
757                .map_or(Val::Null, Val::Num),
758            Expr::ToBool(e) => Val::Bool(match e.eval(c) {
759                Val::Num(n) => n != 0.0 && !n.is_nan(),
760                Val::Str(s) => !s.is_empty(),
761                v => v.truthy(),
762            }),
763            Expr::ToColor(v) => v.iter().find_map(|e| e.eval(c).as_color()).map_or(Val::Null, Val::Color),
764            Expr::Length(e) => match e.eval(c) {
765                Val::Str(s) => Val::Num(s.chars().count() as f64),
766                Val::Array(a) => Val::Num(a.len() as f64),
767                _ => Val::Null,
768            },
769            Expr::TypeOf(e) => Val::Str(
770                match e.eval(c) {
771                    Val::Null => "null",
772                    Val::Bool(_) => "boolean",
773                    Val::Num(_) => "number",
774                    Val::Str(_) => "string",
775                    Val::Color(_) => "color",
776                    Val::Array(_) => "array",
777                }
778                .into(),
779            ),
780            Expr::IndexOf(v) => {
781                let (needle, hay) = (v[0].eval(c), v[1].eval(c));
782                let from = v.get(2).and_then(|e| e.eval(c).as_num()).unwrap_or(0.0).max(0.0) as usize;
783                Val::Num(match hay {
784                    Val::Str(h) => {
785                        let (h, n): (Vec<char>, Vec<char>) = (h.chars().collect(), needle.to_text().chars().collect());
786                        (from..=h.len().saturating_sub(n.len())).find(|&i| h.len() >= n.len() && h[i..i + n.len()] == n[..]).map_or(-1.0, |i| i as f64)
787                    }
788                    Val::Array(a) => a.iter().skip(from).position(|x| loose_eq(x, &needle)).map_or(-1.0, |i| (i + from) as f64),
789                    _ => -1.0,
790                })
791            }
792            Expr::Slice(v) => {
793                let input = v[0].eval(c);
794                let len = match &input {
795                    Val::Str(s) => s.chars().count(),
796                    Val::Array(a) => a.len(),
797                    _ => return Val::Null,
798                } as f64;
799                // Negative positions count from the end, as in JavaScript.
800                let pos = |e: Option<&Expr>, default: f64| {
801                    let p = e.and_then(|e| e.eval(c).as_num()).unwrap_or(default);
802                    (if p < 0.0 { len + p } else { p }).clamp(0.0, len) as usize
803                };
804                let (start, end) = (pos(v.get(1), 0.0), pos(v.get(2), len));
805                let end = end.max(start);
806                match input {
807                    Val::Str(s) => Val::Str(s.chars().skip(start).take(end - start).collect()),
808                    Val::Array(a) => Val::Array(a[start..end].to_vec()),
809                    _ => Val::Null,
810                }
811            }
812            Expr::Assert(kind, v) => v
813                .iter()
814                .map(|e| e.eval(c))
815                .find(|x| matches!((kind, x), (Assert::Str, Val::Str(_)) | (Assert::Num, Val::Num(_)) | (Assert::Bool, Val::Bool(_))))
816                .unwrap_or(Val::Null),
817            Expr::Template(parts) => Val::Str(
818                parts
819                    .iter()
820                    .map(|p| match p {
821                        Part::Text(t) => t.clone(),
822                        Part::Prop(k) => c.prop(k).map(|v| v.to_string()).unwrap_or_default(),
823                    })
824                    .collect(),
825            ),
826        }
827    }
828}
829
830fn loose_eq(a: &Val, b: &Val) -> bool {
831    match (a, b) {
832        (Val::Num(x), Val::Num(y)) => x == y,
833        _ => a == b,
834    }
835}
836
837fn compare(op: Cmp, l: &Val, r: &Val) -> bool {
838    use std::cmp::Ordering;
839    let ord: Option<Ordering> = match (l, r) {
840        (Val::Num(a), Val::Num(b)) => a.partial_cmp(b),
841        (Val::Str(a), Val::Str(b)) => Some(a.cmp(b)),
842        (Val::Bool(a), Val::Bool(b)) => Some(a.cmp(b)),
843        _ => None,
844    };
845    match op {
846        Cmp::Eq => ord == Some(Ordering::Equal) || (l == &Val::Null && r == &Val::Null),
847        Cmp::Ne => !(ord == Some(Ordering::Equal) || (l == &Val::Null && r == &Val::Null)),
848        Cmp::Lt => ord == Some(Ordering::Less),
849        Cmp::Le => matches!(ord, Some(Ordering::Less | Ordering::Equal)),
850        Cmp::Gt => ord == Some(Ordering::Greater),
851        Cmp::Ge => matches!(ord, Some(Ordering::Greater | Ordering::Equal)),
852    }
853}
854
855fn interpolate(base: f64, x: f64, stops: &[(f64, Expr)], c: &Ctx) -> Val {
856    let Some(first) = stops.first() else { return Val::Null };
857    if x <= first.0 {
858        return first.1.eval(c);
859    }
860    let Some(i) = stops.iter().position(|(s, _)| *s > x) else {
861        return stops[stops.len() - 1].1.eval(c);
862    };
863    let ((x0, lo), (x1, hi)) = (&stops[i - 1], &stops[i]);
864    let span = x1 - x0;
865    let t = if span <= 0.0 {
866        0.0
867    } else if (base - 1.0).abs() < 1e-9 {
868        (x - x0) / span
869    } else {
870        (base.powf(x - x0) - 1.0) / (base.powf(span) - 1.0)
871    };
872    match (lo.eval(c), hi.eval(c)) {
873        (Val::Num(a), Val::Num(b)) => Val::Num(a + (b - a) * t),
874        (a, b) => match (a.as_color(), b.as_color()) {
875            (Some(ca), Some(cb)) => Val::Color(ca.lerp(cb, t)),
876            _ => a,
877        },
878    }
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884    use serde_json::json;
885
886    fn props(p: &[(&str, Value)]) -> Vec<(String, Value)> {
887        p.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
888    }
889
890    fn eval(e: Json, z: f64, geom: GeomType, p: &[(String, Value)]) -> Val {
891        Expr::parse(&e).unwrap().eval(&Ctx { zoom: z, geom, props: p, state: &[] })
892    }
893
894    #[test]
895    fn colors() {
896        assert_eq!(parse_color("#fff").unwrap().to_u8(), (255, 255, 255));
897        assert_eq!(parse_color("#336699").unwrap().to_u8(), (0x33, 0x66, 0x99));
898        assert!((parse_color("#33669980").unwrap().a - 0.502).abs() < 0.01);
899        assert_eq!(parse_color("rgb(10, 20, 30)").unwrap().to_u8(), (10, 20, 30));
900        assert_eq!(parse_color("rgba(10,20,30,0.5)").unwrap().a, 0.5);
901        assert_eq!(parse_color("hsl(0, 100%, 50%)").unwrap().to_u8(), (255, 0, 0));
902        assert_eq!(parse_color("hsla(120,100%,25%,1)").unwrap().to_u8(), (0, 128, 0));
903        assert_eq!(parse_color("Navy").unwrap().to_u8(), (0, 0, 128));
904        assert!(parse_color("nonsense").is_none());
905    }
906
907    #[test]
908    fn legacy_filters() {
909        let p = props(&[("class", Value::Str("motorway".into())), ("admin_level", Value::Num(2.0))]);
910        let t = |e: Json| eval(e, 10.0, GeomType::Line, &p).as_bool();
911        assert!(t(json!(["==", "class", "motorway"])));
912        assert!(t(json!(["!=", "class", "primary"])));
913        assert!(t(json!(["!=", "missing", "x"])), "a missing property is not equal");
914        assert!(!t(json!(["==", "missing", "x"])));
915        assert!(t(json!(["in", "class", "trunk", "motorway"])));
916        assert!(t(json!(["!in", "class", "path", "track"])));
917        assert!(t(json!(["<=", "admin_level", 4])));
918        assert!(t(json!(["has", "class"])));
919        assert!(t(json!(["!has", "name"])));
920        assert!(t(json!(["==", "$type", "LineString"])));
921        assert!(t(json!(["all", ["==", "class", "motorway"], ["none", ["==", "admin_level", 4]]])));
922    }
923
924    #[test]
925    fn modern_expressions() {
926        let p = props(&[("class", Value::Str("park".into())), ("rank", Value::Num(3.0)), ("name", Value::Str("Mall".into()))]);
927        let e = |j: Json| eval(j, 12.0, GeomType::Polygon, &p);
928        assert!(e(json!(["==", ["get", "class"], "park"])).as_bool());
929        assert!(e(json!(["in", ["get", "class"], ["literal", ["park", "wood"]]])).as_bool());
930        assert!(e(json!(["in", "ar", ["get", "class"]])).as_bool(), "substring");
931        assert_eq!(e(json!(["match", ["get", "class"], ["wood", "park"], "green", "gray"])), Val::Str("green".into()));
932        assert_eq!(e(json!(["case", [">", ["get", "rank"], 5], "big", "small"])), Val::Str("small".into()));
933        assert_eq!(e(json!(["coalesce", ["get", "name:en"], ["get", "name"]])), Val::Str("Mall".into()));
934        assert_eq!(e(json!(["step", ["zoom"], 1, 10, 2, 14, 3])), Val::Num(2.0));
935        assert_eq!(e(json!(["interpolate", ["linear"], ["zoom"], 10, 0, 14, 8])), Val::Num(4.0));
936        assert_eq!(e(json!(["+", ["get", "rank"], 2])), Val::Num(5.0));
937        assert_eq!(e(json!(["concat", ["upcase", ["get", "name"]], "!"])), Val::Str("MALL!".into()));
938        assert_eq!(e(json!(["format", ["get", "name"], {"font-scale": 1.2}])), Val::Str("Mall".into()));
939        assert_eq!(e(json!(["to-number", "x", "7"])), Val::Num(7.0));
940        assert_eq!(e(json!(["typeof", ["get", "rank"]])), Val::Str("number".into()));
941        assert_eq!(e(json!(["index-of", ";", "I 270;US 40"])), Val::Num(5.0));
942        assert_eq!(e(json!(["index-of", "x", "abc"])), Val::Num(-1.0));
943        assert_eq!(e(json!(["index-of", "a", "banana", 2])), Val::Num(3.0));
944        assert_eq!(e(json!(["slice", "I 270;US 40", 0, 5])), Val::Str("I 270".into()));
945        assert_eq!(e(json!(["slice", "I 270", 2])), Val::Str("270".into()));
946        assert_eq!(e(json!(["slice", "abc", -2])), Val::Str("bc".into()));
947        assert_eq!(e(json!(["slice", ["literal", [1, 2, 3]], 1, 2])), Val::Array(vec![Val::Num(2.0)]));
948        let c = e(json!(["interpolate", ["linear"], ["zoom"], 10, "#000000", 14, "#ffffff"])).as_color().unwrap();
949        assert_eq!(c.to_u8(), (128, 128, 128));
950        assert!(Expr::parse(&json!(["distance", 1])).is_err());
951        assert!(Expr::parse(&json!(["interpolate", ["linear"], ["zoom"], 1, 2])).is_ok());
952    }
953
954    #[test]
955    fn exponential_and_functions() {
956        let p = props(&[("kind", Value::Str("a".into()))]);
957        let e = |j: Json, z: f64| eval(j, z, GeomType::Point, &p);
958        // Base 2 between 10 and 12 at 11: (2^1 - 1) / (2^2 - 1) = 1/3 of the way.
959        assert!((e(json!(["interpolate", ["exponential", 2], ["zoom"], 10, 0, 12, 3]), 11.0).as_num().unwrap() - 1.0).abs() < 1e-9);
960        assert_eq!(e(json!({"stops": [[5, 1], [10, 6]]}), 7.0), Val::Num(3.0));
961        assert_eq!(e(json!({"stops": [[5, "none"], [10, "visible"]]}), 11.0), Val::Str("visible".into()));
962        assert_eq!(e(json!({"property": "kind", "type": "categorical", "stops": [["a", 1], ["b", 2]]}), 0.0), Val::Num(1.0));
963        let c = e(json!({"base": 1, "stops": [[0, "#000"], [10, "#fff"]]}), 5.0).as_color().unwrap();
964        assert_eq!(c.to_u8(), (128, 128, 128));
965    }
966
967    #[test]
968    fn text_templates() {
969        let p = props(&[("name", Value::Str("Georgetown".into())), ("ref", Value::Num(7.0))]);
970        let e = Expr::parse_text(&json!("{name} ({ref}) {missing}")).unwrap();
971        assert_eq!(e.eval(&Ctx { zoom: 0.0, geom: GeomType::Point, props: &p, state: &[] }), Val::Str("Georgetown (7) ".into()));
972        assert_eq!(Expr::parse_text(&json!("plain")).unwrap(), Expr::Lit(Val::Str("plain".into())));
973        assert!(!Expr::parse(&json!(["get", "name"])).unwrap().is_constant());
974        assert!(Expr::parse(&json!(["concat", "a", "b"])).unwrap().is_constant());
975        let st = vec![("hover".to_string(), Val::Bool(true))];
976        let fs = Expr::parse(&json!(["case", ["boolean", ["feature-state", "hover"], false], "on", "off"])).unwrap();
977        assert!(fs.uses_state() && !fs.is_constant());
978        assert_eq!(fs.eval(&Ctx { zoom: 0.0, geom: GeomType::Point, props: &p, state: &st }), Val::Str("on".into()));
979        assert_eq!(fs.eval(&Ctx { zoom: 0.0, geom: GeomType::Point, props: &p, state: &[] }), Val::Str("off".into()));
980        assert!(Expr::parse(&json!(["interpolate", ["linear"], ["zoom"], 5, 1, 10, 2])).unwrap().uses_zoom());
981        assert!(Expr::parse(&json!({"stops": [[5, "#000"], [10, "#fff"]]})).unwrap().uses_zoom());
982        assert!(Expr::parse(&json!(["all", ["==", "class", "x"], [">=", ["zoom"], 14]])).unwrap().uses_zoom());
983        assert!(!Expr::parse(&json!(["match", ["get", "class"], "a", "#fff", "#000"])).unwrap().uses_zoom());
984    }
985}