maplibre/style/
layer.rs

1//! Vector tile layer drawing utilities.
2
3use std::{
4    collections::HashMap,
5    hash::{Hash, Hasher},
6};
7
8use cint::{Alpha, EncodedSrgb};
9use csscolorparser::Color;
10use serde::{Deserialize, Deserializer, Serialize};
11
12#[derive(Serialize, Deserialize, Debug, Clone)]
13#[serde(untagged)]
14pub enum StyleProperty<T> {
15    Constant(T),
16    Expression(serde_json::Value),
17}
18
19impl<T: std::str::FromStr + Clone> StyleProperty<T> {
20    pub fn evaluate(&self, feature_properties: &HashMap<String, String>) -> Option<T> {
21        match self {
22            StyleProperty::Constant(value) => Some(value.clone()),
23            StyleProperty::Expression(expr) => {
24                if let Some(arr) = expr.as_array() {
25                    if let Some(op) = arr.get(0).and_then(|v| v.as_str()) {
26                        if op == "match" && arr.len() > 3 {
27                            // Extract the getter e.g. ["get", "ADM0_A3"]
28                            if let Some(get_arr) = arr.get(1).and_then(|v| v.as_array()) {
29                                if get_arr.get(0).and_then(|v| v.as_str()) == Some("get") {
30                                    if let Some(prop_name) = get_arr.get(1).and_then(|v| v.as_str())
31                                    {
32                                        let feature_val_opt = feature_properties.get(prop_name);
33
34                                        // If property is missing, skip match pairs and return fallback
35                                        if feature_val_opt.is_none() {
36                                            if let Some(fallback) =
37                                                arr.last().and_then(|v| v.as_str())
38                                            {
39                                                return fallback.parse::<T>().ok();
40                                            }
41                                            return None;
42                                        }
43
44                                        let feature_val = feature_val_opt.unwrap();
45
46                                        // Search the match array pairs
47                                        let mut i = 2;
48                                        while i < arr.len() - 1 {
49                                            if let Some(match_keys) =
50                                                arr.get(i).and_then(|v| v.as_array())
51                                            {
52                                                // Does this feature_val exist in the match keys?
53                                                let matches = match_keys.iter().any(|k| {
54                                                    k.as_str() == Some(feature_val.as_str())
55                                                });
56                                                if matches {
57                                                    if let Some(color_str) =
58                                                        arr.get(i + 1).and_then(|v| v.as_str())
59                                                    {
60                                                        return color_str.parse::<T>().ok();
61                                                    }
62                                                }
63                                            }
64                                            i += 2;
65                                        }
66                                        // Fallback (last element)
67                                        if i == arr.len() - 1 {
68                                            if let Some(fallback) =
69                                                arr.get(i).and_then(|v| v.as_str())
70                                            {
71                                                return fallback.parse::<T>().ok();
72                                            }
73                                        }
74                                    }
75                                }
76                            }
77                        }
78                    }
79                }
80                None
81            }
82        }
83    }
84
85    pub fn deserialize_color_or_none<'de, D>(
86        deserializer: D,
87    ) -> Result<Option<StyleProperty<T>>, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        // For Color types, allow either a raw color string, or an expression value.
92        let v = serde_json::Value::deserialize(deserializer).map_err(serde::de::Error::custom)?;
93        if let Some(s) = v.as_str() {
94            if let Ok(color) = s.parse::<T>() {
95                return Ok(Some(StyleProperty::Constant(color)));
96            }
97        }
98        // If it's a structural generic expression like match arrays
99        if v.is_array() {
100            return Ok(Some(StyleProperty::Expression(v)));
101        }
102        Ok(None)
103    }
104}
105
106impl StyleProperty<f32> {
107    pub fn deserialize_f32_or_none<'de, D>(
108        deserializer: D,
109    ) -> Result<Option<StyleProperty<f32>>, D::Error>
110    where
111        D: Deserializer<'de>,
112    {
113        let v = serde_json::Value::deserialize(deserializer).map_err(serde::de::Error::custom)?;
114        if let Some(f) = v.as_f64() {
115            return Ok(Some(StyleProperty::Constant(f as f32)));
116        }
117        if v.is_array() {
118            return Ok(Some(StyleProperty::Expression(v)));
119        }
120        // Handle {"stops": [[zoom, value], ...]} format
121        if v.is_object() {
122            return Ok(Some(StyleProperty::Expression(v)));
123        }
124        Ok(None)
125    }
126
127    /// Evaluate a zoom-dependent f32 property at the given zoom level.
128    /// Supports constants and `{"stops": [[z0, v0], [z1, v1], ...]}`.
129    pub fn evaluate_at_zoom(&self, zoom: f32) -> f32 {
130        match self {
131            StyleProperty::Constant(v) => *v,
132            StyleProperty::Expression(expr) => {
133                let stops = expr
134                    .get("stops")
135                    .and_then(|s| s.as_array())
136                    .or_else(|| expr.as_array());
137                let Some(stops) = stops else {
138                    return 1.0;
139                };
140                // Parse stops as [(zoom, value), ...]
141                let parsed: Vec<(f32, f32)> = stops
142                    .iter()
143                    .filter_map(|stop| {
144                        let arr = stop.as_array()?;
145                        let z = arr.first()?.as_f64()? as f32;
146                        let v = arr.get(1)?.as_f64()? as f32;
147                        Some((z, v))
148                    })
149                    .collect();
150
151                if parsed.is_empty() {
152                    return 1.0;
153                }
154                if zoom <= parsed[0].0 {
155                    return parsed[0].1;
156                }
157                if zoom >= parsed[parsed.len() - 1].0 {
158                    return parsed[parsed.len() - 1].1;
159                }
160                // Linear interpolation between stops
161                for window in parsed.windows(2) {
162                    let (z0, v0) = window[0];
163                    let (z1, v1) = window[1];
164                    if zoom >= z0 && zoom <= z1 {
165                        let t = (zoom - z0) / (z1 - z0);
166                        return v0 + t * (v1 - v0);
167                    }
168                }
169                parsed[parsed.len() - 1].1
170            }
171        }
172    }
173}
174
175#[derive(Serialize, Deserialize, Debug, Clone)]
176pub struct BackgroundPaint {
177    #[serde(rename = "background-color")]
178    #[serde(
179        default,
180        deserialize_with = "StyleProperty::<Color>::deserialize_color_or_none"
181    )]
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub background_color: Option<StyleProperty<Color>>,
184    // TODO a lot
185}
186
187#[derive(Serialize, Deserialize, Debug, Clone)]
188pub struct FillPaint {
189    #[serde(rename = "fill-color")]
190    #[serde(
191        default,
192        deserialize_with = "StyleProperty::<Color>::deserialize_color_or_none"
193    )]
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub fill_color: Option<StyleProperty<Color>>,
196    // TODO a lot
197}
198
199#[derive(Serialize, Deserialize, Debug, Clone)]
200pub struct LinePaint {
201    #[serde(rename = "line-color")]
202    #[serde(
203        default,
204        deserialize_with = "StyleProperty::<Color>::deserialize_color_or_none"
205    )]
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub line_color: Option<StyleProperty<Color>>,
208
209    #[serde(rename = "line-width")]
210    #[serde(
211        default,
212        deserialize_with = "StyleProperty::<f32>::deserialize_f32_or_none"
213    )]
214    pub line_width: Option<StyleProperty<f32>>,
215    // TODO a lot
216}
217
218#[derive(Serialize, Deserialize, Debug, Clone)]
219pub enum RasterResampling {
220    #[serde(rename = "linear")]
221    Linear,
222    #[serde(rename = "nearest")]
223    Nearest,
224}
225
226/// Raster tile layer description
227#[derive(Serialize, Deserialize, Debug, Clone)]
228pub struct RasterPaint {
229    #[serde(rename = "raster-brightness-max")]
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub raster_brightness_max: Option<f32>,
232    #[serde(rename = "raster-brightness-min")]
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub raster_brightness_min: Option<f32>,
235    #[serde(rename = "raster-contrast")]
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub raster_contrast: Option<f32>,
238    #[serde(rename = "raster-fade-duration")]
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub raster_fade_duration: Option<u32>,
241    #[serde(rename = "raster-hue-rotate")]
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub raster_hue_rotate: Option<f32>,
244    #[serde(rename = "raster-opacity")]
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub raster_opacity: Option<f32>,
247    #[serde(rename = "raster-resampling")]
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub raster_resampling: Option<RasterResampling>,
250    #[serde(rename = "raster-saturation")]
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub raster_saturation: Option<f32>,
253}
254
255impl Default for RasterPaint {
256    fn default() -> Self {
257        RasterPaint {
258            raster_brightness_max: Some(1.0),
259            raster_brightness_min: Some(0.0),
260            raster_contrast: Some(0.0),
261            raster_fade_duration: Some(0),
262            raster_hue_rotate: Some(0.0),
263            raster_opacity: Some(1.0),
264            raster_resampling: Some(RasterResampling::Linear),
265            raster_saturation: Some(0.0),
266        }
267    }
268}
269
270#[derive(Serialize, Deserialize, Debug, Clone)]
271pub struct SymbolPaint {
272    #[serde(rename = "text-field")]
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub text_field: Option<String>,
275
276    #[serde(rename = "text-size")]
277    #[serde(
278        default,
279        deserialize_with = "StyleProperty::<f32>::deserialize_f32_or_none"
280    )]
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub text_size: Option<StyleProperty<f32>>,
283    // TODO a lot
284}
285
286/// Extract the property name from a text-field template string like "{NAME}" → "NAME".
287/// If no braces, returns the string as-is.
288fn extract_text_field_property(template: &str) -> String {
289    let trimmed = template.trim();
290    if trimmed.starts_with('{') && trimmed.ends_with('}') {
291        trimmed[1..trimmed.len() - 1].to_string()
292    } else {
293        trimmed.to_string()
294    }
295}
296
297/// Extract a text-field property name from a layout JSON value.
298/// Handles both:
299///   - `"text-field": "{NAME}"` (constant string)
300///   - `"text-field": {"stops": [[2, "{ABBREV}"], [4, "{NAME}"]]}` (zoom-dependent)
301fn parse_text_field_from_layout(layout: &serde_json::Value) -> Option<String> {
302    let tf = layout.get("text-field")?;
303    if let Some(s) = tf.as_str() {
304        return Some(extract_text_field_property(s));
305    }
306    // Zoom-dependent: use the last stop's value (highest zoom = most detailed)
307    if let Some(stops) = tf.get("stops").and_then(|v| v.as_array()) {
308        if let Some(last_stop) = stops.last() {
309            if let Some(s) = last_stop.get(1).and_then(|v| v.as_str()) {
310                return Some(extract_text_field_property(s));
311            }
312        }
313    }
314    None
315}
316
317/// Extract text-size from a layout JSON value.
318/// Handles constant numbers and zoom-dependent `{"stops": [[z, size], ...]}`.
319fn parse_text_size_from_layout(layout: &serde_json::Value) -> Option<StyleProperty<f32>> {
320    let ts = layout.get("text-size")?;
321    if let Some(f) = ts.as_f64() {
322        return Some(StyleProperty::Constant(f as f32));
323    }
324    // Object with stops or array
325    if ts.is_object() || ts.is_array() {
326        return Some(StyleProperty::Expression(ts.clone()));
327    }
328    None
329}
330
331/// The different types of paints.
332#[derive(Serialize, Deserialize, Debug, Clone)]
333#[serde(tag = "type", content = "paint")]
334pub enum LayerPaint {
335    #[serde(rename = "background")]
336    Background(BackgroundPaint),
337    #[serde(rename = "line")]
338    Line(LinePaint),
339    #[serde(rename = "fill")]
340    Fill(FillPaint),
341    #[serde(rename = "raster")]
342    Raster(RasterPaint),
343    #[serde(rename = "symbol")]
344    Symbol(SymbolPaint),
345}
346
347impl LayerPaint {
348    pub fn get_color(&self) -> Option<Alpha<EncodedSrgb<f32>>> {
349        match self {
350            LayerPaint::Background(paint) => paint.background_color.as_ref().and_then(|property| {
351                if let StyleProperty::Constant(color) = property {
352                    Some(color.clone().into())
353                } else {
354                    None // Expression types have no single static color
355                }
356            }),
357            LayerPaint::Line(paint) => paint.line_color.as_ref().and_then(|property| {
358                if let StyleProperty::Constant(color) = property {
359                    Some(color.clone().into())
360                } else {
361                    None
362                }
363            }),
364            LayerPaint::Fill(paint) => paint.fill_color.as_ref().and_then(|property| {
365                if let StyleProperty::Constant(color) = property {
366                    Some(color.clone().into())
367                } else {
368                    None
369                }
370            }),
371            LayerPaint::Raster(_) => None,
372            LayerPaint::Symbol(_) => None,
373        }
374    }
375}
376
377/// Stores all the styles for a specific layer.
378#[derive(Debug, Clone)]
379pub struct StyleLayer {
380    pub index: u32,
381    pub id: String,
382    pub type_: String,
383    pub filter: Option<serde_json::Value>,
384    pub maxzoom: Option<u8>,
385    pub minzoom: Option<u8>,
386    pub metadata: Option<HashMap<String, String>>,
387    pub paint: Option<LayerPaint>,
388    pub source: Option<String>,
389    pub source_layer: Option<String>,
390}
391
392impl Serialize for StyleLayer {
393    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
394    where
395        S: serde::Serializer,
396    {
397        use serde::ser::SerializeMap;
398        // Count non-None optional fields
399        let mut count = 2; // id + type are always present
400        if self.filter.is_some() {
401            count += 1;
402        }
403        if self.maxzoom.is_some() {
404            count += 1;
405        }
406        if self.minzoom.is_some() {
407            count += 1;
408        }
409        if self.metadata.is_some() {
410            count += 1;
411        }
412        if self.paint.is_some() {
413            count += 1;
414        }
415        if self.source.is_some() {
416            count += 1;
417        }
418        if self.source_layer.is_some() {
419            count += 1;
420        }
421        let mut map = serializer.serialize_map(Some(count))?;
422        map.serialize_entry("id", &self.id)?;
423        map.serialize_entry("type", &self.type_)?;
424        if let Some(ref filter) = self.filter {
425            map.serialize_entry("filter", filter)?;
426        }
427        if let Some(ref maxzoom) = self.maxzoom {
428            map.serialize_entry("maxzoom", maxzoom)?;
429        }
430        if let Some(ref minzoom) = self.minzoom {
431            map.serialize_entry("minzoom", minzoom)?;
432        }
433        if let Some(ref metadata) = self.metadata {
434            map.serialize_entry("metadata", metadata)?;
435        }
436        if let Some(ref paint) = self.paint {
437            // Serialize just the inner paint data (without the LayerPaint tag)
438            match paint {
439                LayerPaint::Background(p) => map.serialize_entry("paint", p)?,
440                LayerPaint::Line(p) => map.serialize_entry("paint", p)?,
441                LayerPaint::Fill(p) => map.serialize_entry("paint", p)?,
442                LayerPaint::Raster(p) => map.serialize_entry("paint", p)?,
443                LayerPaint::Symbol(p) => map.serialize_entry("paint", p)?,
444            }
445        }
446        if let Some(ref source) = self.source {
447            map.serialize_entry("source", source)?;
448        }
449        if let Some(ref source_layer) = self.source_layer {
450            map.serialize_entry("source-layer", source_layer)?;
451        }
452        map.end()
453    }
454}
455
456#[derive(Deserialize)]
457struct StyleLayerDef {
458    id: String,
459    #[serde(rename = "type")]
460    type_: String,
461    filter: Option<serde_json::Value>,
462    maxzoom: Option<u8>,
463    minzoom: Option<u8>,
464    metadata: Option<HashMap<String, String>>,
465    source: Option<String>,
466    #[serde(rename = "source-layer")]
467    source_layer: Option<String>,
468    paint: Option<serde_json::Value>,
469    layout: Option<serde_json::Value>,
470}
471
472impl<'de> serde::Deserialize<'de> for StyleLayer {
473    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
474    where
475        D: serde::Deserializer<'de>,
476    {
477        let def = StyleLayerDef::deserialize(deserializer)?;
478
479        let paint = if let Some(p) = def.paint {
480            match def.type_.as_str() {
481                "background" => serde_json::from_value(p.clone())
482                    .map(LayerPaint::Background)
483                    .ok(),
484                "line" => serde_json::from_value(p.clone())
485                    .map(LayerPaint::Line)
486                    .map_err(|e| log::error!("line paint failed {}: {:?}", def.id, e))
487                    .ok(),
488                "fill" => serde_json::from_value(p.clone())
489                    .map(LayerPaint::Fill)
490                    .map_err(|e| log::error!("fill paint failed {}: {:?}", def.id, e))
491                    .ok(),
492                "raster" => serde_json::from_value(p.clone())
493                    .map(LayerPaint::Raster)
494                    .ok(),
495                "symbol" => {
496                    let mut paint: Option<SymbolPaint> = serde_json::from_value(p.clone())
497                        .map_err(|e| log::error!("symbol paint failed {}: {:?}", def.id, e))
498                        .ok();
499                    // text-field and text-size live in layout, not paint — merge them in
500                    if let (Some(sp), Some(layout)) = (paint.as_mut(), def.layout.as_ref()) {
501                        if sp.text_field.is_none() {
502                            sp.text_field = parse_text_field_from_layout(layout);
503                        }
504                        if sp.text_size.is_none() {
505                            sp.text_size = parse_text_size_from_layout(layout);
506                        }
507                    }
508                    paint.map(LayerPaint::Symbol)
509                }
510                _ => None,
511            }
512        } else if def.type_ == "symbol" {
513            // Symbol layers may have no paint but still have layout with text-field/text-size
514            let text_field = def.layout.as_ref().and_then(parse_text_field_from_layout);
515            let text_size = def.layout.as_ref().and_then(parse_text_size_from_layout);
516            Some(LayerPaint::Symbol(SymbolPaint {
517                text_field,
518                text_size,
519            }))
520        } else {
521            None
522        };
523
524        Ok(StyleLayer {
525            index: 0,
526            id: def.id,
527            type_: def.type_,
528            filter: def.filter,
529            maxzoom: def.maxzoom,
530            minzoom: def.minzoom,
531            metadata: def.metadata,
532            paint,
533            source: def.source,
534            source_layer: def.source_layer,
535        })
536    }
537}
538
539impl Eq for StyleLayer {}
540impl PartialEq for StyleLayer {
541    fn eq(&self, other: &Self) -> bool {
542        self.id.eq(&other.id)
543    }
544}
545
546impl Hash for StyleLayer {
547    fn hash<H: Hasher>(&self, state: &mut H) {
548        self.id.hash(state)
549    }
550}
551
552impl Default for StyleLayer {
553    fn default() -> Self {
554        Self {
555            index: 0,
556            id: "id".to_string(),
557            type_: "background".to_string(),
558            filter: None,
559            maxzoom: None,
560            minzoom: None,
561            metadata: None,
562            paint: None,
563            source: None,
564            source_layer: Some("does not exist".to_string()),
565        }
566    }
567}
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572
573    #[test]
574    fn test_evaluate_match_missing_property_returns_fallback() {
575        let json = r#"
576        [
577            "match",
578            ["get", "ADM0_A3"],
579            ["ARM", "ATG"],
580            "rgba(1, 2, 3, 1)",
581            "rgba(9, 9, 9, 1)"
582        ]
583        "#;
584        let expr: serde_json::Value = serde_json::from_str(json).unwrap();
585        let prop: StyleProperty<csscolorparser::Color> = StyleProperty::Expression(expr);
586
587        // Feature that does NOT have the property → should return the JSON fallback color
588        let empty_props = HashMap::new();
589        let color = prop.evaluate(&empty_props).unwrap();
590        assert_eq!(color.to_rgba8(), [9, 9, 9, 255]);
591    }
592
593    #[test]
594    fn test_evaluate_match() {
595        let json = r#"
596        [
597            "match",
598            ["get", "ADM0_A3"],
599            ["ARM", "ATG"],
600            "rgba(1, 2, 3, 1)",
601            "rgba(0, 0, 0, 1)"
602        ]
603        "#;
604        let expr: serde_json::Value = serde_json::from_str(json).unwrap();
605        let prop: StyleProperty<csscolorparser::Color> = StyleProperty::Expression(expr);
606
607        let mut feature_properties = HashMap::new();
608        feature_properties.insert("ADM0_A3".to_string(), "ARM".to_string());
609
610        let color = prop.evaluate(&feature_properties).unwrap();
611        assert_eq!(color.to_rgba8(), [1, 2, 3, 255]);
612    }
613
614    #[test]
615    fn test_symbol_text_field_from_layout() {
616        let json = r#"{
617            "id": "countries-label",
618            "type": "symbol",
619            "paint": {
620                "text-color": "rgba(8, 37, 77, 1)"
621            },
622            "layout": {
623                "text-field": "{NAME}",
624                "text-font": ["Open Sans Semibold"]
625            },
626            "source": "maplibre",
627            "source-layer": "centroids"
628        }"#;
629        let layer: StyleLayer = serde_json::from_str(json).unwrap();
630        assert_eq!(layer.type_, "symbol");
631        match &layer.paint {
632            Some(LayerPaint::Symbol(sp)) => {
633                assert_eq!(sp.text_field.as_deref(), Some("NAME"));
634            }
635            other => panic!("expected Symbol paint, got {:?}", other),
636        }
637    }
638
639    #[test]
640    fn test_symbol_text_field_zoom_dependent() {
641        let json = r#"{
642            "id": "test-label",
643            "type": "symbol",
644            "paint": {},
645            "layout": {
646                "text-field": {"stops": [[2, "{ABBREV}"], [4, "{NAME}"]]}
647            },
648            "source": "maplibre",
649            "source-layer": "centroids"
650        }"#;
651        let layer: StyleLayer = serde_json::from_str(json).unwrap();
652        match &layer.paint {
653            Some(LayerPaint::Symbol(sp)) => {
654                // Should pick the last stop (highest zoom) → NAME
655                assert_eq!(sp.text_field.as_deref(), Some("NAME"));
656            }
657            other => panic!("expected Symbol paint, got {:?}", other),
658        }
659    }
660
661    #[test]
662    fn test_demotiles_symbol_layers_have_text_field() {
663        let style: crate::style::Style = Default::default();
664        for layer in &style.layers {
665            if layer.type_ == "symbol" {
666                match &layer.paint {
667                    Some(LayerPaint::Symbol(sp)) => {
668                        assert!(
669                            sp.text_field.is_some(),
670                            "symbol layer '{}' should have text_field parsed from layout",
671                            layer.id
672                        );
673                    }
674                    _ => panic!("symbol layer '{}' has no Symbol paint", layer.id),
675                }
676            }
677        }
678    }
679}