Skip to main content

oxigdal_services/
style.rs

1//! Mapbox GL Style Specification v8 core types.
2//!
3//! Implements the Mapbox GL Style Spec v8 for dynamic map style rendering,
4//! including style documents, sources, layers, filters, expressions, paint
5//! properties, layout properties, and style validation/rendering utilities.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12// ─────────────────────────────────────────────────────────────────────────────
13// Error types
14// ─────────────────────────────────────────────────────────────────────────────
15
16/// Errors that can occur when parsing or validating a Mapbox GL style.
17#[derive(Debug, Error)]
18pub enum StyleError {
19    /// Style version must be 8.
20    #[error("invalid style version {0}; must be 8")]
21    InvalidVersion(u8),
22
23    /// Encountered an unknown layer type string.
24    #[error("unknown layer type: {0}")]
25    UnknownLayerType(String),
26
27    /// Failed to parse a CSS color string.
28    #[error("color parse error: {0}")]
29    ColorParseError(String),
30
31    /// Failed to parse a filter expression.
32    #[error("invalid filter: {0}")]
33    InvalidFilter(String),
34
35    /// A serde_json serialisation/deserialisation error.
36    #[error("serde error: {0}")]
37    SerdeError(#[from] serde_json::Error),
38}
39
40// ─────────────────────────────────────────────────────────────────────────────
41// Root style document
42// ─────────────────────────────────────────────────────────────────────────────
43
44/// Root Mapbox GL Style Specification v8 document.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct StyleSpec {
48    /// Must be 8.
49    pub version: u8,
50    /// Human-readable name for the style.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub name: Option<String>,
53    /// Arbitrary metadata.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub metadata: Option<serde_json::Value>,
56    /// Default map center `[longitude, latitude]`.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub center: Option<[f64; 2]>,
59    /// Default zoom level.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub zoom: Option<f64>,
62    /// Default bearing (degrees clockwise from north).
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub bearing: Option<f64>,
65    /// Default pitch (degrees toward horizon).
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub pitch: Option<f64>,
68    /// Global light source settings.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub light: Option<Light>,
71    /// Data sources available to layers.
72    pub sources: HashMap<String, Source>,
73    /// URL template for sprite images.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub sprite: Option<String>,
76    /// URL template for glyph PBF files (`{fontstack}` / `{range}`).
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub glyphs: Option<String>,
79    /// Default transition options.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub transition: Option<Transition>,
82    /// Ordered list of rendering layers.
83    pub layers: Vec<Layer>,
84}
85
86// ─────────────────────────────────────────────────────────────────────────────
87// Sources
88// ─────────────────────────────────────────────────────────────────────────────
89
90/// A data source referenced by one or more layers.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "type", rename_all = "kebab-case")]
93pub enum Source {
94    /// Vector tile source.
95    Vector {
96        /// TileJSON URL or direct tile URL.
97        #[serde(skip_serializing_if = "Option::is_none")]
98        url: Option<String>,
99        /// Explicit tile URL templates.
100        #[serde(skip_serializing_if = "Option::is_none")]
101        tiles: Option<Vec<String>>,
102        /// Minimum zoom level.
103        #[serde(rename = "minzoom", skip_serializing_if = "Option::is_none")]
104        min_zoom: Option<u8>,
105        /// Maximum zoom level.
106        #[serde(rename = "maxzoom", skip_serializing_if = "Option::is_none")]
107        max_zoom: Option<u8>,
108        /// HTML attribution string.
109        #[serde(skip_serializing_if = "Option::is_none")]
110        attribution: Option<String>,
111    },
112    /// Raster tile source.
113    Raster {
114        /// TileJSON URL.
115        #[serde(skip_serializing_if = "Option::is_none")]
116        url: Option<String>,
117        /// Explicit tile URL templates.
118        #[serde(skip_serializing_if = "Option::is_none")]
119        tiles: Option<Vec<String>>,
120        /// Tile size in pixels.
121        #[serde(rename = "tileSize", skip_serializing_if = "Option::is_none")]
122        tile_size: Option<u32>,
123        /// Minimum zoom level.
124        #[serde(rename = "minzoom", skip_serializing_if = "Option::is_none")]
125        min_zoom: Option<u8>,
126        /// Maximum zoom level.
127        #[serde(rename = "maxzoom", skip_serializing_if = "Option::is_none")]
128        max_zoom: Option<u8>,
129    },
130    /// Raster DEM elevation source.
131    #[serde(rename = "raster-dem")]
132    RasterDem {
133        /// TileJSON URL.
134        #[serde(skip_serializing_if = "Option::is_none")]
135        url: Option<String>,
136        /// Elevation encoding scheme.
137        #[serde(default)]
138        encoding: DemEncoding,
139    },
140    /// GeoJSON data source.
141    #[serde(rename = "geojson")]
142    GeoJson {
143        /// Inline GeoJSON or URL.
144        data: serde_json::Value,
145        /// Maximum zoom for tile index.
146        #[serde(rename = "maxzoom", skip_serializing_if = "Option::is_none")]
147        max_zoom: Option<u8>,
148        /// Enable feature clustering.
149        #[serde(skip_serializing_if = "Option::is_none")]
150        cluster: Option<bool>,
151        /// Cluster radius in pixels.
152        #[serde(rename = "clusterRadius", skip_serializing_if = "Option::is_none")]
153        cluster_radius: Option<u32>,
154    },
155    /// Image overlay source.
156    Image {
157        /// Image URL.
158        url: String,
159        /// `[[lng,lat]; 4]` corner coordinates (TL, TR, BR, BL).
160        coordinates: [[f64; 2]; 4],
161    },
162    /// Video overlay source.
163    Video {
164        /// Video URLs (multiple formats for browser compatibility).
165        urls: Vec<String>,
166        /// Corner coordinates.
167        coordinates: [[f64; 2]; 4],
168    },
169}
170
171/// Elevation encoding for raster-dem sources.
172#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
173#[serde(rename_all = "lowercase")]
174pub enum DemEncoding {
175    /// Mapbox Terrain-RGB encoding.
176    #[default]
177    Mapbox,
178    /// Terrarium encoding.
179    Terrarium,
180}
181
182// ─────────────────────────────────────────────────────────────────────────────
183// Layers
184// ─────────────────────────────────────────────────────────────────────────────
185
186/// A single rendering layer.
187#[derive(Debug, Clone, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct Layer {
190    /// Unique layer identifier.
191    pub id: String,
192    /// The rendering type.
193    #[serde(rename = "type")]
194    pub layer_type: LayerType,
195    /// Source name from [`StyleSpec::sources`].
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub source: Option<String>,
198    /// Source layer within a vector tile source.
199    #[serde(rename = "source-layer", skip_serializing_if = "Option::is_none")]
200    pub source_layer: Option<String>,
201    /// Minimum zoom level at which the layer is visible.
202    #[serde(rename = "minzoom", skip_serializing_if = "Option::is_none")]
203    pub min_zoom: Option<f64>,
204    /// Maximum zoom level at which the layer is visible.
205    #[serde(rename = "maxzoom", skip_serializing_if = "Option::is_none")]
206    pub max_zoom: Option<f64>,
207    /// Feature filter expression.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub filter: Option<Filter>,
210    /// Layout properties.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub layout: Option<Layout>,
213    /// Paint properties.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub paint: Option<Paint>,
216}
217
218/// Layer rendering type.
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
220#[serde(rename_all = "kebab-case")]
221pub enum LayerType {
222    /// Solid-color background.
223    Background,
224    /// Filled polygon layer.
225    Fill,
226    /// Line/stroke layer.
227    Line,
228    /// Symbol (icon + text) layer.
229    Symbol,
230    /// Raster image layer.
231    Raster,
232    /// Circle layer.
233    Circle,
234    /// Extruded fill (3-D buildings).
235    FillExtrusion,
236    /// Heatmap density layer.
237    Heatmap,
238    /// Hillshade terrain layer.
239    Hillshade,
240    /// Sky / atmosphere layer.
241    Sky,
242}
243
244// ─────────────────────────────────────────────────────────────────────────────
245// Filters
246// ─────────────────────────────────────────────────────────────────────────────
247
248/// Geometry type for geometry-type filters.
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
250pub enum GeomFilter {
251    /// Point geometry.
252    Point,
253    /// LineString geometry.
254    LineString,
255    /// Polygon geometry.
256    Polygon,
257}
258
259/// Mapbox GL filter expression (v2).
260#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(untagged)]
262pub enum Filter {
263    /// All sub-filters must match.
264    All(#[serde(skip)] Vec<Filter>),
265    /// At least one sub-filter must match.
266    Any(#[serde(skip)] Vec<Filter>),
267    /// No sub-filter must match.
268    None(#[serde(skip)] Vec<Filter>),
269    /// Property equals value.
270    Eq {
271        /// Feature property name.
272        property: String,
273        /// Expected value.
274        value: serde_json::Value,
275    },
276    /// Property not equal to value.
277    Ne {
278        /// Feature property name.
279        property: String,
280        /// Excluded value.
281        value: serde_json::Value,
282    },
283    /// Property less than value.
284    Lt {
285        /// Feature property name.
286        property: String,
287        /// Threshold.
288        value: f64,
289    },
290    /// Property less than or equal to value.
291    Lte {
292        /// Feature property name.
293        property: String,
294        /// Threshold.
295        value: f64,
296    },
297    /// Property greater than value.
298    Gt {
299        /// Feature property name.
300        property: String,
301        /// Threshold.
302        value: f64,
303    },
304    /// Property greater than or equal to value.
305    Gte {
306        /// Feature property name.
307        property: String,
308        /// Threshold.
309        value: f64,
310    },
311    /// Property value is in a set.
312    In {
313        /// Feature property name.
314        property: String,
315        /// Allowed values.
316        values: Vec<serde_json::Value>,
317    },
318    /// Property exists.
319    Has(String),
320    /// Property does not exist.
321    NotHas(String),
322    /// Geometry type matches.
323    GeometryType(GeomFilter),
324}
325
326// ─────────────────────────────────────────────────────────────────────────────
327// Expressions
328// ─────────────────────────────────────────────────────────────────────────────
329
330/// Interpolation type for [`Expression::Interpolate`].
331#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(rename_all = "lowercase")]
333pub enum Interpolation {
334    /// Linear interpolation.
335    Linear,
336    /// Exponential interpolation with base.
337    Exponential(f64),
338    /// Cubic-bezier interpolation with four control-point components.
339    CubicBezier([f64; 4]),
340}
341
342/// Mapbox GL expression (core subset).
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(untagged)]
345pub enum Expression {
346    /// `["get", property]` — read a feature property.
347    Get(String),
348    /// `["has", property]` — check whether a property exists.
349    Has(String),
350    /// A JSON literal value.
351    Literal(serde_json::Value),
352    /// An array of sub-expressions.
353    Array(Vec<Expression>),
354    /// `["case", cond, val, cond, val, …, fallback]`.
355    Case {
356        /// `(condition, result)` pairs.
357        conditions: Vec<(Expression, Expression)>,
358        /// Default result.
359        fallback: Box<Expression>,
360    },
361    /// `["match", input, label, val, …, fallback]`.
362    Match {
363        /// Input expression.
364        input: Box<Expression>,
365        /// `(label, result)` pairs.
366        cases: Vec<(serde_json::Value, Expression)>,
367        /// Default result.
368        fallback: Box<Expression>,
369    },
370    /// `["interpolate", interpolation, input, stop, val, …]`.
371    Interpolate {
372        /// Interpolation method.
373        interpolation: Interpolation,
374        /// Input expression (e.g. `Zoom`).
375        input: Box<Expression>,
376        /// `(stop, value)` pairs.
377        stops: Vec<(f64, Expression)>,
378    },
379    /// `["step", input, default, stop, val, …]`.
380    Step {
381        /// Input expression.
382        input: Box<Expression>,
383        /// Value below the first stop.
384        default: Box<Expression>,
385        /// `(stop, value)` pairs.
386        stops: Vec<(f64, Expression)>,
387    },
388    /// `["zoom"]` — current zoom level.
389    Zoom,
390    /// Addition.
391    Add(Box<Expression>, Box<Expression>),
392    /// Subtraction.
393    Subtract(Box<Expression>, Box<Expression>),
394    /// Multiplication.
395    Multiply(Box<Expression>, Box<Expression>),
396    /// Division.
397    Divide(Box<Expression>, Box<Expression>),
398    /// `["coalesce", …]` — first non-null result.
399    Coalesce(Vec<Expression>),
400}
401
402/// Either a literal value or a data-driven [`Expression`].
403#[derive(Debug, Clone, Serialize, Deserialize)]
404#[serde(untagged)]
405pub enum PropertyValue<T: Clone> {
406    /// A constant value.
407    Literal(T),
408    /// A data-driven expression.
409    Expression(Expression),
410}
411
412// ─────────────────────────────────────────────────────────────────────────────
413// Color
414// ─────────────────────────────────────────────────────────────────────────────
415
416/// An RGBA color.
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418pub struct Color {
419    /// Red channel (0–255).
420    pub r: u8,
421    /// Green channel (0–255).
422    pub g: u8,
423    /// Blue channel (0–255).
424    pub b: u8,
425    /// Alpha channel (0.0–1.0).
426    pub a: f32,
427}
428
429impl Color {
430    /// Parse a CSS color string: `#rrggbb`, `#rgb`, `rgb(r,g,b)`, `rgba(r,g,b,a)`.
431    pub fn parse(s: &str) -> Result<Self, StyleError> {
432        let s = s.trim();
433        if let Some(hex) = s.strip_prefix('#') {
434            Self::parse_hex(hex)
435        } else if let Some(inner) = s.strip_prefix("rgba(").and_then(|t| t.strip_suffix(')')) {
436            Self::parse_rgba(inner)
437        } else if let Some(inner) = s.strip_prefix("rgb(").and_then(|t| t.strip_suffix(')')) {
438            Self::parse_rgb(inner)
439        } else {
440            Err(StyleError::ColorParseError(format!(
441                "unsupported color format: {s}"
442            )))
443        }
444    }
445
446    fn parse_hex(hex: &str) -> Result<Self, StyleError> {
447        let err = || StyleError::ColorParseError(format!("invalid hex color: #{hex}"));
448        match hex.len() {
449            6 => {
450                let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| err())?;
451                let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| err())?;
452                let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| err())?;
453                Ok(Color { r, g, b, a: 1.0 })
454            }
455            3 => {
456                let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).map_err(|_| err())?;
457                let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).map_err(|_| err())?;
458                let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).map_err(|_| err())?;
459                Ok(Color { r, g, b, a: 1.0 })
460            }
461            _ => Err(err()),
462        }
463    }
464
465    fn parse_rgb(inner: &str) -> Result<Self, StyleError> {
466        let parts: Vec<&str> = inner.split(',').collect();
467        if parts.len() != 3 {
468            return Err(StyleError::ColorParseError(format!(
469                "rgb() expects 3 components, got {}",
470                parts.len()
471            )));
472        }
473        let parse_u8 = |s: &str| -> Result<u8, StyleError> {
474            s.trim()
475                .parse::<u8>()
476                .map_err(|_| StyleError::ColorParseError(format!("invalid channel value: {s}")))
477        };
478        Ok(Color {
479            r: parse_u8(parts[0])?,
480            g: parse_u8(parts[1])?,
481            b: parse_u8(parts[2])?,
482            a: 1.0,
483        })
484    }
485
486    fn parse_rgba(inner: &str) -> Result<Self, StyleError> {
487        let parts: Vec<&str> = inner.split(',').collect();
488        if parts.len() != 4 {
489            return Err(StyleError::ColorParseError(format!(
490                "rgba() expects 4 components, got {}",
491                parts.len()
492            )));
493        }
494        let parse_u8 = |s: &str| -> Result<u8, StyleError> {
495            s.trim()
496                .parse::<u8>()
497                .map_err(|_| StyleError::ColorParseError(format!("invalid channel value: {s}")))
498        };
499        let a: f32 = parts[3]
500            .trim()
501            .parse()
502            .map_err(|_| StyleError::ColorParseError(format!("invalid alpha: {}", parts[3])))?;
503        Ok(Color {
504            r: parse_u8(parts[0])?,
505            g: parse_u8(parts[1])?,
506            b: parse_u8(parts[2])?,
507            a,
508        })
509    }
510
511    /// Serialize to a CSS `rgba(r,g,b,a)` string.
512    pub fn to_css(&self) -> String {
513        format!("rgba({},{},{},{:.6})", self.r, self.g, self.b, self.a)
514    }
515}
516
517// ─────────────────────────────────────────────────────────────────────────────
518// Paint
519// ─────────────────────────────────────────────────────────────────────────────
520
521/// Paint properties for a layer (flexible map of property name → JSON value).
522#[derive(Debug, Clone, Default, Serialize, Deserialize)]
523pub struct Paint(pub HashMap<String, serde_json::Value>);
524
525impl Paint {
526    /// Helper: attempt to deserialise a key as `PropertyValue<T>`.
527    fn get_pv<T>(&self, key: &str) -> Option<PropertyValue<T>>
528    where
529        T: Clone + for<'de> Deserialize<'de>,
530    {
531        let v = self.0.get(key)?;
532        serde_json::from_value::<PropertyValue<T>>(v.clone()).ok()
533    }
534
535    /// `fill-color` paint property.
536    pub fn fill_color(&self) -> Option<PropertyValue<Color>> {
537        self.get_pv("fill-color")
538    }
539
540    /// `fill-opacity` paint property.
541    pub fn fill_opacity(&self) -> Option<PropertyValue<f64>> {
542        self.get_pv("fill-opacity")
543    }
544
545    /// `line-color` paint property.
546    pub fn line_color(&self) -> Option<PropertyValue<Color>> {
547        self.get_pv("line-color")
548    }
549
550    /// `line-width` paint property.
551    pub fn line_width(&self) -> Option<PropertyValue<f64>> {
552        self.get_pv("line-width")
553    }
554
555    /// `line-opacity` paint property.
556    pub fn line_opacity(&self) -> Option<PropertyValue<f64>> {
557        self.get_pv("line-opacity")
558    }
559
560    /// `circle-color` paint property.
561    pub fn circle_color(&self) -> Option<PropertyValue<Color>> {
562        self.get_pv("circle-color")
563    }
564
565    /// `circle-radius` paint property.
566    pub fn circle_radius(&self) -> Option<PropertyValue<f64>> {
567        self.get_pv("circle-radius")
568    }
569
570    /// `raster-opacity` paint property.
571    pub fn raster_opacity(&self) -> Option<PropertyValue<f64>> {
572        self.get_pv("raster-opacity")
573    }
574
575    /// `raster-hue-rotate` paint property.
576    pub fn raster_hue_rotate(&self) -> Option<PropertyValue<f64>> {
577        self.get_pv("raster-hue-rotate")
578    }
579
580    /// `background-color` paint property.
581    pub fn background_color(&self) -> Option<PropertyValue<Color>> {
582        self.get_pv("background-color")
583    }
584}
585
586// ─────────────────────────────────────────────────────────────────────────────
587// Layout
588// ─────────────────────────────────────────────────────────────────────────────
589
590/// Layout properties for a layer.
591#[derive(Debug, Clone, Default, Serialize, Deserialize)]
592pub struct Layout(pub HashMap<String, serde_json::Value>);
593
594impl Layout {
595    fn get_str(&self, key: &str) -> Option<&str> {
596        self.0.get(key)?.as_str()
597    }
598
599    fn get_pv<T>(&self, key: &str) -> Option<PropertyValue<T>>
600    where
601        T: Clone + for<'de> Deserialize<'de>,
602    {
603        let v = self.0.get(key)?;
604        serde_json::from_value::<PropertyValue<T>>(v.clone()).ok()
605    }
606
607    /// Layer visibility.
608    pub fn visibility(&self) -> Visibility {
609        match self.get_str("visibility") {
610            Some("none") => Visibility::None,
611            _ => Visibility::Visible,
612        }
613    }
614
615    /// Line cap style.
616    pub fn line_cap(&self) -> LineCap {
617        match self.get_str("line-cap") {
618            Some("round") => LineCap::Round,
619            Some("square") => LineCap::Square,
620            _ => LineCap::Butt,
621        }
622    }
623
624    /// Line join style.
625    pub fn line_join(&self) -> LineJoin {
626        match self.get_str("line-join") {
627            Some("round") => LineJoin::Round,
628            Some("miter") => LineJoin::Miter,
629            _ => LineJoin::Bevel,
630        }
631    }
632
633    /// Symbol placement.
634    pub fn symbol_placement(&self) -> SymbolPlacement {
635        match self.get_str("symbol-placement") {
636            Some("line") => SymbolPlacement::Line,
637            Some("line-center") => SymbolPlacement::LineCenter,
638            _ => SymbolPlacement::Point,
639        }
640    }
641
642    /// `text-field` layout property.
643    pub fn text_field(&self) -> Option<PropertyValue<String>> {
644        self.get_pv("text-field")
645    }
646
647    /// `text-size` layout property.
648    pub fn text_size(&self) -> Option<PropertyValue<f64>> {
649        self.get_pv("text-size")
650    }
651
652    /// `icon-image` layout property.
653    pub fn icon_image(&self) -> Option<PropertyValue<String>> {
654        self.get_pv("icon-image")
655    }
656}
657
658// ─────────────────────────────────────────────────────────────────────────────
659// Layout enums
660// ─────────────────────────────────────────────────────────────────────────────
661
662/// Layer visibility.
663#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
664#[serde(rename_all = "lowercase")]
665pub enum Visibility {
666    /// Layer is rendered.
667    #[default]
668    Visible,
669    /// Layer is hidden.
670    None,
671}
672
673/// Line cap style.
674#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
675#[serde(rename_all = "lowercase")]
676pub enum LineCap {
677    /// Flat square cap at endpoint.
678    #[default]
679    Butt,
680    /// Rounded cap.
681    Round,
682    /// Square cap extending past endpoint.
683    Square,
684}
685
686/// Line join style.
687#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
688#[serde(rename_all = "lowercase")]
689pub enum LineJoin {
690    /// Flat bevel join.
691    #[default]
692    Bevel,
693    /// Rounded join.
694    Round,
695    /// Sharp miter join.
696    Miter,
697}
698
699/// Symbol placement along a line or at a point.
700#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
701#[serde(rename_all = "kebab-case")]
702pub enum SymbolPlacement {
703    /// Placed at the feature's point / centroid.
704    #[default]
705    Point,
706    /// Placed along the line.
707    Line,
708    /// Placed at the centre of the line.
709    LineCenter,
710}
711
712// ─────────────────────────────────────────────────────────────────────────────
713// Transition
714// ─────────────────────────────────────────────────────────────────────────────
715
716/// Default transition options for paint property changes.
717#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct Transition {
719    /// Transition duration in milliseconds.
720    #[serde(default)]
721    pub duration: u32,
722    /// Transition delay in milliseconds.
723    #[serde(default)]
724    pub delay: u32,
725}
726
727// ─────────────────────────────────────────────────────────────────────────────
728// Light
729// ─────────────────────────────────────────────────────────────────────────────
730
731/// Global light source configuration.
732#[derive(Debug, Clone, Serialize, Deserialize)]
733pub struct Light {
734    /// Reference frame for the light position.
735    #[serde(default)]
736    pub anchor: LightAnchor,
737    /// Light color.
738    pub color: Color,
739    /// Light intensity (0–1).
740    #[serde(default = "default_intensity")]
741    pub intensity: f64,
742    /// `[radial, azimuthal, polar]` position.
743    #[serde(default = "default_light_position")]
744    pub position: [f64; 3],
745}
746
747fn default_intensity() -> f64 {
748    0.5
749}
750
751fn default_light_position() -> [f64; 3] {
752    [1.15, 210.0, 30.0]
753}
754
755/// Anchor reference frame for the light source.
756#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
757#[serde(rename_all = "lowercase")]
758pub enum LightAnchor {
759    /// Light position is in viewport space.
760    #[default]
761    Viewport,
762    /// Light position is in map space.
763    Map,
764}
765
766// ─────────────────────────────────────────────────────────────────────────────
767// Validation
768// ─────────────────────────────────────────────────────────────────────────────
769
770/// A single validation diagnostic.
771#[derive(Debug, Clone)]
772pub struct ValidationError {
773    /// Layer id, if the error is layer-specific.
774    pub layer_id: Option<String>,
775    /// Human-readable description of the problem.
776    pub message: String,
777}
778
779/// Validates a [`StyleSpec`] for structural correctness.
780pub struct StyleValidator;
781
782impl StyleValidator {
783    /// Validate a style specification and return any errors found.
784    pub fn validate(spec: &StyleSpec) -> Vec<ValidationError> {
785        let mut errors: Vec<ValidationError> = Vec::new();
786
787        // version must be 8
788        if spec.version != 8 {
789            errors.push(ValidationError {
790                layer_id: None,
791                message: format!("style version must be 8, got {}", spec.version),
792            });
793        }
794
795        // no duplicate layer ids
796        let mut seen_ids: HashMap<&str, usize> = HashMap::new();
797        for (idx, layer) in spec.layers.iter().enumerate() {
798            if let Some(prev) = seen_ids.insert(layer.id.as_str(), idx) {
799                errors.push(ValidationError {
800                    layer_id: Some(layer.id.clone()),
801                    message: format!(
802                        "duplicate layer id '{}' (first at index {prev}, repeated at index {idx})",
803                        layer.id
804                    ),
805                });
806            }
807        }
808
809        for layer in &spec.layers {
810            // check source references
811            if let Some(src) = &layer.source {
812                if !spec.sources.contains_key(src.as_str()) {
813                    errors.push(ValidationError {
814                        layer_id: Some(layer.id.clone()),
815                        message: format!("layer references unknown source '{src}'"),
816                    });
817                }
818            }
819
820            // zoom range sanity
821            if let (Some(min), Some(max)) = (layer.min_zoom, layer.max_zoom) {
822                if min > max {
823                    errors.push(ValidationError {
824                        layer_id: Some(layer.id.clone()),
825                        message: format!("minzoom ({min}) must be <= maxzoom ({max})"),
826                    });
827                }
828            }
829
830            // background layers must have no source
831            if layer.layer_type == LayerType::Background && layer.source.is_some() {
832                errors.push(ValidationError {
833                    layer_id: Some(layer.id.clone()),
834                    message: "background layer must not reference a source".to_string(),
835                });
836            }
837
838            // fill/line/circle/symbol layers must have a source
839            let requires_source = matches!(
840                layer.layer_type,
841                LayerType::Fill | LayerType::Line | LayerType::Circle | LayerType::Symbol
842            );
843            if requires_source && layer.source.is_none() {
844                errors.push(ValidationError {
845                    layer_id: Some(layer.id.clone()),
846                    message: format!("{:?} layer requires a source", layer.layer_type),
847                });
848            }
849        }
850
851        errors
852    }
853}
854
855// ─────────────────────────────────────────────────────────────────────────────
856// Rendering / evaluation
857// ─────────────────────────────────────────────────────────────────────────────
858
859/// Evaluates paint properties and filters at runtime.
860pub struct StyleRenderer;
861
862impl StyleRenderer {
863    /// Evaluate a `PropertyValue<Color>` at a given zoom level.
864    ///
865    /// Returns the literal color or the result of evaluating an interpolation
866    /// expression; falls back to opaque black for complex expressions not yet
867    /// handled by this evaluator.
868    pub fn eval_zoom_color(value: &PropertyValue<Color>, zoom: f64) -> Color {
869        match value {
870            PropertyValue::Literal(c) => c.clone(),
871            PropertyValue::Expression(expr) => Self::eval_expr_color(expr, zoom).unwrap_or(Color {
872                r: 0,
873                g: 0,
874                b: 0,
875                a: 1.0,
876            }),
877        }
878    }
879
880    fn eval_expr_color(expr: &Expression, zoom: f64) -> Option<Color> {
881        match expr {
882            Expression::Literal(v) => {
883                let s = v.as_str()?;
884                Color::parse(s).ok()
885            }
886            Expression::Interpolate {
887                interpolation,
888                input,
889                stops,
890            } => {
891                let input_val = Self::eval_expr_f64(input, zoom)?;
892                if stops.is_empty() {
893                    return None;
894                }
895                // find surrounding stops
896                let (lo_stop, lo_expr, hi_stop, hi_expr) =
897                    Self::find_stops_color(stops, input_val)?;
898                let lo_c = Self::eval_expr_color(lo_expr, zoom)?;
899                let hi_c = Self::eval_expr_color(hi_expr, zoom)?;
900                let t = Self::interp_t(interpolation, input_val, lo_stop, hi_stop);
901                Some(lerp_color(&lo_c, &hi_c, t))
902            }
903            _ => None,
904        }
905    }
906
907    fn find_stops_color(
908        stops: &[(f64, Expression)],
909        input: f64,
910    ) -> Option<(f64, &Expression, f64, &Expression)> {
911        if stops.len() == 1 {
912            return Some((stops[0].0, &stops[0].1, stops[0].0, &stops[0].1));
913        }
914        let last = stops.last()?;
915        if input >= last.0 {
916            let second_last = &stops[stops.len() - 2];
917            return Some((second_last.0, &second_last.1, last.0, &last.1));
918        }
919        let first = stops.first()?;
920        if input <= first.0 {
921            let second = &stops[1];
922            return Some((first.0, &first.1, second.0, &second.1));
923        }
924        for i in 0..stops.len() - 1 {
925            if input >= stops[i].0 && input < stops[i + 1].0 {
926                return Some((stops[i].0, &stops[i].1, stops[i + 1].0, &stops[i + 1].1));
927            }
928        }
929        None
930    }
931
932    /// Evaluate a `PropertyValue<f64>` at a given zoom level.
933    ///
934    /// Supports `Literal`, `Expression::Zoom`, and `Expression::Interpolate`
935    /// with `Linear` and `Exponential` interpolation. Returns `0.0` for
936    /// unrecognised expressions.
937    pub fn eval_zoom_f64(value: &PropertyValue<f64>, zoom: f64) -> f64 {
938        match value {
939            PropertyValue::Literal(v) => *v,
940            PropertyValue::Expression(expr) => Self::eval_expr_f64(expr, zoom).unwrap_or(0.0),
941        }
942    }
943
944    fn eval_expr_f64(expr: &Expression, zoom: f64) -> Option<f64> {
945        match expr {
946            Expression::Zoom => Some(zoom),
947            Expression::Literal(v) => v.as_f64(),
948            Expression::Interpolate {
949                interpolation,
950                input,
951                stops,
952            } => {
953                let input_val = Self::eval_expr_f64(input, zoom)?;
954                if stops.is_empty() {
955                    return None;
956                }
957                if stops.len() == 1 {
958                    return Self::eval_expr_f64(&stops[0].1, zoom);
959                }
960                let last = stops.last()?;
961                if input_val >= last.0 {
962                    return Self::eval_expr_f64(&last.1, zoom);
963                }
964                let first = stops.first()?;
965                if input_val <= first.0 {
966                    return Self::eval_expr_f64(&first.1, zoom);
967                }
968                for i in 0..stops.len() - 1 {
969                    if input_val >= stops[i].0 && input_val < stops[i + 1].0 {
970                        let lo = Self::eval_expr_f64(&stops[i].1, zoom)?;
971                        let hi = Self::eval_expr_f64(&stops[i + 1].1, zoom)?;
972                        let t =
973                            Self::interp_t(interpolation, input_val, stops[i].0, stops[i + 1].0);
974                        return Some(lo + t * (hi - lo));
975                    }
976                }
977                None
978            }
979            Expression::Step {
980                input,
981                default,
982                stops,
983            } => {
984                let input_val = Self::eval_expr_f64(input, zoom)?;
985                let mut result = Self::eval_expr_f64(default, zoom)?;
986                for (stop, val) in stops {
987                    if input_val >= *stop {
988                        result = Self::eval_expr_f64(val, zoom)?;
989                    }
990                }
991                Some(result)
992            }
993            Expression::Add(a, b) => {
994                Some(Self::eval_expr_f64(a, zoom)? + Self::eval_expr_f64(b, zoom)?)
995            }
996            Expression::Subtract(a, b) => {
997                Some(Self::eval_expr_f64(a, zoom)? - Self::eval_expr_f64(b, zoom)?)
998            }
999            Expression::Multiply(a, b) => {
1000                Some(Self::eval_expr_f64(a, zoom)? * Self::eval_expr_f64(b, zoom)?)
1001            }
1002            Expression::Divide(a, b) => {
1003                let divisor = Self::eval_expr_f64(b, zoom)?;
1004                if divisor == 0.0 {
1005                    None
1006                } else {
1007                    Some(Self::eval_expr_f64(a, zoom)? / divisor)
1008                }
1009            }
1010            _ => None,
1011        }
1012    }
1013
1014    /// Compute the interpolation parameter `t ∈ [0,1]` given an input value and stop range.
1015    fn interp_t(interp: &Interpolation, input: f64, lo: f64, hi: f64) -> f64 {
1016        let range = hi - lo;
1017        if range == 0.0 {
1018            return 0.0;
1019        }
1020        match interp {
1021            Interpolation::Linear => (input - lo) / range,
1022            Interpolation::Exponential(base) => {
1023                if (base - 1.0).abs() < f64::EPSILON {
1024                    (input - lo) / range
1025                } else {
1026                    (base.powf(input - lo) - 1.0) / (base.powf(range) - 1.0)
1027                }
1028            }
1029            Interpolation::CubicBezier(_) => {
1030                // Approximate with linear for now
1031                (input - lo) / range
1032            }
1033        }
1034    }
1035
1036    /// Evaluate whether a set of feature properties matches the given [`Filter`].
1037    pub fn feature_matches_filter(
1038        filter: &Filter,
1039        properties: &HashMap<String, serde_json::Value>,
1040    ) -> bool {
1041        match filter {
1042            Filter::All(filters) => filters
1043                .iter()
1044                .all(|f| Self::feature_matches_filter(f, properties)),
1045            Filter::Any(filters) => filters
1046                .iter()
1047                .any(|f| Self::feature_matches_filter(f, properties)),
1048            Filter::None(filters) => !filters
1049                .iter()
1050                .any(|f| Self::feature_matches_filter(f, properties)),
1051            Filter::Eq { property, value } => properties.get(property.as_str()) == Some(value),
1052            Filter::Ne { property, value } => properties.get(property.as_str()) != Some(value),
1053            Filter::Lt { property, value } => properties
1054                .get(property.as_str())
1055                .and_then(|v| v.as_f64())
1056                .is_some_and(|v| v < *value),
1057            Filter::Lte { property, value } => properties
1058                .get(property.as_str())
1059                .and_then(|v| v.as_f64())
1060                .is_some_and(|v| v <= *value),
1061            Filter::Gt { property, value } => properties
1062                .get(property.as_str())
1063                .and_then(|v| v.as_f64())
1064                .is_some_and(|v| v > *value),
1065            Filter::Gte { property, value } => properties
1066                .get(property.as_str())
1067                .and_then(|v| v.as_f64())
1068                .is_some_and(|v| v >= *value),
1069            Filter::In { property, values } => properties
1070                .get(property.as_str())
1071                .is_some_and(|v| values.contains(v)),
1072            Filter::Has(property) => properties.contains_key(property.as_str()),
1073            Filter::NotHas(property) => !properties.contains_key(property.as_str()),
1074            Filter::GeometryType(_) => {
1075                // Geometry type matching requires the geometry itself, which is
1076                // not available from property maps alone; default to true to
1077                // remain non-blocking in property-only evaluation contexts.
1078                true
1079            }
1080        }
1081    }
1082}
1083
1084// ─────────────────────────────────────────────────────────────────────────────
1085// Internal helpers
1086// ─────────────────────────────────────────────────────────────────────────────
1087
1088fn lerp_color(a: &Color, b: &Color, t: f64) -> Color {
1089    let lerp_u8 = |lo: u8, hi: u8| -> u8 {
1090        let v = f64::from(lo) + t * (f64::from(hi) - f64::from(lo));
1091        v.round() as u8
1092    };
1093    Color {
1094        r: lerp_u8(a.r, b.r),
1095        g: lerp_u8(a.g, b.g),
1096        b: lerp_u8(a.b, b.b),
1097        a: a.a + t as f32 * (b.a - a.a),
1098    }
1099}