Skip to main content

rustial_engine/
style_json.rs

1//! JSON style-spec parsing and resolution for the style runtime.
2//!
3//! This module adds a declarative style-document layer closer to MapLibre's
4//! `style/style.ts`: JSON source and layer definitions are parsed into a
5//! lightweight spec and then resolved through a host-provided runtime source
6//! registry into an executable [`StyleDocument`](crate::style::StyleDocument).
7
8use crate::style::{
9    BackgroundStyleLayer, CircleStyleLayer, FillExtrusionStyleLayer, FillStyleLayer,
10    HeatmapStyleLayer, HillshadeStyleLayer, LightConfig, LineStyleLayer, ModelStyleLayer,
11    RasterStyleLayer, SkyConfig, StyleDocument, StyleError, StyleLayer, StyleLayerMeta,
12    StyleProjection, StyleSource, StyleSourceId, StyleSourceKind, StyleValue, SymbolStyleLayer,
13    VectorStyleLayer,
14};
15use crate::symbols::{
16    SymbolAnchor, SymbolIconTextFit, SymbolPlacement, SymbolTextJustify, SymbolTextTransform,
17    SymbolWritingMode,
18};
19use serde::{Deserialize, Serialize};
20use serde_json::{Map, Value};
21use std::collections::HashMap;
22use std::fmt;
23
24/// Errors produced while parsing or resolving a JSON style specification.
25#[derive(Debug)]
26pub enum StyleSpecError {
27    /// Failed to parse JSON.
28    Json(serde_json::Error),
29    /// A style source required by the JSON spec has no runtime binding.
30    MissingRuntimeSource(String),
31    /// The runtime binding for a source does not match the declared source kind.
32    RuntimeSourceKindMismatch {
33        /// Source id being resolved.
34        source_id: String,
35        /// Declared spec kind.
36        declared: StyleSourceKind,
37        /// Actual bound runtime kind.
38        actual: StyleSourceKind,
39    },
40    /// A layer references a source but omits the `source` field.
41    MissingLayerSource(String),
42    /// A JSON field had the wrong shape.
43    InvalidField {
44        /// Layer or source id for diagnostics.
45        id: String,
46        /// Field name.
47        field: &'static str,
48        /// Human-readable expectation.
49        expected: &'static str,
50    },
51    /// The runtime style document rejected the resolved document.
52    Runtime(StyleError),
53}
54
55impl fmt::Display for StyleSpecError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            StyleSpecError::Json(err) => write!(f, "style JSON parse error: {err}"),
59            StyleSpecError::MissingRuntimeSource(id) => {
60                write!(f, "missing runtime source binding for style source `{id}`")
61            }
62            StyleSpecError::RuntimeSourceKindMismatch {
63                source_id,
64                declared,
65                actual,
66            } => write!(
67                f,
68                "runtime source `{source_id}` kind mismatch: declared `{}`, bound `{}`",
69                declared.as_str(),
70                actual.as_str()
71            ),
72            StyleSpecError::MissingLayerSource(id) => {
73                write!(f, "style layer `{id}` requires a `source` field")
74            }
75            StyleSpecError::InvalidField {
76                id,
77                field,
78                expected,
79            } => {
80                write!(f, "invalid field `{field}` in `{id}`, expected {expected}")
81            }
82            StyleSpecError::Runtime(err) => err.fmt(f),
83        }
84    }
85}
86
87impl std::error::Error for StyleSpecError {}
88
89impl From<serde_json::Error> for StyleSpecError {
90    fn from(value: serde_json::Error) -> Self {
91        Self::Json(value)
92    }
93}
94
95impl From<StyleError> for StyleSpecError {
96    fn from(value: StyleError) -> Self {
97        Self::Runtime(value)
98    }
99}
100
101/// Declarative JSON style document.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct StyleSpecDocument {
104    /// Style-spec version.
105    #[serde(default = "default_style_version")]
106    pub version: u8,
107    /// Optional human-readable style name.
108    #[serde(default)]
109    pub name: Option<String>,
110    /// Declared source definitions.
111    #[serde(default)]
112    pub sources: HashMap<StyleSourceId, StyleSpecSource>,
113    /// Ordered layer definitions.
114    #[serde(default)]
115    pub layers: Vec<StyleSpecLayer>,
116    /// Optional terrain configuration.
117    #[serde(default)]
118    pub terrain: Option<StyleSpecTerrain>,
119    /// Optional top-level projection configuration.
120    #[serde(default)]
121    pub projection: Option<StyleSpecProjection>,
122    /// Optional top-level lights configuration (Mapbox v3 style spec).
123    #[serde(default)]
124    pub lights: Option<Vec<StyleSpecLight>>,
125    /// Optional top-level sky configuration (Mapbox style spec).
126    #[serde(default)]
127    pub sky: Option<StyleSpecSky>,
128    /// Optional global transition timing (Mapbox style spec).
129    ///
130    /// Applies to all paint properties that do not have a per-layer or
131    /// per-property transition override.
132    #[serde(default)]
133    pub transition: Option<StyleSpecTransition>,
134}
135
136/// Transition timing as declared in a JSON style spec.
137///
138/// Matches Mapbox GL JS `TransitionSpecification`:
139/// ```json
140/// { "duration": 300, "delay": 0 }
141/// ```
142///
143/// Both values are in **milliseconds** (they are converted to seconds at
144/// resolution time).
145#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
146pub struct StyleSpecTransition {
147    /// Duration in milliseconds. Default: `300`.
148    #[serde(default = "default_transition_duration")]
149    pub duration: f32,
150    /// Delay in milliseconds. Default: `0`.
151    #[serde(default)]
152    pub delay: f32,
153}
154
155fn default_transition_duration() -> f32 {
156    300.0
157}
158
159impl StyleSpecTransition {
160    /// Convert to the runtime [`TransitionSpec`] (seconds).
161    pub fn to_runtime(&self) -> crate::style::TransitionSpec {
162        crate::style::TransitionSpec {
163            duration: self.duration / 1000.0,
164            delay: self.delay / 1000.0,
165        }
166    }
167}
168
169impl StyleSpecDocument {
170    /// Parse a JSON style string.
171    pub fn from_json(json: &str) -> Result<Self, StyleSpecError> {
172        Ok(serde_json::from_str(json)?)
173    }
174
175    /// Resolve the declarative style into a runtime [`StyleDocument`].
176    pub fn resolve(&self, registry: &StyleSourceRegistry) -> Result<StyleDocument, StyleSpecError> {
177        let mut document = StyleDocument::new();
178
179        for (id, source_spec) in &self.sources {
180            let runtime = registry
181                .source(id)
182                .ok_or_else(|| StyleSpecError::MissingRuntimeSource(id.to_string()))?;
183            if runtime.kind() != source_spec.source_type.runtime_kind() {
184                return Err(StyleSpecError::RuntimeSourceKindMismatch {
185                    source_id: id.to_string(),
186                    declared: source_spec.source_type.runtime_kind(),
187                    actual: runtime.kind(),
188                });
189            }
190            document.add_source(id.clone(), runtime.clone())?;
191        }
192
193        if let Some(terrain) = &self.terrain {
194            document.set_terrain_source(Some(terrain.source.clone()));
195        }
196
197        if let Some(projection) = &self.projection {
198            document.set_projection(projection.projection_type.runtime_projection());
199        }
200
201        if let Some(lights) = &self.lights {
202            document.set_lights(Some(resolve_lights(lights)));
203        }
204
205        if let Some(sky) = &self.sky {
206            document.set_sky(Some(resolve_sky(sky)));
207        }
208
209        if let Some(transition) = &self.transition {
210            document.set_transition(transition.to_runtime());
211        }
212
213        for layer in &self.layers {
214            document.add_layer(layer.to_runtime_layer()?)?;
215        }
216
217        Ok(document)
218    }
219}
220
221/// Runtime source registry used while resolving JSON specs.
222#[derive(Debug, Clone, Default)]
223pub struct StyleSourceRegistry {
224    sources: HashMap<StyleSourceId, StyleSource>,
225}
226
227impl StyleSourceRegistry {
228    /// Create an empty source registry.
229    pub fn new() -> Self {
230        Self::default()
231    }
232
233    /// Register or replace a runtime source binding.
234    pub fn set_source(&mut self, id: impl Into<String>, source: StyleSource) {
235        self.sources.insert(id.into(), source);
236    }
237
238    /// Look up a bound source.
239    pub fn source(&self, id: &str) -> Option<&StyleSource> {
240        self.sources.get(id)
241    }
242
243    /// Resolve a parsed spec document into a runtime style document.
244    pub fn resolve_document(
245        &self,
246        spec: &StyleSpecDocument,
247    ) -> Result<StyleDocument, StyleSpecError> {
248        spec.resolve(self)
249    }
250}
251
252/// Top-level terrain entry in a JSON style spec.
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct StyleSpecTerrain {
255    /// Source id referenced by the terrain configuration.
256    pub source: StyleSourceId,
257}
258
259/// Top-level projection entry in a JSON style spec.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct StyleSpecProjection {
262    /// Projection type.
263    #[serde(rename = "type")]
264    pub projection_type: StyleSpecProjectionType,
265}
266
267/// Projection kinds accepted by the current JSON style spec.
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
269pub enum StyleSpecProjectionType {
270    /// Web Mercator projection.
271    #[serde(rename = "mercator")]
272    Mercator,
273    /// Equirectangular planar projection.
274    #[serde(rename = "equirectangular")]
275    Equirectangular,
276    /// Globe / geocentric projection.
277    #[serde(rename = "globe")]
278    Globe,
279    /// Near-sided vertical perspective projection.
280    #[serde(rename = "vertical-perspective")]
281    VerticalPerspective,
282}
283
284impl StyleSpecProjectionType {
285    fn runtime_projection(self) -> StyleProjection {
286        match self {
287            StyleSpecProjectionType::Mercator => StyleProjection::Mercator,
288            StyleSpecProjectionType::Equirectangular => StyleProjection::Equirectangular,
289            StyleSpecProjectionType::Globe => StyleProjection::Globe,
290            StyleSpecProjectionType::VerticalPerspective => StyleProjection::VerticalPerspective,
291        }
292    }
293}
294
295// ---------------------------------------------------------------------------
296// Lights spec
297// ---------------------------------------------------------------------------
298
299/// A single light entry in the style-spec `"lights"` array.
300///
301/// Follows the Mapbox GL JS v3 convention:
302/// ```json
303/// "lights": [
304///   { "type": "ambient", "properties": { "color": "white", "intensity": 0.5 } },
305///   { "type": "directional", "properties": { "direction": [210, 45], "color": "white", "intensity": 0.5 } }
306/// ]
307/// ```
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct StyleSpecLight {
310    /// Light type: `"ambient"`, `"directional"`, or `"flat"`.
311    #[serde(rename = "type")]
312    pub light_type: StyleSpecLightType,
313    /// Light properties (optional; missing fields use defaults).
314    #[serde(default)]
315    pub properties: StyleSpecLightProperties,
316}
317
318/// Light type discriminant.
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
320pub enum StyleSpecLightType {
321    /// Ambient hemisphere light.
322    #[serde(rename = "ambient")]
323    Ambient,
324    /// Directional / sun light.
325    #[serde(rename = "directional")]
326    Directional,
327    /// Flat / unlit mode.
328    #[serde(rename = "flat")]
329    Flat,
330}
331
332/// Unioned property bag for all light types.
333///
334/// Fields that do not apply to a given light type are silently ignored.
335#[derive(Debug, Clone, Default, Serialize, Deserialize)]
336pub struct StyleSpecLightProperties {
337    /// Light colour as a CSS colour string (e.g. `"white"`, `"#ff8800"`).
338    ///
339    /// Currently only `"white"` and `"#rrggbb"` hex are parsed.
340    /// Unrecognised values fall back to white.
341    #[serde(default)]
342    pub color: Option<String>,
343    /// Intensity multiplier.
344    #[serde(default)]
345    pub intensity: Option<f32>,
346    /// `[azimuth_deg, altitude_deg]` for directional lights.
347    #[serde(default)]
348    pub direction: Option<[f32; 2]>,
349    /// Whether the directional light casts shadows.
350    #[serde(default, rename = "cast-shadows")]
351    pub cast_shadows: Option<bool>,
352}
353
354/// Resolve a `"lights"` array from the JSON style spec into a
355/// [`LightConfig`].
356fn resolve_lights(lights: &[StyleSpecLight]) -> LightConfig {
357    use crate::style::{AmbientLight, DirectionalLight, LightingMode};
358
359    let mut config = LightConfig::default();
360
361    for light in lights {
362        let color = light
363            .properties
364            .color
365            .as_deref()
366            .map(parse_light_color)
367            .unwrap_or([1.0, 1.0, 1.0]);
368
369        match light.light_type {
370            StyleSpecLightType::Ambient => {
371                config.ambient = AmbientLight {
372                    color,
373                    intensity: light.properties.intensity.unwrap_or(0.5),
374                };
375            }
376            StyleSpecLightType::Directional => {
377                config.directional = DirectionalLight {
378                    direction: light.properties.direction.unwrap_or([210.0, 45.0]),
379                    color,
380                    intensity: light.properties.intensity.unwrap_or(0.5),
381                    cast_shadows: light.properties.cast_shadows.unwrap_or(false),
382                };
383            }
384            StyleSpecLightType::Flat => {
385                config.mode = LightingMode::Flat;
386            }
387        }
388    }
389
390    config
391}
392
393/// Parse a CSS-style colour string into linear RGB [0, 1].
394///
395/// Supports `"white"`, `"black"`, and `"#rrggbb"` hex notation.
396/// Unknown values fall back to white.
397fn parse_light_color(s: &str) -> [f32; 3] {
398    match s.trim().to_lowercase().as_str() {
399        "white" => [1.0, 1.0, 1.0],
400        "black" => [0.0, 0.0, 0.0],
401        hex if hex.starts_with('#') && hex.len() == 7 => {
402            let r = u8::from_str_radix(&hex[1..3], 16).unwrap_or(255);
403            let g = u8::from_str_radix(&hex[3..5], 16).unwrap_or(255);
404            let b = u8::from_str_radix(&hex[5..7], 16).unwrap_or(255);
405            // sRGB → linear (approximate).
406            [
407                (r as f32 / 255.0).powf(2.2),
408                (g as f32 / 255.0).powf(2.2),
409                (b as f32 / 255.0).powf(2.2),
410            ]
411        }
412        _ => [1.0, 1.0, 1.0],
413    }
414}
415
416// ---------------------------------------------------------------------------
417// Sky spec
418// ---------------------------------------------------------------------------
419
420/// Top-level `"sky"` configuration in the style-spec.
421///
422/// Follows the Mapbox GL JS v3 convention:
423/// ```json
424/// "sky": {
425///   "sky-type": "atmosphere",
426///   "sky-atmosphere-sun": [210, 45],
427///   "sky-atmosphere-sun-intensity": 10,
428///   "sky-atmosphere-color": "white",
429///   "sky-atmosphere-halo-color": "white",
430///   "sky-opacity": 1.0
431/// }
432/// ```
433#[derive(Debug, Clone, Default, Serialize, Deserialize)]
434pub struct StyleSpecSky {
435    /// `"atmosphere"` or `"gradient"`.  Default: `"atmosphere"`.
436    #[serde(default, rename = "sky-type")]
437    pub sky_type: Option<String>,
438    /// Sun position as `[azimuth_deg, altitude_deg]`.
439    #[serde(default, rename = "sky-atmosphere-sun")]
440    pub sun_position: Option<[f32; 2]>,
441    /// Sun brightness multiplier (0–100).
442    #[serde(default, rename = "sky-atmosphere-sun-intensity")]
443    pub sun_intensity: Option<f32>,
444    /// Tint for Rayleigh scattering (CSS colour string).
445    #[serde(default, rename = "sky-atmosphere-color")]
446    pub atmosphere_color: Option<String>,
447    /// Tint for Mie scattering / halo (CSS colour string).
448    #[serde(default, rename = "sky-atmosphere-halo-color")]
449    pub halo_color: Option<String>,
450    /// Overall sky opacity (0–1).
451    #[serde(default, rename = "sky-opacity")]
452    pub opacity: Option<f32>,
453}
454
455/// Resolve a `"sky"` JSON object into a [`SkyConfig`].
456fn resolve_sky(spec: &StyleSpecSky) -> SkyConfig {
457    use crate::style::{SkyConfig, SkyType};
458
459    let sky_type = match spec.sky_type.as_deref() {
460        Some("gradient") => SkyType::Gradient,
461        _ => SkyType::Atmosphere,
462    };
463
464    SkyConfig {
465        sky_type,
466        sun_position: spec.sun_position,
467        sun_intensity: spec.sun_intensity.unwrap_or(10.0),
468        atmosphere_color: spec
469            .atmosphere_color
470            .as_deref()
471            .map(parse_light_color)
472            .unwrap_or([1.0, 1.0, 1.0]),
473        halo_color: spec
474            .halo_color
475            .as_deref()
476            .map(parse_light_color)
477            .unwrap_or([1.0, 1.0, 1.0]),
478        opacity: spec.opacity.unwrap_or(1.0),
479    }
480}
481
482/// Source kinds accepted by the JSON style spec.
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
484pub enum StyleSpecSourceType {
485    /// Raster tile source.
486    #[serde(rename = "raster")]
487    Raster,
488    /// Raster DEM / terrain source.
489    #[serde(rename = "raster-dem")]
490    RasterDem,
491    /// GeoJSON source.
492    #[serde(rename = "geojson")]
493    GeoJson,
494    /// Vector-tile-like source.
495    #[serde(rename = "vector")]
496    Vector,
497    /// Image source.
498    #[serde(rename = "image")]
499    Image,
500    /// Video source.
501    #[serde(rename = "video")]
502    Video,
503    /// Canvas source.
504    #[serde(rename = "canvas")]
505    Canvas,
506    /// Model source.
507    #[serde(rename = "model")]
508    Model,
509}
510
511impl StyleSpecSourceType {
512    fn runtime_kind(self) -> StyleSourceKind {
513        match self {
514            StyleSpecSourceType::Raster => StyleSourceKind::Raster,
515            StyleSpecSourceType::RasterDem => StyleSourceKind::Terrain,
516            StyleSpecSourceType::GeoJson => StyleSourceKind::GeoJson,
517            StyleSpecSourceType::Vector => StyleSourceKind::VectorTile,
518            StyleSpecSourceType::Image => StyleSourceKind::Image,
519            StyleSpecSourceType::Video => StyleSourceKind::Video,
520            StyleSpecSourceType::Canvas => StyleSourceKind::Canvas,
521            StyleSpecSourceType::Model => StyleSourceKind::Model,
522        }
523    }
524}
525
526/// Declarative source entry.
527#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct StyleSpecSource {
529    /// Source type.
530    #[serde(rename = "type")]
531    pub source_type: StyleSpecSourceType,
532    /// Optional URL.
533    #[serde(default)]
534    pub url: Option<String>,
535    /// Optional tile URL templates.
536    #[serde(default)]
537    pub tiles: Vec<String>,
538    /// Optional arbitrary metadata.
539    #[serde(default)]
540    pub metadata: HashMap<String, Value>,
541}
542
543/// Layer kinds accepted by the JSON style spec.
544#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
545pub enum StyleSpecLayerType {
546    #[serde(rename = "background")]
547    /// Background style layer entry.
548    Background,
549    #[serde(rename = "hillshade")]
550    /// Hillshade style layer entry.
551    Hillshade,
552    #[serde(rename = "raster")]
553    /// Raster style layer entry.
554    Raster,
555    #[serde(rename = "vector")]
556    /// Generic vector style layer entry.
557    Vector,
558    #[serde(rename = "fill")]
559    /// Fill style layer entry.
560    Fill,
561    #[serde(rename = "line")]
562    /// Line style layer entry.
563    Line,
564    #[serde(rename = "circle")]
565    /// Circle style layer entry.
566    Circle,
567    #[serde(rename = "heatmap")]
568    /// Heatmap style layer entry.
569    Heatmap,
570    #[serde(rename = "fill-extrusion")]
571    /// Fill-extrusion style layer entry.
572    FillExtrusion,
573    #[serde(rename = "symbol")]
574    /// Symbol style layer entry.
575    Symbol,
576    #[serde(rename = "model")]
577    /// Model style layer entry.
578    Model,
579}
580
581/// Declarative layer entry.
582#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct StyleSpecLayer {
584    /// Style layer id.
585    pub id: String,
586    /// Layer type.
587    #[serde(rename = "type")]
588    pub layer_type: StyleSpecLayerType,
589    /// Optional source id.
590    #[serde(default)]
591    pub source: Option<StyleSourceId>,
592    /// Optional source-layer id for vector-tile-like sources.
593    #[serde(default, rename = "source-layer")]
594    pub source_layer: Option<String>,
595    /// Optional minimum zoom.
596    #[serde(default)]
597    pub minzoom: Option<f32>,
598    /// Optional maximum zoom.
599    #[serde(default)]
600    pub maxzoom: Option<f32>,
601    /// Layout properties.
602    #[serde(default)]
603    pub layout: Map<String, Value>,
604    /// Paint properties.
605    #[serde(default)]
606    pub paint: Map<String, Value>,
607}
608
609impl StyleSpecLayer {
610    fn to_runtime_layer(&self) -> Result<StyleLayer, StyleSpecError> {
611        let meta = self.meta()?;
612        let layer = match self.layer_type {
613            StyleSpecLayerType::Background => {
614                let mut layer = BackgroundStyleLayer::new(
615                    self.id.clone(),
616                    paint_color(&self.paint, "background-color")?
617                        .unwrap_or_else(|| [0.0, 0.0, 0.0, 1.0].into()),
618                );
619                layer.meta = meta;
620                StyleLayer::Background(layer)
621            }
622            StyleSpecLayerType::Hillshade => {
623                let mut layer = HillshadeStyleLayer::new(self.id.clone());
624                layer.meta = meta;
625                if let Some(value) = paint_color(&self.paint, "hillshade-highlight-color")? {
626                    layer.highlight_color = value;
627                }
628                if let Some(value) = paint_color(&self.paint, "hillshade-shadow-color")? {
629                    layer.shadow_color = value;
630                }
631                if let Some(value) = paint_color(&self.paint, "hillshade-accent-color")? {
632                    layer.accent_color = value;
633                }
634                if let Some(value) = paint_f32(&self.paint, "hillshade-illumination-direction")? {
635                    layer.illumination_direction_deg = value;
636                }
637                if let Some(value) = paint_f32(&self.paint, "hillshade-illumination-altitude")? {
638                    layer.illumination_altitude_deg = value;
639                }
640                if let Some(value) = paint_f32(&self.paint, "hillshade-exaggeration")? {
641                    layer.exaggeration = value;
642                }
643                StyleLayer::Hillshade(layer)
644            }
645            StyleSpecLayerType::Raster => {
646                let source = self.required_source()?;
647                let mut layer = RasterStyleLayer::new(self.id.clone(), source);
648                layer.meta = meta;
649                StyleLayer::Raster(layer)
650            }
651            StyleSpecLayerType::Vector => {
652                let source = self.required_source()?;
653                let mut layer = VectorStyleLayer::new(self.id.clone(), source);
654                layer.meta = meta;
655                layer.source_layer = self.source_layer.clone();
656                if let Some(value) = paint_color(&self.paint, "fill-color")? {
657                    layer.fill_color = value;
658                }
659                if let Some(value) = paint_color(&self.paint, "line-color")? {
660                    layer.stroke_color = value;
661                }
662                if let Some(value) = paint_f32(&self.paint, "line-width")? {
663                    layer.stroke_width = value;
664                }
665                StyleLayer::Vector(layer)
666            }
667            StyleSpecLayerType::Fill => {
668                let source = self.required_source()?;
669                let mut layer = FillStyleLayer::new(self.id.clone(), source);
670                layer.meta = meta;
671                layer.source_layer = self.source_layer.clone();
672                if let Some(value) = paint_color(&self.paint, "fill-color")? {
673                    layer.fill_color = value;
674                }
675                if let Some(value) = paint_color(&self.paint, "fill-outline-color")? {
676                    layer.outline_color = value;
677                }
678                if let Some(value) = paint_f32(&self.paint, "fill-outline-width")? {
679                    layer.outline_width = value;
680                }
681                StyleLayer::Fill(layer)
682            }
683            StyleSpecLayerType::Line => {
684                let source = self.required_source()?;
685                let mut layer = LineStyleLayer::new(self.id.clone(), source);
686                layer.meta = meta;
687                layer.source_layer = self.source_layer.clone();
688                if let Some(value) = paint_color(&self.paint, "line-color")? {
689                    layer.color = value;
690                }
691                if let Some(value) = paint_f32(&self.paint, "line-width")? {
692                    layer.width = value;
693                }
694                StyleLayer::Line(layer)
695            }
696            StyleSpecLayerType::Circle => {
697                let source = self.required_source()?;
698                let mut layer = CircleStyleLayer::new(self.id.clone(), source);
699                layer.meta = meta;
700                layer.source_layer = self.source_layer.clone();
701                if let Some(value) = paint_color(&self.paint, "circle-color")? {
702                    layer.color = value;
703                }
704                if let Some(value) = paint_f32(&self.paint, "circle-radius")? {
705                    layer.radius = value;
706                }
707                if let Some(value) = paint_color(&self.paint, "circle-stroke-color")? {
708                    layer.stroke_color = value;
709                }
710                if let Some(value) = paint_f32(&self.paint, "circle-stroke-width")? {
711                    layer.stroke_width = value;
712                }
713                StyleLayer::Circle(layer)
714            }
715            StyleSpecLayerType::Heatmap => {
716                let source = self.required_source()?;
717                let mut layer = HeatmapStyleLayer::new(self.id.clone(), source);
718                layer.meta = meta;
719                layer.source_layer = self.source_layer.clone();
720                if let Some(value) = paint_color(&self.paint, "heatmap-color")? {
721                    layer.color = value;
722                }
723                if let Some(value) = paint_f32(&self.paint, "heatmap-radius")? {
724                    layer.radius = value;
725                }
726                if let Some(value) = paint_f32(&self.paint, "heatmap-intensity")? {
727                    layer.intensity = value;
728                }
729                StyleLayer::Heatmap(layer)
730            }
731            StyleSpecLayerType::FillExtrusion => {
732                let source = self.required_source()?;
733                let mut layer = FillExtrusionStyleLayer::new(self.id.clone(), source);
734                layer.meta = meta;
735                layer.source_layer = self.source_layer.clone();
736                if let Some(value) = paint_color(&self.paint, "fill-extrusion-color")? {
737                    layer.color = value;
738                }
739                if let Some(value) = paint_f32(&self.paint, "fill-extrusion-base")? {
740                    layer.base = value;
741                }
742                if let Some(value) = paint_f32(&self.paint, "fill-extrusion-height")? {
743                    layer.height = value;
744                }
745                StyleLayer::FillExtrusion(layer)
746            }
747            StyleSpecLayerType::Symbol => {
748                let source = self.required_source()?;
749                let mut layer = SymbolStyleLayer::new(self.id.clone(), source);
750                layer.meta = meta;
751                layer.source_layer = self.source_layer.clone();
752                if let Some(value) = paint_color(&self.paint, "text-color")?
753                    .or_else(|| paint_color(&self.paint, "icon-color").ok().flatten())
754                {
755                    layer.color = value;
756                }
757                if let Some(value) = paint_color(&self.paint, "text-halo-color")? {
758                    layer.halo_color = value;
759                }
760                if let Some(value) = paint_f32(&self.layout, "text-size")?
761                    .or_else(|| paint_f32(&self.layout, "icon-size").ok().flatten())
762                {
763                    layer.size = value;
764                }
765                if let Some(value) = string_value(self.layout.get("text-field"))? {
766                    layer.text_field = Some(value);
767                }
768                if let Some(value) = string_value(self.layout.get("icon-image"))? {
769                    layer.icon_image = Some(value);
770                }
771                if let Some(value) = string_value(self.layout.get("text-font"))
772                    .or_else(|_| first_string_from_array(self.layout.get("text-font")))?
773                {
774                    layer.font_stack = value;
775                }
776                if let Some(value) = paint_f32(&self.layout, "text-padding")? {
777                    layer.padding = value;
778                }
779                if let Some(value) = bool_value(self.layout.get("text-allow-overlap"))? {
780                    layer.text_allow_overlap = Some(value);
781                }
782                if let Some(value) = bool_value(self.layout.get("icon-allow-overlap"))? {
783                    layer.icon_allow_overlap = Some(value);
784                }
785                if let Some(value) = bool_value(self.layout.get("text-optional"))? {
786                    layer.text_optional = Some(value);
787                }
788                if let Some(value) = bool_value(self.layout.get("icon-optional"))? {
789                    layer.icon_optional = Some(value);
790                }
791                if let Some(value) = bool_value(self.layout.get("text-ignore-placement"))? {
792                    layer.text_ignore_placement = Some(value);
793                }
794                if let Some(value) = bool_value(self.layout.get("icon-ignore-placement"))? {
795                    layer.icon_ignore_placement = Some(value);
796                }
797                if let Some(value) = paint_f32(&self.layout, "text-radial-offset")? {
798                    layer.radial_offset = Some(value);
799                }
800                if let Some(value) =
801                    symbol_anchor_offset_array(self.layout.get("text-variable-anchor-offset"))?
802                {
803                    layer.variable_anchor_offsets = Some(value);
804                }
805                if let Some(value) = symbol_anchor(self.layout.get("text-anchor"))? {
806                    layer.anchor = value;
807                }
808                if let Some(value) = symbol_text_justify(self.layout.get("text-justify"))? {
809                    layer.justify = value;
810                }
811                if let Some(value) = symbol_text_transform(self.layout.get("text-transform"))? {
812                    layer.transform = value;
813                }
814                if let Some(value) = paint_f32(&self.layout, "text-max-width")? {
815                    layer.max_width = Some(value);
816                }
817                if let Some(value) = paint_f32(&self.layout, "text-line-height")? {
818                    layer.line_height = Some(value);
819                }
820                if let Some(value) = paint_f32(&self.layout, "text-letter-spacing")? {
821                    layer.letter_spacing = Some(value);
822                }
823                if let Some(value) = symbol_icon_text_fit(self.layout.get("icon-text-fit"))? {
824                    layer.icon_text_fit = value;
825                }
826                if let Some(value) = vec4_value(
827                    self.layout.get("icon-text-fit-padding"),
828                    "icon-text-fit-padding",
829                )? {
830                    layer.icon_text_fit_padding = value;
831                }
832                if let Some(value) = paint_f32(&self.layout, "symbol-sort-key")? {
833                    layer.sort_key = Some(value);
834                }
835                if let Some(value) = symbol_placement(self.layout.get("symbol-placement"))? {
836                    layer.placement = value;
837                }
838                if let Some(value) = paint_f32(&self.layout, "symbol-spacing")? {
839                    layer.spacing = value;
840                }
841                if let Some(value) = paint_f32(&self.layout, "text-max-angle")? {
842                    layer.max_angle = value;
843                }
844                if let Some(value) = bool_value(self.layout.get("text-keep-upright"))? {
845                    layer.keep_upright = value;
846                }
847                if let Some(value) = symbol_anchor_array(self.layout.get("text-variable-anchor"))? {
848                    layer.variable_anchors = value;
849                }
850                if let Some(value) = symbol_writing_mode(self.layout.get("text-writing-mode"))? {
851                    layer.writing_mode = value;
852                }
853                if let Some(value) = vec2_value(self.layout.get("text-offset"))? {
854                    layer.offset = value;
855                }
856                StyleLayer::Symbol(layer)
857            }
858            StyleSpecLayerType::Model => {
859                let source = self.required_source()?;
860                let mut layer = ModelStyleLayer::new(self.id.clone(), source);
861                layer.meta = meta;
862                StyleLayer::Model(layer)
863            }
864        };
865        Ok(layer)
866    }
867
868    fn meta(&self) -> Result<StyleLayerMeta, StyleSpecError> {
869        let mut meta = StyleLayerMeta::new(self.id.clone());
870        meta.min_zoom = self.minzoom;
871        meta.max_zoom = self.maxzoom;
872
873        if let Some(value) = self.opacity_value()? {
874            meta.opacity = value;
875        }
876
877        if let Some(visibility) = self.layout.get("visibility") {
878            let visible = match visibility.as_str() {
879                Some("visible") | None => true,
880                Some("none") => false,
881                Some(_) => {
882                    return Err(StyleSpecError::InvalidField {
883                        id: self.id.clone(),
884                        field: "layout.visibility",
885                        expected: "`visible` or `none`",
886                    })
887                }
888            };
889            meta.visible = visible.into();
890        }
891
892        Ok(meta)
893    }
894
895    fn opacity_value(&self) -> Result<Option<StyleValue<f32>>, StyleSpecError> {
896        let keys = match self.layer_type {
897            StyleSpecLayerType::Background => &["background-opacity"][..],
898            StyleSpecLayerType::Hillshade => &["hillshade-opacity"],
899            StyleSpecLayerType::Raster => &["raster-opacity"],
900            StyleSpecLayerType::Vector => &["opacity"],
901            StyleSpecLayerType::Fill => &["fill-opacity"],
902            StyleSpecLayerType::Line => &["line-opacity"],
903            StyleSpecLayerType::Circle => &["circle-opacity"],
904            StyleSpecLayerType::Heatmap => &["heatmap-opacity"],
905            StyleSpecLayerType::FillExtrusion => &["fill-extrusion-opacity"],
906            StyleSpecLayerType::Symbol => &["text-opacity", "icon-opacity"],
907            StyleSpecLayerType::Model => &["model-opacity"],
908        };
909
910        for key in keys {
911            if let Some(value) = self.paint.get(*key) {
912                return Ok(Some(f32_value_from_json(&self.id, key, value)?));
913            }
914        }
915        Ok(None)
916    }
917
918    fn required_source(&self) -> Result<String, StyleSpecError> {
919        self.source
920            .clone()
921            .ok_or_else(|| StyleSpecError::MissingLayerSource(self.id.clone()))
922    }
923}
924
925/// Parse a JSON style string.
926pub fn parse_style_json(json: &str) -> Result<StyleSpecDocument, StyleSpecError> {
927    StyleSpecDocument::from_json(json)
928}
929
930fn paint_color(
931    map: &Map<String, Value>,
932    key: &'static str,
933) -> Result<Option<StyleValue<[f32; 4]>>, StyleSpecError> {
934    map.get(key)
935        .map(|value| color_value_from_json(key, key, value))
936        .transpose()
937}
938
939fn paint_f32(
940    map: &Map<String, Value>,
941    key: &'static str,
942) -> Result<Option<StyleValue<f32>>, StyleSpecError> {
943    map.get(key)
944        .map(|value| f32_value_from_json(key, key, value))
945        .transpose()
946}
947
948fn string_value(value: Option<&Value>) -> Result<Option<StyleValue<String>>, StyleSpecError> {
949    value
950        .map(|value| string_value_from_json("symbol", "text-field", value))
951        .transpose()
952}
953
954fn bool_value(value: Option<&Value>) -> Result<Option<StyleValue<bool>>, StyleSpecError> {
955    value
956        .map(|value| bool_value_from_json("symbol", "allow-overlap", value))
957        .transpose()
958}
959
960fn first_string_from_array(
961    value: Option<&Value>,
962) -> Result<Option<StyleValue<String>>, StyleSpecError> {
963    let Some(value) = value else {
964        return Ok(None);
965    };
966    let Some(array) = value.as_array() else {
967        return Err(StyleSpecError::InvalidField {
968            id: "symbol".into(),
969            field: "text-font",
970            expected: "string or string array",
971        });
972    };
973    let Some(first) = array.first() else {
974        return Ok(None);
975    };
976    Ok(Some(string_value_from_json("symbol", "text-font", first)?))
977}
978
979fn vec2_value(value: Option<&Value>) -> Result<Option<[f32; 2]>, StyleSpecError> {
980    let Some(value) = value else {
981        return Ok(None);
982    };
983    let Some(array) = value.as_array() else {
984        return Err(StyleSpecError::InvalidField {
985            id: "symbol".into(),
986            field: "text-offset",
987            expected: "two-element numeric array",
988        });
989    };
990    if array.len() != 2 {
991        return Err(StyleSpecError::InvalidField {
992            id: "symbol".into(),
993            field: "text-offset",
994            expected: "two-element numeric array",
995        });
996    }
997    Ok(Some([
998        array[0]
999            .as_f64()
1000            .ok_or_else(|| StyleSpecError::InvalidField {
1001                id: "symbol".into(),
1002                field: "text-offset",
1003                expected: "two-element numeric array",
1004            })? as f32,
1005        array[1]
1006            .as_f64()
1007            .ok_or_else(|| StyleSpecError::InvalidField {
1008                id: "symbol".into(),
1009                field: "text-offset",
1010                expected: "two-element numeric array",
1011            })? as f32,
1012    ]))
1013}
1014
1015fn symbol_anchor_array(value: Option<&Value>) -> Result<Option<Vec<SymbolAnchor>>, StyleSpecError> {
1016    let Some(value) = value else {
1017        return Ok(None);
1018    };
1019    let Some(array) = value.as_array() else {
1020        return Err(StyleSpecError::InvalidField {
1021            id: "symbol".into(),
1022            field: "text-variable-anchor",
1023            expected: "string array",
1024        });
1025    };
1026    let mut anchors = Vec::with_capacity(array.len());
1027    for value in array {
1028        let Some(text) = value.as_str() else {
1029            return Err(StyleSpecError::InvalidField {
1030                id: "symbol".into(),
1031                field: "text-variable-anchor",
1032                expected: "string array",
1033            });
1034        };
1035        anchors.push(parse_symbol_anchor(text)?);
1036    }
1037    Ok(Some(anchors))
1038}
1039
1040#[allow(clippy::type_complexity)]
1041fn symbol_anchor_offset_array(
1042    value: Option<&Value>,
1043) -> Result<Option<Vec<(SymbolAnchor, [f32; 2])>>, StyleSpecError> {
1044    let Some(value) = value else {
1045        return Ok(None);
1046    };
1047    let Some(array) = value.as_array() else {
1048        return Err(StyleSpecError::InvalidField {
1049            id: "symbol".into(),
1050            field: "text-variable-anchor-offset",
1051            expected: "alternating anchor / [x, y] array",
1052        });
1053    };
1054    if array.len() % 2 != 0 {
1055        return Err(StyleSpecError::InvalidField {
1056            id: "symbol".into(),
1057            field: "text-variable-anchor-offset",
1058            expected: "alternating anchor / [x, y] array",
1059        });
1060    }
1061
1062    let mut parsed = Vec::with_capacity(array.len() / 2);
1063    for pair in array.chunks_exact(2) {
1064        let anchor_text = pair[0]
1065            .as_str()
1066            .ok_or_else(|| StyleSpecError::InvalidField {
1067                id: "symbol".into(),
1068                field: "text-variable-anchor-offset",
1069                expected: "anchor string in alternating anchor / [x, y] array",
1070            })?;
1071        let anchor = parse_symbol_anchor(anchor_text)?;
1072        let offset = parse_vec2_array(
1073            "symbol",
1074            "text-variable-anchor-offset",
1075            pair[1]
1076                .as_array()
1077                .ok_or_else(|| StyleSpecError::InvalidField {
1078                    id: "symbol".into(),
1079                    field: "text-variable-anchor-offset",
1080                    expected: "[x, y] offset array in alternating anchor / [x, y] array",
1081                })?,
1082        )?;
1083        parsed.push((anchor, offset));
1084    }
1085
1086    Ok(Some(parsed))
1087}
1088
1089fn symbol_writing_mode(value: Option<&Value>) -> Result<Option<SymbolWritingMode>, StyleSpecError> {
1090    let Some(value) = value else {
1091        return Ok(None);
1092    };
1093    let Some(text) = value.as_str() else {
1094        return Err(StyleSpecError::InvalidField {
1095            id: "symbol".into(),
1096            field: "text-writing-mode",
1097            expected: "string value",
1098        });
1099    };
1100    Ok(Some(parse_symbol_writing_mode(text)?))
1101}
1102
1103fn symbol_placement(value: Option<&Value>) -> Result<Option<SymbolPlacement>, StyleSpecError> {
1104    let Some(value) = value else {
1105        return Ok(None);
1106    };
1107    let Some(text) = value.as_str() else {
1108        return Err(StyleSpecError::InvalidField {
1109            id: "symbol".into(),
1110            field: "symbol-placement",
1111            expected: "string value",
1112        });
1113    };
1114    Ok(Some(parse_symbol_placement(text)?))
1115}
1116
1117fn symbol_anchor(value: Option<&Value>) -> Result<Option<SymbolAnchor>, StyleSpecError> {
1118    let Some(value) = value else {
1119        return Ok(None);
1120    };
1121    let Some(text) = value.as_str() else {
1122        return Err(StyleSpecError::InvalidField {
1123            id: "symbol".into(),
1124            field: "text-anchor",
1125            expected: "string value",
1126        });
1127    };
1128    Ok(Some(parse_symbol_anchor(text)?))
1129}
1130
1131fn symbol_text_justify(
1132    value: Option<&Value>,
1133) -> Result<Option<StyleValue<SymbolTextJustify>>, StyleSpecError> {
1134    let Some(value) = value else {
1135        return Ok(None);
1136    };
1137    Ok(Some(match value {
1138        Value::String(text) => StyleValue::Constant(parse_symbol_text_justify(text)?),
1139        Value::Object(object) => {
1140            parse_zoom_stops("symbol", "text-justify", object, |id, field, value| {
1141                let text = value.as_str().ok_or_else(|| StyleSpecError::InvalidField {
1142                    id: id.to_owned(),
1143                    field,
1144                    expected: "string value",
1145                })?;
1146                parse_symbol_text_justify(text)
1147            })?
1148        }
1149
1150        _ => {
1151            return Err(StyleSpecError::InvalidField {
1152                id: "symbol".into(),
1153                field: "text-justify",
1154                expected: "string value or `{ stops: ... }`",
1155            })
1156        }
1157    }))
1158}
1159
1160fn symbol_icon_text_fit(
1161    value: Option<&Value>,
1162) -> Result<Option<StyleValue<SymbolIconTextFit>>, StyleSpecError> {
1163    let Some(value) = value else {
1164        return Ok(None);
1165    };
1166    Ok(Some(match value {
1167        Value::String(text) => StyleValue::Constant(parse_symbol_icon_text_fit(text)?),
1168        Value::Object(object) => {
1169            parse_zoom_stops("symbol", "icon-text-fit", object, |id, field, value| {
1170                let text = value.as_str().ok_or_else(|| StyleSpecError::InvalidField {
1171                    id: id.to_owned(),
1172                    field,
1173                    expected: "string value",
1174                })?;
1175                parse_symbol_icon_text_fit(text)
1176            })?
1177        }
1178        _ => {
1179            return Err(StyleSpecError::InvalidField {
1180                id: "symbol".into(),
1181                field: "icon-text-fit",
1182                expected: "string value or `{ stops: ... }`",
1183            })
1184        }
1185    }))
1186}
1187
1188fn symbol_text_transform(
1189    value: Option<&Value>,
1190) -> Result<Option<StyleValue<SymbolTextTransform>>, StyleSpecError> {
1191    let Some(value) = value else {
1192        return Ok(None);
1193    };
1194    Ok(Some(match value {
1195        Value::String(text) => StyleValue::Constant(parse_symbol_text_transform(text)?),
1196        Value::Object(object) => {
1197            parse_zoom_stops("symbol", "text-transform", object, |id, field, value| {
1198                let text = value.as_str().ok_or_else(|| StyleSpecError::InvalidField {
1199                    id: id.to_owned(),
1200                    field,
1201                    expected: "string value",
1202                })?;
1203                parse_symbol_text_transform(text)
1204            })?
1205        }
1206        _ => {
1207            return Err(StyleSpecError::InvalidField {
1208                id: "symbol".into(),
1209                field: "text-transform",
1210                expected: "string value or `{ stops: ... }`",
1211            })
1212        }
1213    }))
1214}
1215
1216fn parse_symbol_anchor(text: &str) -> Result<SymbolAnchor, StyleSpecError> {
1217    match text {
1218        "center" => Ok(SymbolAnchor::Center),
1219        "top" => Ok(SymbolAnchor::Top),
1220        "bottom" => Ok(SymbolAnchor::Bottom),
1221        "left" => Ok(SymbolAnchor::Left),
1222        "right" => Ok(SymbolAnchor::Right),
1223        "top-left" => Ok(SymbolAnchor::TopLeft),
1224        "top-right" => Ok(SymbolAnchor::TopRight),
1225        "bottom-left" => Ok(SymbolAnchor::BottomLeft),
1226        "bottom-right" => Ok(SymbolAnchor::BottomRight),
1227        _ => Err(StyleSpecError::InvalidField {
1228            id: "symbol".into(),
1229            field: "text-variable-anchor",
1230            expected: "known symbol-anchor string",
1231        }),
1232    }
1233}
1234
1235fn parse_symbol_icon_text_fit(text: &str) -> Result<SymbolIconTextFit, StyleSpecError> {
1236    match text {
1237        "none" => Ok(SymbolIconTextFit::None),
1238        "width" => Ok(SymbolIconTextFit::Width),
1239        "height" => Ok(SymbolIconTextFit::Height),
1240        "both" => Ok(SymbolIconTextFit::Both),
1241        _ => Err(StyleSpecError::InvalidField {
1242            id: "symbol".into(),
1243            field: "icon-text-fit",
1244            expected: "`none`, `width`, `height`, or `both`",
1245        }),
1246    }
1247}
1248
1249fn parse_symbol_text_transform(text: &str) -> Result<SymbolTextTransform, StyleSpecError> {
1250    match text {
1251        "none" => Ok(SymbolTextTransform::None),
1252        "uppercase" => Ok(SymbolTextTransform::Uppercase),
1253        "lowercase" => Ok(SymbolTextTransform::Lowercase),
1254        _ => Err(StyleSpecError::InvalidField {
1255            id: "symbol".into(),
1256            field: "text-transform",
1257            expected: "`none`, `uppercase`, or `lowercase`",
1258        }),
1259    }
1260}
1261
1262fn parse_symbol_text_justify(text: &str) -> Result<SymbolTextJustify, StyleSpecError> {
1263    match text {
1264        "auto" => Ok(SymbolTextJustify::Auto),
1265        "left" => Ok(SymbolTextJustify::Left),
1266        "center" => Ok(SymbolTextJustify::Center),
1267        "right" => Ok(SymbolTextJustify::Right),
1268        _ => Err(StyleSpecError::InvalidField {
1269            id: "symbol".into(),
1270            field: "text-justify",
1271            expected: "`auto`, `left`, `center`, or `right`",
1272        }),
1273    }
1274}
1275
1276fn parse_symbol_placement(text: &str) -> Result<SymbolPlacement, StyleSpecError> {
1277    match text {
1278        "point" => Ok(SymbolPlacement::Point),
1279        "line" => Ok(SymbolPlacement::Line),
1280        _ => Err(StyleSpecError::InvalidField {
1281            id: "symbol".into(),
1282            field: "symbol-placement",
1283            expected: "`point` or `line`",
1284        }),
1285    }
1286}
1287
1288fn parse_symbol_writing_mode(text: &str) -> Result<SymbolWritingMode, StyleSpecError> {
1289    match text {
1290        "horizontal" => Ok(SymbolWritingMode::Horizontal),
1291        "vertical" => Ok(SymbolWritingMode::Vertical),
1292        _ => Err(StyleSpecError::InvalidField {
1293            id: "symbol".into(),
1294            field: "text-writing-mode",
1295            expected: "`horizontal` or `vertical`",
1296        }),
1297    }
1298}
1299
1300fn bool_value_from_json(
1301    id: &str,
1302    field: &'static str,
1303    value: &Value,
1304) -> Result<StyleValue<bool>, StyleSpecError> {
1305    match value {
1306        Value::Bool(value) => Ok(StyleValue::Constant(*value)),
1307        Value::Object(object) => parse_zoom_stops(id, field, object, parse_bool_constant),
1308        Value::Array(arr) if is_expression_array(arr) => parse_expression_bool(id, field, arr),
1309        _ => Err(StyleSpecError::InvalidField {
1310            id: id.to_owned(),
1311            field,
1312            expected: "boolean value, `{ stops: ... }`, or expression array",
1313        }),
1314    }
1315}
1316
1317fn f32_value_from_json(
1318    id: &str,
1319    field: &'static str,
1320    value: &Value,
1321) -> Result<StyleValue<f32>, StyleSpecError> {
1322    match value {
1323        Value::Number(number) => {
1324            Ok(StyleValue::Constant(
1325                number
1326                    .as_f64()
1327                    .ok_or_else(|| StyleSpecError::InvalidField {
1328                        id: id.to_owned(),
1329                        field,
1330                        expected: "numeric value",
1331                    })? as f32,
1332            ))
1333        }
1334        Value::Object(object) => parse_zoom_stops(id, field, object, parse_f32_constant),
1335        Value::Array(arr) if is_expression_array(arr) => parse_expression_f32(id, field, arr),
1336        _ => Err(StyleSpecError::InvalidField {
1337            id: id.to_owned(),
1338            field,
1339            expected: "numeric value, `{ stops: ... }`, or expression array",
1340        }),
1341    }
1342}
1343
1344fn string_value_from_json(
1345    id: &str,
1346    field: &'static str,
1347    value: &Value,
1348) -> Result<StyleValue<String>, StyleSpecError> {
1349    match value {
1350        Value::String(text) => Ok(StyleValue::Constant(text.clone())),
1351        Value::Object(object) => parse_zoom_stops(id, field, object, parse_string_constant),
1352        Value::Array(arr) if is_expression_array(arr) => parse_expression_string(id, field, arr),
1353        _ => Err(StyleSpecError::InvalidField {
1354            id: id.to_owned(),
1355            field,
1356            expected: "string value, `{ stops: ... }`, or expression array",
1357        }),
1358    }
1359}
1360
1361fn color_value_from_json(
1362    id: &str,
1363    field: &'static str,
1364    value: &Value,
1365) -> Result<StyleValue<[f32; 4]>, StyleSpecError> {
1366    match value {
1367        Value::String(text) => Ok(StyleValue::Constant(parse_color_string(id, field, text)?)),
1368        Value::Array(array) if is_expression_array(array) => {
1369            parse_expression_color(id, field, array)
1370        }
1371        Value::Array(array) => Ok(StyleValue::Constant(parse_color_array(id, field, array)?)),
1372        Value::Object(object) => {
1373            parse_zoom_stops(id, field, object, |id, field, value| match value {
1374                Value::String(text) => parse_color_string(id, field, text),
1375                Value::Array(array) => parse_color_array(id, field, array),
1376                _ => Err(StyleSpecError::InvalidField {
1377                    id: id.to_owned(),
1378                    field,
1379                    expected: "color string or RGBA array",
1380                }),
1381            })
1382        }
1383        _ => Err(StyleSpecError::InvalidField {
1384            id: id.to_owned(),
1385            field,
1386            expected: "color string, RGBA array, or `{ stops: ... }`",
1387        }),
1388    }
1389}
1390
1391fn parse_zoom_stops<T>(
1392    id: &str,
1393    field: &'static str,
1394    object: &Map<String, Value>,
1395    parse_value: impl Fn(&str, &'static str, &Value) -> Result<T, StyleSpecError>,
1396) -> Result<StyleValue<T>, StyleSpecError> {
1397    let Some(stops) = object.get("stops").and_then(Value::as_array) else {
1398        return Err(StyleSpecError::InvalidField {
1399            id: id.to_owned(),
1400            field,
1401            expected: "object with `stops` array",
1402        });
1403    };
1404
1405    let mut parsed = Vec::with_capacity(stops.len());
1406    for stop in stops {
1407        let pair = stop
1408            .as_array()
1409            .ok_or_else(|| StyleSpecError::InvalidField {
1410                id: id.to_owned(),
1411                field,
1412                expected: "stop tuple `[zoom, value]`",
1413            })?;
1414        if pair.len() != 2 {
1415            return Err(StyleSpecError::InvalidField {
1416                id: id.to_owned(),
1417                field,
1418                expected: "stop tuple `[zoom, value]`",
1419            });
1420        }
1421        let zoom = pair[0]
1422            .as_f64()
1423            .ok_or_else(|| StyleSpecError::InvalidField {
1424                id: id.to_owned(),
1425                field,
1426                expected: "numeric stop zoom",
1427            })? as f32;
1428        let value = parse_value(id, field, &pair[1])?;
1429        parsed.push((zoom, value));
1430    }
1431    Ok(StyleValue::ZoomStops(parsed))
1432}
1433
1434fn parse_f32_constant(id: &str, field: &'static str, value: &Value) -> Result<f32, StyleSpecError> {
1435    value
1436        .as_f64()
1437        .map(|v| v as f32)
1438        .ok_or_else(|| StyleSpecError::InvalidField {
1439            id: id.to_owned(),
1440            field,
1441            expected: "numeric value",
1442        })
1443}
1444
1445fn parse_string_constant(
1446    id: &str,
1447    field: &'static str,
1448    value: &Value,
1449) -> Result<String, StyleSpecError> {
1450    value
1451        .as_str()
1452        .map(ToOwned::to_owned)
1453        .ok_or_else(|| StyleSpecError::InvalidField {
1454            id: id.to_owned(),
1455            field,
1456            expected: "string value",
1457        })
1458}
1459
1460fn parse_bool_constant(
1461    id: &str,
1462    field: &'static str,
1463    value: &Value,
1464) -> Result<bool, StyleSpecError> {
1465    value.as_bool().ok_or_else(|| StyleSpecError::InvalidField {
1466        id: id.to_owned(),
1467        field,
1468        expected: "boolean value",
1469    })
1470}
1471
1472fn vec4_value(
1473    value: Option<&Value>,
1474    field: &'static str,
1475) -> Result<Option<[f32; 4]>, StyleSpecError> {
1476    let Some(value) = value else {
1477        return Ok(None);
1478    };
1479    let arr = value
1480        .as_array()
1481        .ok_or_else(|| StyleSpecError::InvalidField {
1482            id: "symbol".into(),
1483            field,
1484            expected: "array of 4 numbers",
1485        })?;
1486    if arr.len() != 4 {
1487        return Err(StyleSpecError::InvalidField {
1488            id: "symbol".into(),
1489            field,
1490            expected: "array of 4 numbers",
1491        });
1492    }
1493    let mut out = [0.0f32; 4];
1494    for (i, v) in arr.iter().enumerate() {
1495        out[i] = v.as_f64().ok_or_else(|| StyleSpecError::InvalidField {
1496            id: "symbol".into(),
1497            field,
1498            expected: "numeric array element",
1499        })? as f32;
1500    }
1501    Ok(Some(out))
1502}
1503
1504fn parse_vec2_array(
1505    id: &str,
1506    field: &'static str,
1507    arr: &[Value],
1508) -> Result<[f32; 2], StyleSpecError> {
1509    if arr.len() != 2 {
1510        return Err(StyleSpecError::InvalidField {
1511            id: id.to_owned(),
1512            field,
1513            expected: "[x, y] array of 2 numbers",
1514        });
1515    }
1516    let x = arr[0]
1517        .as_f64()
1518        .ok_or_else(|| StyleSpecError::InvalidField {
1519            id: id.to_owned(),
1520            field,
1521            expected: "numeric value in [x, y] array",
1522        })? as f32;
1523    let y = arr[1]
1524        .as_f64()
1525        .ok_or_else(|| StyleSpecError::InvalidField {
1526            id: id.to_owned(),
1527            field,
1528            expected: "numeric value in [x, y] array",
1529        })? as f32;
1530    Ok([x, y])
1531}
1532
1533fn parse_color_array(
1534    id: &str,
1535    field: &'static str,
1536    array: &[Value],
1537) -> Result<[f32; 4], StyleSpecError> {
1538    if !(array.len() == 3 || array.len() == 4) {
1539        return Err(StyleSpecError::InvalidField {
1540            id: id.to_owned(),
1541            field,
1542            expected: "RGB or RGBA array",
1543        });
1544    }
1545    let mut rgba = [0.0, 0.0, 0.0, 1.0];
1546    for (i, value) in array.iter().enumerate() {
1547        rgba[i] = value.as_f64().ok_or_else(|| StyleSpecError::InvalidField {
1548            id: id.to_owned(),
1549            field,
1550            expected: "numeric RGB/RGBA components",
1551        })? as f32;
1552    }
1553    Ok(rgba)
1554}
1555
1556fn parse_color_string(
1557    id: &str,
1558    field: &'static str,
1559    text: &str,
1560) -> Result<[f32; 4], StyleSpecError> {
1561    let hex = text
1562        .strip_prefix('#')
1563        .ok_or_else(|| StyleSpecError::InvalidField {
1564            id: id.to_owned(),
1565            field,
1566            expected: "hex color string like `#RRGGBB` or `#RRGGBBAA`",
1567        })?;
1568
1569    let rgba = match hex.len() {
1570        3 => {
1571            let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok();
1572            let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok();
1573            let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok();
1574            [r, g, b, Some(255)]
1575        }
1576        4 => {
1577            let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok();
1578            let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok();
1579            let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok();
1580            let a = u8::from_str_radix(&hex[3..4].repeat(2), 16).ok();
1581            [r, g, b, a]
1582        }
1583        6 => {
1584            let r = u8::from_str_radix(&hex[0..2], 16).ok();
1585            let g = u8::from_str_radix(&hex[2..4], 16).ok();
1586            let b = u8::from_str_radix(&hex[4..6], 16).ok();
1587            [r, g, b, Some(255)]
1588        }
1589        8 => {
1590            let r = u8::from_str_radix(&hex[0..2], 16).ok();
1591            let g = u8::from_str_radix(&hex[2..4], 16).ok();
1592            let b = u8::from_str_radix(&hex[4..6], 16).ok();
1593            let a = u8::from_str_radix(&hex[6..8], 16).ok();
1594            [r, g, b, a]
1595        }
1596        _ => [None, None, None, None],
1597    };
1598
1599    match rgba {
1600        [Some(r), Some(g), Some(b), Some(a)] => Ok([
1601            r as f32 / 255.0,
1602            g as f32 / 255.0,
1603            b as f32 / 255.0,
1604            a as f32 / 255.0,
1605        ]),
1606        _ => Err(StyleSpecError::InvalidField {
1607            id: id.to_owned(),
1608            field,
1609            expected: "hex color string like `#RRGGBB` or `#RRGGBBAA`",
1610        }),
1611    }
1612}
1613
1614fn default_style_version() -> u8 {
1615    8
1616}
1617
1618// ---------------------------------------------------------------------------
1619// MapLibre expression JSON parsing
1620// ---------------------------------------------------------------------------
1621//
1622// MapLibre expressions are JSON arrays where the first element is a string
1623// operator name. This parser converts them into the typed `Expression<T>`
1624// AST. For example:
1625//
1626//   ["get", "height"]          → Expression::GetProperty { key: "height", fallback }
1627//   ["interpolate", ["linear"], ["zoom"], 0, 1, 20, 10]
1628//                              → Expression::Interpolate { input: Zoom, stops }
1629//   ["match", ["get", "type"], "residential", "#00f", "commercial", "#f00", "#888"]
1630//                              → Expression::Match { input, cases, fallback }
1631//   ["step", ["zoom"], 1, 5, 2, 10, 3]
1632//                              → Expression::Step { input: Zoom, default, stops }
1633//   ["case", cond1, val1, ..., fallback]
1634//                              → Expression::Case { branches, fallback }
1635
1636use crate::expression::{BoolExpression, Expression, NumericExpression, StringExpression};
1637
1638/// Check whether a JSON array looks like a MapLibre expression (starts with a string operator).
1639fn is_expression_array(arr: &[Value]) -> bool {
1640    arr.first()
1641        .and_then(Value::as_str)
1642        .map(|op| {
1643            matches!(
1644                op,
1645                "get"
1646                    | "has"
1647                    | "!"
1648                    | "all"
1649                    | "any"
1650                    | "=="
1651                    | "!="
1652                    | ">"
1653                    | ">="
1654                    | "<"
1655                    | "<="
1656                    | "+"
1657                    | "-"
1658                    | "*"
1659                    | "/"
1660                    | "%"
1661                    | "^"
1662                    | "abs"
1663                    | "ln"
1664                    | "sqrt"
1665                    | "min"
1666                    | "max"
1667                    | "interpolate"
1668                    | "step"
1669                    | "match"
1670                    | "case"
1671                    | "coalesce"
1672                    | "zoom"
1673                    | "pitch"
1674                    | "concat"
1675                    | "upcase"
1676                    | "downcase"
1677                    | "feature-state"
1678                    | "to-number"
1679                    | "to-string"
1680                    | "to-boolean"
1681                    | "literal"
1682            )
1683        })
1684        .unwrap_or(false)
1685}
1686
1687/// Parse a MapLibre expression array into an `Expression<f32>`.
1688fn parse_expression_f32(
1689    id: &str,
1690    field: &'static str,
1691    arr: &[Value],
1692) -> Result<StyleValue<f32>, StyleSpecError> {
1693    let op = arr[0].as_str().unwrap_or("");
1694    match op {
1695        "literal" if arr.len() == 2 => {
1696            let v = arr[1]
1697                .as_f64()
1698                .ok_or_else(|| expr_err(id, field, "literal number"))? as f32;
1699            Ok(Expression::Constant(v))
1700        }
1701        "get" if arr.len() == 2 => {
1702            let key = arr[1]
1703                .as_str()
1704                .ok_or_else(|| expr_err(id, field, "get property key"))?;
1705            Ok(Expression::GetProperty {
1706                key: key.to_owned(),
1707                fallback: 0.0,
1708            })
1709        }
1710        "feature-state" if arr.len() == 2 => {
1711            let key = arr[1]
1712                .as_str()
1713                .ok_or_else(|| expr_err(id, field, "feature-state key"))?;
1714            Ok(Expression::FeatureState {
1715                key: key.to_owned(),
1716                fallback: 0.0,
1717            })
1718        }
1719        "zoom" => Ok(Expression::Interpolate {
1720            input: Box::new(NumericExpression::Zoom),
1721            stops: vec![],
1722        }),
1723        "interpolate" => parse_interpolate_f32(id, field, arr),
1724        "step" => parse_step_f32(id, field, arr),
1725        "case" => parse_case_f32(id, field, arr),
1726        "coalesce" => {
1727            let exprs: Result<Vec<_>, _> = arr[1..]
1728                .iter()
1729                .map(|v| match v {
1730                    Value::Number(n) => Ok(Expression::Constant(n.as_f64().unwrap_or(0.0) as f32)),
1731                    Value::Array(a) if is_expression_array(a) => parse_expression_f32(id, field, a),
1732                    _ => Err(expr_err(id, field, "coalesce sub-expression")),
1733                })
1734                .collect();
1735            Ok(Expression::Coalesce(exprs?))
1736        }
1737        _ => Err(expr_err(
1738            id,
1739            field,
1740            &format!("f32 expression (unsupported operator `{op}`)"),
1741        )),
1742    }
1743}
1744
1745/// Parse a MapLibre expression array into an `Expression<[f32; 4]>` (color).
1746fn parse_expression_color(
1747    id: &str,
1748    field: &'static str,
1749    arr: &[Value],
1750) -> Result<StyleValue<[f32; 4]>, StyleSpecError> {
1751    let op = arr[0].as_str().unwrap_or("");
1752    match op {
1753        "literal" if arr.len() == 2 => {
1754            if let Some(a) = arr[1].as_array() {
1755                let c = parse_color_array(id, field, a)?;
1756                Ok(Expression::Constant(c))
1757            } else {
1758                Err(expr_err(id, field, "literal color array"))
1759            }
1760        }
1761        "interpolate" => parse_interpolate_color(id, field, arr),
1762        "step" => parse_step_color(id, field, arr),
1763        "match" => parse_match_color(id, field, arr),
1764        "case" => parse_case_color(id, field, arr),
1765        _ => Err(expr_err(
1766            id,
1767            field,
1768            &format!("color expression (unsupported operator `{op}`)"),
1769        )),
1770    }
1771}
1772
1773/// Parse a MapLibre expression array into an `Expression<String>`.
1774fn parse_expression_string(
1775    id: &str,
1776    field: &'static str,
1777    arr: &[Value],
1778) -> Result<StyleValue<String>, StyleSpecError> {
1779    let op = arr[0].as_str().unwrap_or("");
1780    match op {
1781        "literal" if arr.len() == 2 => {
1782            let s = arr[1]
1783                .as_str()
1784                .ok_or_else(|| expr_err(id, field, "literal string"))?;
1785            Ok(Expression::Constant(s.to_owned()))
1786        }
1787        "get" if arr.len() == 2 => {
1788            let key = arr[1]
1789                .as_str()
1790                .ok_or_else(|| expr_err(id, field, "get property key"))?;
1791            Ok(Expression::GetProperty {
1792                key: key.to_owned(),
1793                fallback: String::new(),
1794            })
1795        }
1796        "feature-state" if arr.len() == 2 => {
1797            let key = arr[1]
1798                .as_str()
1799                .ok_or_else(|| expr_err(id, field, "feature-state key"))?;
1800            Ok(Expression::FeatureState {
1801                key: key.to_owned(),
1802                fallback: String::new(),
1803            })
1804        }
1805        "concat" => {
1806            let exprs: Result<Vec<_>, _> = arr[1..]
1807                .iter()
1808                .map(|v| match v {
1809                    Value::String(s) => Ok(Expression::Constant(s.clone())),
1810                    Value::Array(a) if is_expression_array(a) => {
1811                        parse_expression_string(id, field, a)
1812                    }
1813                    _ => Err(expr_err(id, field, "concat sub-expression")),
1814                })
1815                .collect();
1816            Ok(Expression::Coalesce(exprs?))
1817        }
1818        _ => Err(expr_err(
1819            id,
1820            field,
1821            &format!("string expression (unsupported operator `{op}`)"),
1822        )),
1823    }
1824}
1825
1826/// Parse a MapLibre expression array into an `Expression<bool>`.
1827fn parse_expression_bool(
1828    id: &str,
1829    field: &'static str,
1830    arr: &[Value],
1831) -> Result<StyleValue<bool>, StyleSpecError> {
1832    let op = arr[0].as_str().unwrap_or("");
1833    match op {
1834        "literal" if arr.len() == 2 => {
1835            let b = arr[1]
1836                .as_bool()
1837                .ok_or_else(|| expr_err(id, field, "literal boolean"))?;
1838            Ok(Expression::Constant(b))
1839        }
1840        "get" if arr.len() == 2 => {
1841            let key = arr[1]
1842                .as_str()
1843                .ok_or_else(|| expr_err(id, field, "get property key"))?;
1844            Ok(Expression::GetProperty {
1845                key: key.to_owned(),
1846                fallback: false,
1847            })
1848        }
1849        "feature-state" if arr.len() == 2 => {
1850            let key = arr[1]
1851                .as_str()
1852                .ok_or_else(|| expr_err(id, field, "feature-state key"))?;
1853            Ok(Expression::FeatureState {
1854                key: key.to_owned(),
1855                fallback: false,
1856            })
1857        }
1858        "has" if arr.len() == 2 => {
1859            let key = arr[1]
1860                .as_str()
1861                .ok_or_else(|| expr_err(id, field, "has property key"))?;
1862            Ok(Expression::Case {
1863                branches: vec![(BoolExpression::Has(key.to_owned()), true)],
1864                fallback: false,
1865            })
1866        }
1867        _ => Err(expr_err(
1868            id,
1869            field,
1870            &format!("boolean expression (unsupported operator `{op}`)"),
1871        )),
1872    }
1873}
1874
1875// ---------------------------------------------------------------------------
1876// Expression sub-parsers
1877// ---------------------------------------------------------------------------
1878
1879/// Parse a numeric sub-expression (used as input to interpolate/step).
1880fn parse_numeric_input(
1881    id: &str,
1882    field: &'static str,
1883    value: &Value,
1884) -> Result<NumericExpression, StyleSpecError> {
1885    match value {
1886        Value::Number(n) => Ok(NumericExpression::Literal(n.as_f64().unwrap_or(0.0))),
1887        Value::Array(arr) if !arr.is_empty() => {
1888            let op = arr[0].as_str().unwrap_or("");
1889            match op {
1890                "zoom" => Ok(NumericExpression::Zoom),
1891                "pitch" => Ok(NumericExpression::Pitch),
1892                "get" if arr.len() == 2 => {
1893                    let key = arr[1]
1894                        .as_str()
1895                        .ok_or_else(|| expr_err(id, field, "get key"))?;
1896                    Ok(NumericExpression::GetProperty {
1897                        key: key.to_owned(),
1898                        fallback: 0.0,
1899                    })
1900                }
1901                "feature-state" if arr.len() == 2 => {
1902                    let key = arr[1]
1903                        .as_str()
1904                        .ok_or_else(|| expr_err(id, field, "feature-state key"))?;
1905                    Ok(NumericExpression::GetState {
1906                        key: key.to_owned(),
1907                        fallback: 0.0,
1908                    })
1909                }
1910                "+" if arr.len() == 3 => Ok(NumericExpression::Add(
1911                    Box::new(parse_numeric_input(id, field, &arr[1])?),
1912                    Box::new(parse_numeric_input(id, field, &arr[2])?),
1913                )),
1914                "-" if arr.len() == 3 => Ok(NumericExpression::Sub(
1915                    Box::new(parse_numeric_input(id, field, &arr[1])?),
1916                    Box::new(parse_numeric_input(id, field, &arr[2])?),
1917                )),
1918                "*" if arr.len() == 3 => Ok(NumericExpression::Mul(
1919                    Box::new(parse_numeric_input(id, field, &arr[1])?),
1920                    Box::new(parse_numeric_input(id, field, &arr[2])?),
1921                )),
1922                "/" if arr.len() == 3 => Ok(NumericExpression::Div(
1923                    Box::new(parse_numeric_input(id, field, &arr[1])?),
1924                    Box::new(parse_numeric_input(id, field, &arr[2])?),
1925                )),
1926                "%" if arr.len() == 3 => Ok(NumericExpression::Mod(
1927                    Box::new(parse_numeric_input(id, field, &arr[1])?),
1928                    Box::new(parse_numeric_input(id, field, &arr[2])?),
1929                )),
1930                "^" if arr.len() == 3 => Ok(NumericExpression::Pow(
1931                    Box::new(parse_numeric_input(id, field, &arr[1])?),
1932                    Box::new(parse_numeric_input(id, field, &arr[2])?),
1933                )),
1934                "abs" if arr.len() == 2 => Ok(NumericExpression::Abs(Box::new(
1935                    parse_numeric_input(id, field, &arr[1])?,
1936                ))),
1937                "ln" if arr.len() == 2 => Ok(NumericExpression::Ln(Box::new(parse_numeric_input(
1938                    id, field, &arr[1],
1939                )?))),
1940                "sqrt" if arr.len() == 2 => Ok(NumericExpression::Sqrt(Box::new(
1941                    parse_numeric_input(id, field, &arr[1])?,
1942                ))),
1943                "min" if arr.len() == 3 => Ok(NumericExpression::Min(
1944                    Box::new(parse_numeric_input(id, field, &arr[1])?),
1945                    Box::new(parse_numeric_input(id, field, &arr[2])?),
1946                )),
1947                "max" if arr.len() == 3 => Ok(NumericExpression::Max(
1948                    Box::new(parse_numeric_input(id, field, &arr[1])?),
1949                    Box::new(parse_numeric_input(id, field, &arr[2])?),
1950                )),
1951                _ => Err(expr_err(
1952                    id,
1953                    field,
1954                    &format!("numeric input (unsupported `{op}`)"),
1955                )),
1956            }
1957        }
1958        _ => Err(expr_err(id, field, "numeric input")),
1959    }
1960}
1961
1962/// Parse a string sub-expression input (used as input to match).
1963fn parse_string_input(
1964    id: &str,
1965    field: &'static str,
1966    value: &Value,
1967) -> Result<StringExpression, StyleSpecError> {
1968    match value {
1969        Value::String(s) => Ok(StringExpression::Literal(s.clone())),
1970        Value::Array(arr) if !arr.is_empty() => {
1971            let op = arr[0].as_str().unwrap_or("");
1972            match op {
1973                "get" if arr.len() == 2 => {
1974                    let key = arr[1]
1975                        .as_str()
1976                        .ok_or_else(|| expr_err(id, field, "get key"))?;
1977                    Ok(StringExpression::GetProperty {
1978                        key: key.to_owned(),
1979                        fallback: String::new(),
1980                    })
1981                }
1982                "feature-state" if arr.len() == 2 => {
1983                    let key = arr[1]
1984                        .as_str()
1985                        .ok_or_else(|| expr_err(id, field, "feature-state key"))?;
1986                    Ok(StringExpression::GetState {
1987                        key: key.to_owned(),
1988                        fallback: String::new(),
1989                    })
1990                }
1991                "upcase" if arr.len() == 2 => Ok(StringExpression::Upcase(Box::new(
1992                    parse_string_input(id, field, &arr[1])?,
1993                ))),
1994                "downcase" if arr.len() == 2 => Ok(StringExpression::Downcase(Box::new(
1995                    parse_string_input(id, field, &arr[1])?,
1996                ))),
1997                "concat" if arr.len() >= 3 => {
1998                    let left = parse_string_input(id, field, &arr[1])?;
1999                    let right = parse_string_input(id, field, &arr[2])?;
2000                    Ok(StringExpression::Concat(Box::new(left), Box::new(right)))
2001                }
2002                _ => Err(expr_err(
2003                    id,
2004                    field,
2005                    &format!("string input (unsupported `{op}`)"),
2006                )),
2007            }
2008        }
2009        _ => Err(expr_err(id, field, "string input")),
2010    }
2011}
2012
2013/// Parse a boolean condition expression.
2014fn parse_bool_condition(
2015    id: &str,
2016    field: &'static str,
2017    value: &Value,
2018) -> Result<BoolExpression, StyleSpecError> {
2019    match value {
2020        Value::Bool(b) => Ok(BoolExpression::Literal(*b)),
2021        Value::Array(arr) if !arr.is_empty() => {
2022            let op = arr[0].as_str().unwrap_or("");
2023            match op {
2024                "has" if arr.len() == 2 => {
2025                    let key = arr[1]
2026                        .as_str()
2027                        .ok_or_else(|| expr_err(id, field, "has key"))?;
2028                    Ok(BoolExpression::Has(key.to_owned()))
2029                }
2030                "get" if arr.len() == 2 => {
2031                    let key = arr[1]
2032                        .as_str()
2033                        .ok_or_else(|| expr_err(id, field, "get key"))?;
2034                    Ok(BoolExpression::GetProperty {
2035                        key: key.to_owned(),
2036                        fallback: false,
2037                    })
2038                }
2039                "!" if arr.len() == 2 => Ok(BoolExpression::Not(Box::new(parse_bool_condition(
2040                    id, field, &arr[1],
2041                )?))),
2042                "all" => {
2043                    let exprs: Result<Vec<_>, _> = arr[1..]
2044                        .iter()
2045                        .map(|v| parse_bool_condition(id, field, v))
2046                        .collect();
2047                    Ok(BoolExpression::All(exprs?))
2048                }
2049                "any" => {
2050                    let exprs: Result<Vec<_>, _> = arr[1..]
2051                        .iter()
2052                        .map(|v| parse_bool_condition(id, field, v))
2053                        .collect();
2054                    Ok(BoolExpression::Any(exprs?))
2055                }
2056                "==" if arr.len() == 3 => {
2057                    // Try numeric comparison first, then string.
2058                    if let (Ok(a), Ok(b)) = (
2059                        parse_numeric_input(id, field, &arr[1]),
2060                        parse_numeric_input(id, field, &arr[2]),
2061                    ) {
2062                        Ok(BoolExpression::Eq(a, b))
2063                    } else if let (Ok(a), Ok(b)) = (
2064                        parse_string_input(id, field, &arr[1]),
2065                        parse_string_input(id, field, &arr[2]),
2066                    ) {
2067                        Ok(BoolExpression::StrEq(a, b))
2068                    } else {
2069                        Err(expr_err(id, field, "== operands"))
2070                    }
2071                }
2072                "!=" if arr.len() == 3 => {
2073                    let a = parse_numeric_input(id, field, &arr[1])?;
2074                    let b = parse_numeric_input(id, field, &arr[2])?;
2075                    Ok(BoolExpression::Neq(a, b))
2076                }
2077                ">" if arr.len() == 3 => {
2078                    let a = parse_numeric_input(id, field, &arr[1])?;
2079                    let b = parse_numeric_input(id, field, &arr[2])?;
2080                    Ok(BoolExpression::Gt(a, b))
2081                }
2082                ">=" if arr.len() == 3 => {
2083                    let a = parse_numeric_input(id, field, &arr[1])?;
2084                    let b = parse_numeric_input(id, field, &arr[2])?;
2085                    Ok(BoolExpression::Gte(a, b))
2086                }
2087                "<" if arr.len() == 3 => {
2088                    let a = parse_numeric_input(id, field, &arr[1])?;
2089                    let b = parse_numeric_input(id, field, &arr[2])?;
2090                    Ok(BoolExpression::Lt(a, b))
2091                }
2092                "<=" if arr.len() == 3 => {
2093                    let a = parse_numeric_input(id, field, &arr[1])?;
2094                    let b = parse_numeric_input(id, field, &arr[2])?;
2095                    Ok(BoolExpression::Lte(a, b))
2096                }
2097                _ => Err(expr_err(
2098                    id,
2099                    field,
2100                    &format!("boolean condition (unsupported `{op}`)"),
2101                )),
2102            }
2103        }
2104        _ => Err(expr_err(id, field, "boolean condition")),
2105    }
2106}
2107
2108// ---------------------------------------------------------------------------
2109// Interpolate / Step / Match / Case parsers (f32)
2110// ---------------------------------------------------------------------------
2111
2112/// Parse `["interpolate", ["linear"], input, z0, v0, z1, v1, ...]` for f32.
2113fn parse_interpolate_f32(
2114    id: &str,
2115    field: &'static str,
2116    arr: &[Value],
2117) -> Result<StyleValue<f32>, StyleSpecError> {
2118    // arr[0] = "interpolate", arr[1] = interpolation type, arr[2] = input, arr[3..] = stop pairs
2119    if arr.len() < 5 || !(arr.len() - 3).is_multiple_of(2) {
2120        return Err(expr_err(id, field, "interpolate with even stop pairs"));
2121    }
2122    let input = parse_numeric_input(id, field, &arr[2])?;
2123    let mut stops = Vec::with_capacity((arr.len() - 3) / 2);
2124    for chunk in arr[3..].chunks(2) {
2125        let z = chunk[0]
2126            .as_f64()
2127            .ok_or_else(|| expr_err(id, field, "interpolate stop zoom"))? as f32;
2128        let v = chunk[1]
2129            .as_f64()
2130            .ok_or_else(|| expr_err(id, field, "interpolate stop value"))? as f32;
2131        stops.push((z, v));
2132    }
2133    Ok(Expression::Interpolate {
2134        input: Box::new(input),
2135        stops,
2136    })
2137}
2138
2139/// Parse `["step", input, default, z0, v0, z1, v1, ...]` for f32.
2140fn parse_step_f32(
2141    id: &str,
2142    field: &'static str,
2143    arr: &[Value],
2144) -> Result<StyleValue<f32>, StyleSpecError> {
2145    if arr.len() < 3 {
2146        return Err(expr_err(id, field, "step expression"));
2147    }
2148    let input = parse_numeric_input(id, field, &arr[1])?;
2149    let default = arr[2]
2150        .as_f64()
2151        .ok_or_else(|| expr_err(id, field, "step default"))? as f32;
2152    let mut stops = Vec::new();
2153    for chunk in arr[3..].chunks(2) {
2154        if chunk.len() == 2 {
2155            let z = chunk[0]
2156                .as_f64()
2157                .ok_or_else(|| expr_err(id, field, "step threshold"))? as f32;
2158            let v = chunk[1]
2159                .as_f64()
2160                .ok_or_else(|| expr_err(id, field, "step value"))? as f32;
2161            stops.push((z, v));
2162        }
2163    }
2164    Ok(Expression::Step {
2165        input: Box::new(input),
2166        default,
2167        stops,
2168    })
2169}
2170
2171/// Parse `["case", cond1, val1, ..., fallback]` for f32.
2172fn parse_case_f32(
2173    id: &str,
2174    field: &'static str,
2175    arr: &[Value],
2176) -> Result<StyleValue<f32>, StyleSpecError> {
2177    // arr = ["case", cond1, val1, ..., condN, valN, fallback]
2178    // After "case": (arr.len()-1) interior elements = pairs + 1 fallback → must be odd, i.e. (arr.len()-1) is odd.
2179    if arr.len() < 4 || (arr.len() - 1).is_multiple_of(2) {
2180        return Err(expr_err(id, field, "case expression"));
2181    }
2182    // Last element is fallback, preceding pairs are (condition, value).
2183    let fallback = arr[arr.len() - 1]
2184        .as_f64()
2185        .ok_or_else(|| expr_err(id, field, "case fallback"))? as f32;
2186    let mut branches = Vec::new();
2187    for chunk in arr[1..arr.len() - 1].chunks(2) {
2188        let cond = parse_bool_condition(id, field, &chunk[0])?;
2189        let val = chunk[1]
2190            .as_f64()
2191            .ok_or_else(|| expr_err(id, field, "case value"))? as f32;
2192        branches.push((cond, val));
2193    }
2194    Ok(Expression::Case { branches, fallback })
2195}
2196
2197// ---------------------------------------------------------------------------
2198// Interpolate / Step / Match / Case parsers (color)
2199// ---------------------------------------------------------------------------
2200
2201/// Parse `["interpolate", ["linear"], input, z0, c0, z1, c1, ...]` for colors.
2202fn parse_interpolate_color(
2203    id: &str,
2204    field: &'static str,
2205    arr: &[Value],
2206) -> Result<StyleValue<[f32; 4]>, StyleSpecError> {
2207    if arr.len() < 5 || !(arr.len() - 3).is_multiple_of(2) {
2208        return Err(expr_err(id, field, "interpolate with even stop pairs"));
2209    }
2210    let input = parse_numeric_input(id, field, &arr[2])?;
2211    let mut stops = Vec::with_capacity((arr.len() - 3) / 2);
2212    for chunk in arr[3..].chunks(2) {
2213        let z = chunk[0]
2214            .as_f64()
2215            .ok_or_else(|| expr_err(id, field, "interpolate stop zoom"))? as f32;
2216        let c = parse_json_color(id, field, &chunk[1])?;
2217        stops.push((z, c));
2218    }
2219    Ok(Expression::Interpolate {
2220        input: Box::new(input),
2221        stops,
2222    })
2223}
2224
2225/// Parse `["step", input, default_color, z0, c0, z1, c1, ...]` for colors.
2226fn parse_step_color(
2227    id: &str,
2228    field: &'static str,
2229    arr: &[Value],
2230) -> Result<StyleValue<[f32; 4]>, StyleSpecError> {
2231    if arr.len() < 3 {
2232        return Err(expr_err(id, field, "step expression"));
2233    }
2234    let input = parse_numeric_input(id, field, &arr[1])?;
2235    let default = parse_json_color(id, field, &arr[2])?;
2236    let mut stops = Vec::new();
2237    for chunk in arr[3..].chunks(2) {
2238        if chunk.len() == 2 {
2239            let z = chunk[0]
2240                .as_f64()
2241                .ok_or_else(|| expr_err(id, field, "step threshold"))? as f32;
2242            let c = parse_json_color(id, field, &chunk[1])?;
2243            stops.push((z, c));
2244        }
2245    }
2246    Ok(Expression::Step {
2247        input: Box::new(input),
2248        default,
2249        stops,
2250    })
2251}
2252
2253/// Parse `["match", input, label1, color1, ..., fallback_color]`.
2254fn parse_match_color(
2255    id: &str,
2256    field: &'static str,
2257    arr: &[Value],
2258) -> Result<StyleValue<[f32; 4]>, StyleSpecError> {
2259    if arr.len() < 4 {
2260        return Err(expr_err(id, field, "match expression"));
2261    }
2262    let input = parse_string_input(id, field, &arr[1])?;
2263    let fallback = parse_json_color(id, field, &arr[arr.len() - 1])?;
2264    let mut cases = Vec::new();
2265    // arr[2..last-1] are label/value pairs.
2266    let pairs = &arr[2..arr.len() - 1];
2267    for chunk in pairs.chunks(2) {
2268        if chunk.len() == 2 {
2269            let label = chunk[0]
2270                .as_str()
2271                .ok_or_else(|| expr_err(id, field, "match label"))?;
2272            let color = parse_json_color(id, field, &chunk[1])?;
2273            cases.push((label.to_owned(), color));
2274        }
2275    }
2276    Ok(Expression::Match {
2277        input: Box::new(input),
2278        cases,
2279        fallback,
2280    })
2281}
2282
2283/// Parse `["case", cond1, color1, ..., fallback_color]`.
2284fn parse_case_color(
2285    id: &str,
2286    field: &'static str,
2287    arr: &[Value],
2288) -> Result<StyleValue<[f32; 4]>, StyleSpecError> {
2289    // arr = ["case", cond1, val1, ..., condN, valN, fallback] → need at least 4 elements
2290    if arr.len() < 4 || (arr.len() - 1).is_multiple_of(2) {
2291        return Err(expr_err(id, field, "case expression"));
2292    }
2293    let fallback = parse_json_color(id, field, &arr[arr.len() - 1])?;
2294    let mut branches = Vec::new();
2295    for chunk in arr[1..arr.len() - 1].chunks(2) {
2296        if chunk.len() == 2 {
2297            let cond = parse_bool_condition(id, field, &chunk[0])?;
2298            let color = parse_json_color(id, field, &chunk[1])?;
2299            branches.push((cond, color));
2300        }
2301    }
2302    Ok(Expression::Case { branches, fallback })
2303}
2304
2305// ---------------------------------------------------------------------------
2306// Helpers
2307// ---------------------------------------------------------------------------
2308
2309/// Parse a color from a JSON value (string or array).
2310fn parse_json_color(
2311    id: &str,
2312    field: &'static str,
2313    value: &Value,
2314) -> Result<[f32; 4], StyleSpecError> {
2315    match value {
2316        Value::String(s) => parse_color_string(id, field, s),
2317        Value::Array(a) => parse_color_array(id, field, a),
2318        _ => Err(expr_err(id, field, "color value")),
2319    }
2320}
2321
2322/// Build a standardised expression parse error.
2323fn expr_err(id: &str, field: &'static str, expected: &str) -> StyleSpecError {
2324    StyleSpecError::InvalidField {
2325        id: id.to_owned(),
2326        field,
2327        expected: Box::leak(format!("expression: {expected}").into_boxed_str()),
2328    }
2329}
2330
2331#[cfg(test)]
2332mod tests {
2333    use super::*;
2334    use crate::geometry::{Feature, FeatureCollection, Geometry, Point};
2335    use crate::models::{ModelInstance, ModelMesh};
2336    use crate::style::{GeoJsonSource, ModelSource, VectorTileSource};
2337    use crate::tile_source::{TileResponse, TileSource};
2338    use rustial_math::{GeoCoord, TileId};
2339
2340    #[derive(Clone, Default)]
2341    struct StubTileSource;
2342
2343    impl TileSource for StubTileSource {
2344        fn request(&self, _id: TileId) {}
2345        fn poll(&self) -> Vec<(TileId, Result<TileResponse, crate::TileError>)> {
2346            Vec::new()
2347        }
2348    }
2349
2350    fn sample_features() -> FeatureCollection {
2351        FeatureCollection {
2352            features: vec![Feature {
2353                geometry: Geometry::Point(Point {
2354                    coord: GeoCoord::from_lat_lon(51.1, 17.0),
2355                }),
2356                properties: HashMap::new(),
2357            }],
2358        }
2359    }
2360
2361    #[test]
2362    fn parses_and_resolves_style_json() {
2363        let json = r##"
2364        {
2365          "version": 8,
2366          "projection": { "type": "equirectangular" },
2367           "terrain": { "source": "dem" },
2368           "sources": {
2369             "base": { "type": "raster" },
2370             "dem": { "type": "raster-dem" },
2371             "places": { "type": "geojson" },
2372             "labels": { "type": "vector" },
2373             "hero": { "type": "model" }
2374           },
2375           "layers": [
2376             { "id": "bg", "type": "background", "paint": { "background-color": "#112233" } },
2377             { "id": "raster", "type": "raster", "source": "base", "paint": { "raster-opacity": 0.75 } },
2378             { "id": "fill", "type": "fill", "source": "places", "paint": { "fill-color": "#ff0000" } },
2379             { "id": "symbol", "type": "symbol", "source": "labels", "layout": { "text-field": "name", "text-size": 18 }, "paint": { "text-color": "#00ff00" } },
2380             { "id": "model", "type": "model", "source": "hero" }
2381           ]
2382        }
2383        "##;
2384
2385        let spec = parse_style_json(json).expect("parse spec");
2386        let mut registry = StyleSourceRegistry::new();
2387        registry.set_source(
2388            "base",
2389            StyleSource::Raster(crate::style::RasterSource::new(|| Box::new(StubTileSource))),
2390        );
2391        registry.set_source(
2392            "dem",
2393            StyleSource::Terrain(crate::style::TerrainSource::new(|| {
2394                Box::new(crate::terrain::FlatElevationSource::new(4, 4))
2395            })),
2396        );
2397        registry.set_source(
2398            "places",
2399            StyleSource::GeoJson(GeoJsonSource::new(sample_features())),
2400        );
2401        registry.set_source(
2402            "labels",
2403            StyleSource::VectorTile(VectorTileSource::new(sample_features())),
2404        );
2405        registry.set_source(
2406            "hero",
2407            StyleSource::Model(ModelSource::new(vec![ModelInstance::new(
2408                GeoCoord::from_lat_lon(0.0, 0.0),
2409                ModelMesh {
2410                    positions: vec![[0.0, 0.0, 0.0]],
2411                    normals: vec![[0.0, 0.0, 1.0]],
2412                    uvs: vec![[0.0, 0.0]],
2413                    indices: vec![0],
2414                },
2415            )])),
2416        );
2417
2418        let document = spec.resolve(&registry).expect("resolve");
2419        assert_eq!(document.layers().len(), 5);
2420        assert_eq!(document.terrain_source(), Some("dem"));
2421        assert_eq!(document.projection(), StyleProjection::Equirectangular);
2422    }
2423
2424    #[test]
2425    fn parses_projection_from_json_without_layers() {
2426        let json = r##"
2427        {
2428          "version": 8,
2429          "projection": { "type": "globe" },
2430           "sources": {},
2431           "layers": []
2432         }
2433         "##;
2434
2435        let spec = parse_style_json(json).expect("parse spec");
2436        assert!(matches!(
2437            spec.projection,
2438            Some(StyleSpecProjection {
2439                projection_type: StyleSpecProjectionType::Globe
2440            })
2441        ));
2442
2443        let registry = StyleSourceRegistry::new();
2444        let document = spec.resolve(&registry).expect("resolve");
2445        assert_eq!(document.projection(), StyleProjection::Globe);
2446    }
2447
2448    #[test]
2449    fn parses_vertical_perspective_projection_from_json() {
2450        let json = r##"
2451        {
2452          "version": 8,
2453          "projection": { "type": "vertical-perspective" },
2454          "sources": {},
2455          "layers": []
2456        }
2457        "##;
2458
2459        let spec = parse_style_json(json).expect("parse spec");
2460        assert!(matches!(
2461            spec.projection,
2462            Some(StyleSpecProjection {
2463                projection_type: StyleSpecProjectionType::VerticalPerspective
2464            })
2465        ));
2466
2467        let registry = StyleSourceRegistry::new();
2468        let document = spec.resolve(&registry).expect("resolve");
2469        assert_eq!(document.projection(), StyleProjection::VerticalPerspective);
2470    }
2471
2472    #[test]
2473    fn parses_zoom_stop_values_from_json() {
2474        let json = r##"
2475        {
2476          "version": 8,
2477          "sources": { "places": { "type": "geojson" } },
2478          "layers": [
2479            {
2480              "id": "circles",
2481              "type": "circle",
2482              "source": "places",
2483              "paint": {
2484                "circle-radius": { "stops": [[5, 4], [10, 12]] },
2485                "circle-color": { "stops": [[5, "#000000"], [10, "#ffffff"]] }
2486              }
2487            }
2488          ]
2489        }
2490        "##;
2491
2492        let spec = parse_style_json(json).expect("parse spec");
2493        let mut registry = StyleSourceRegistry::new();
2494        registry.set_source(
2495            "places",
2496            StyleSource::GeoJson(GeoJsonSource::new(sample_features())),
2497        );
2498        let document = spec.resolve(&registry).expect("resolve");
2499
2500        match &document.layers()[0] {
2501            StyleLayer::Circle(layer) => {
2502                assert!(matches!(layer.radius, StyleValue::ZoomStops(_)));
2503                assert!(matches!(layer.color, StyleValue::ZoomStops(_)));
2504            }
2505            other => panic!("expected circle layer, got {other:?}"),
2506        }
2507    }
2508
2509    #[test]
2510    fn parses_symbol_overlap_placement_spacing_max_angle_keep_upright_writing_mode_and_offset() {
2511        let json = r##"
2512        {
2513          "version": 8,
2514          "sources": { "labels": { "type": "vector" } },
2515          "layers": [
2516            {
2517              "id": "symbol",
2518              "type": "symbol",
2519              "source": "labels",
2520              "layout": {
2521                "symbol-placement": "line",
2522                "text-allow-overlap": true,
2523                "icon-allow-overlap": false,
2524                "text-optional": true,
2525                "icon-optional": false,
2526                "text-ignore-placement": true,
2527                "icon-ignore-placement": false,
2528                "text-radial-offset": 2,
2529                "text-anchor": "top-right",
2530                "text-justify": "auto",
2531                "text-transform": "uppercase",
2532                "text-max-width": 8,
2533                "text-line-height": 1.5,
2534                "text-letter-spacing": 0.25,
2535                "icon-text-fit": "both",
2536                "icon-text-fit-padding": [1, 2, 3, 4],
2537                "text-variable-anchor-offset": ["top", [1, 2], "bottom", [3, 4]],
2538                "symbol-sort-key": 7,
2539                "symbol-spacing": 320,
2540                "text-max-angle": 90,
2541                "text-keep-upright": false,
2542                "text-field": "name",
2543                "text-variable-anchor": ["center", "top"],
2544                "text-writing-mode": "vertical",
2545                "text-offset": [1, 2]
2546              }
2547            }
2548          ]
2549        }
2550        "##;
2551
2552        let spec = parse_style_json(json).expect("parse spec");
2553        let mut registry = StyleSourceRegistry::new();
2554        registry.set_source(
2555            "labels",
2556            StyleSource::VectorTile(VectorTileSource::new(sample_features())),
2557        );
2558        let document = spec.resolve(&registry).expect("resolve");
2559
2560        match &document.layers()[0] {
2561            StyleLayer::Symbol(layer) => {
2562                assert_eq!(
2563                    layer.text_allow_overlap.as_ref().map(StyleValue::evaluate),
2564                    Some(true)
2565                );
2566                assert_eq!(
2567                    layer.icon_allow_overlap.as_ref().map(StyleValue::evaluate),
2568                    Some(false)
2569                );
2570                assert_eq!(
2571                    layer.text_optional.as_ref().map(StyleValue::evaluate),
2572                    Some(true)
2573                );
2574                assert_eq!(
2575                    layer.icon_optional.as_ref().map(StyleValue::evaluate),
2576                    Some(false)
2577                );
2578                assert_eq!(
2579                    layer
2580                        .text_ignore_placement
2581                        .as_ref()
2582                        .map(StyleValue::evaluate),
2583                    Some(true)
2584                );
2585                assert_eq!(
2586                    layer
2587                        .icon_ignore_placement
2588                        .as_ref()
2589                        .map(StyleValue::evaluate),
2590                    Some(false)
2591                );
2592                assert_eq!(
2593                    layer.radial_offset.as_ref().map(StyleValue::evaluate),
2594                    Some(2.0)
2595                );
2596                assert_eq!(layer.anchor, SymbolAnchor::TopRight);
2597                assert_eq!(layer.justify.evaluate(), SymbolTextJustify::Auto);
2598                assert_eq!(layer.transform.evaluate(), SymbolTextTransform::Uppercase);
2599                assert_eq!(
2600                    layer.max_width.as_ref().map(StyleValue::evaluate),
2601                    Some(8.0)
2602                );
2603                assert_eq!(
2604                    layer.line_height.as_ref().map(StyleValue::evaluate),
2605                    Some(1.5)
2606                );
2607                assert_eq!(
2608                    layer.letter_spacing.as_ref().map(StyleValue::evaluate),
2609                    Some(0.25)
2610                );
2611                assert_eq!(layer.icon_text_fit.evaluate(), SymbolIconTextFit::Both);
2612                assert_eq!(layer.icon_text_fit_padding, [1.0, 2.0, 3.0, 4.0]);
2613                assert_eq!(
2614                    layer.variable_anchor_offsets,
2615                    Some(vec![
2616                        (SymbolAnchor::Top, [1.0, 2.0]),
2617                        (SymbolAnchor::Bottom, [3.0, 4.0]),
2618                    ])
2619                );
2620                assert_eq!(layer.sort_key.as_ref().map(StyleValue::evaluate), Some(7.0));
2621                assert_eq!(layer.placement, SymbolPlacement::Line);
2622                assert_eq!(layer.spacing.evaluate(), 320.0);
2623                assert_eq!(layer.max_angle.evaluate(), 90.0);
2624                assert!(!layer.keep_upright.evaluate());
2625                assert_eq!(
2626                    layer.variable_anchors,
2627                    vec![SymbolAnchor::Center, SymbolAnchor::Top]
2628                );
2629                assert_eq!(layer.writing_mode, SymbolWritingMode::Vertical);
2630                assert_eq!(layer.offset, [1.0, 2.0]);
2631            }
2632            other => panic!("expected symbol layer, got {other:?}"),
2633        }
2634    }
2635
2636    #[test]
2637    fn parses_source_layer_for_vector_style_layers() {
2638        let json = r##"
2639        {
2640          "version": 8,
2641          "sources": { "labels": { "type": "vector" } },
2642          "layers": [
2643            {
2644              "id": "roads",
2645              "type": "line",
2646              "source": "labels",
2647              "source-layer": "transport"
2648            }
2649          ]
2650        }
2651        "##;
2652
2653        let spec = parse_style_json(json).expect("parse spec");
2654        assert_eq!(spec.layers.len(), 1);
2655        assert_eq!(spec.layers[0].source_layer.as_deref(), Some("transport"));
2656
2657        let mut registry = StyleSourceRegistry::new();
2658        let mut source_layers = HashMap::new();
2659        source_layers.insert(
2660            "transport".to_string(),
2661            FeatureCollection {
2662                features: vec![Feature {
2663                    geometry: Geometry::Point(Point {
2664                        coord: GeoCoord::from_lat_lon(1.0, 2.0),
2665                    }),
2666                    properties: HashMap::new(),
2667                }],
2668            },
2669        );
2670        registry.set_source(
2671            "labels",
2672            StyleSource::VectorTile(VectorTileSource::from_source_layers(source_layers)),
2673        );
2674
2675        let document = spec.resolve(&registry).expect("resolve");
2676        match &document.layers()[0] {
2677            StyleLayer::Line(layer) => {
2678                assert_eq!(layer.source_layer.as_deref(), Some("transport"));
2679            }
2680            other => panic!("expected line layer, got {other:?}"),
2681        }
2682    }
2683
2684    // -----------------------------------------------------------------------
2685    // Expression JSON parsing tests
2686    // -----------------------------------------------------------------------
2687
2688    #[test]
2689    fn f32_expression_interpolate_on_zoom() {
2690        let json = r##"
2691        {
2692          "version": 8,
2693          "sources": { "s": { "type": "vector" } },
2694          "layers": [
2695            {
2696              "id": "l",
2697              "type": "line",
2698              "source": "s",
2699              "paint": {
2700                "line-width": ["interpolate", ["linear"], ["zoom"], 0, 1, 20, 10]
2701              }
2702            }
2703          ]
2704        }
2705        "##;
2706        let spec = parse_style_json(json).expect("parse");
2707        let mut registry = StyleSourceRegistry::new();
2708        registry.set_source(
2709            "s",
2710            StyleSource::VectorTile(VectorTileSource::new(sample_features())),
2711        );
2712        let doc = spec.resolve(&registry).expect("resolve");
2713        match &doc.layers()[0] {
2714            StyleLayer::Line(layer) => {
2715                let ctx = crate::style::StyleEvalContext::new(10.0);
2716                let width = layer.width.evaluate_with_context(ctx);
2717                assert!((width - 5.5).abs() < 0.1, "expected ~5.5, got {width}");
2718            }
2719            other => panic!("expected line, got {other:?}"),
2720        }
2721    }
2722
2723    #[test]
2724    fn f32_expression_step_on_zoom() {
2725        let json = r##"
2726        {
2727          "version": 8,
2728          "sources": { "s": { "type": "vector" } },
2729          "layers": [
2730            {
2731              "id": "l",
2732              "type": "circle",
2733              "source": "s",
2734              "paint": {
2735                "circle-radius": ["step", ["zoom"], 2, 5, 4, 10, 8]
2736              }
2737            }
2738          ]
2739        }
2740        "##;
2741        let spec = parse_style_json(json).expect("parse");
2742        let mut registry = StyleSourceRegistry::new();
2743        registry.set_source(
2744            "s",
2745            StyleSource::VectorTile(VectorTileSource::new(sample_features())),
2746        );
2747        let doc = spec.resolve(&registry).expect("resolve");
2748        match &doc.layers()[0] {
2749            StyleLayer::Circle(layer) => {
2750                use crate::expression::ExprEvalContext;
2751                // Below first stop
2752                assert!(
2753                    (layer.radius.eval_full(&ExprEvalContext::zoom_only(3.0)) - 2.0).abs()
2754                        < f32::EPSILON
2755                );
2756                // At first stop
2757                assert!(
2758                    (layer.radius.eval_full(&ExprEvalContext::zoom_only(5.0)) - 4.0).abs()
2759                        < f32::EPSILON
2760                );
2761                // Between stops
2762                assert!(
2763                    (layer.radius.eval_full(&ExprEvalContext::zoom_only(7.0)) - 4.0).abs()
2764                        < f32::EPSILON
2765                );
2766                // At second stop
2767                assert!(
2768                    (layer.radius.eval_full(&ExprEvalContext::zoom_only(10.0)) - 8.0).abs()
2769                        < f32::EPSILON
2770                );
2771                // Above all stops
2772                assert!(
2773                    (layer.radius.eval_full(&ExprEvalContext::zoom_only(15.0)) - 8.0).abs()
2774                        < f32::EPSILON
2775                );
2776            }
2777            other => panic!("expected circle, got {other:?}"),
2778        }
2779    }
2780
2781    #[test]
2782    fn color_expression_match_on_property() {
2783        let json = r##"
2784        {
2785          "version": 8,
2786          "sources": { "s": { "type": "vector" } },
2787          "layers": [
2788            {
2789              "id": "l",
2790              "type": "fill",
2791              "source": "s",
2792              "paint": {
2793                "fill-color": ["match", ["get", "type"], "residential", "#0000ff", "commercial", "#ff0000", "#888888"]
2794              }
2795            }
2796          ]
2797        }
2798        "##;
2799        let spec = parse_style_json(json).expect("parse");
2800        let mut registry = StyleSourceRegistry::new();
2801        registry.set_source(
2802            "s",
2803            StyleSource::VectorTile(VectorTileSource::new(sample_features())),
2804        );
2805        let doc = spec.resolve(&registry).expect("resolve");
2806        match &doc.layers()[0] {
2807            StyleLayer::Fill(layer) => {
2808                use crate::expression::ExprEvalContext;
2809                use crate::geometry::PropertyValue;
2810
2811                let mut props = std::collections::HashMap::new();
2812                props.insert(
2813                    "type".to_string(),
2814                    PropertyValue::String("residential".to_string()),
2815                );
2816                let ctx = ExprEvalContext::with_feature(10.0, &props);
2817                let color = layer.fill_color.eval_full(&ctx);
2818                assert!((color[0] - 0.0).abs() < 0.01);
2819                assert!((color[2] - 1.0).abs() < 0.01);
2820
2821                props.insert(
2822                    "type".to_string(),
2823                    PropertyValue::String("commercial".to_string()),
2824                );
2825                let ctx = ExprEvalContext::with_feature(10.0, &props);
2826                let color = layer.fill_color.eval_full(&ctx);
2827                assert!((color[0] - 1.0).abs() < 0.01);
2828                assert!((color[2] - 0.0).abs() < 0.01);
2829
2830                // fallback
2831                props.insert(
2832                    "type".to_string(),
2833                    PropertyValue::String("industrial".to_string()),
2834                );
2835                let ctx = ExprEvalContext::with_feature(10.0, &props);
2836                let color = layer.fill_color.eval_full(&ctx);
2837                assert!((color[0] - 0.533).abs() < 0.01);
2838            }
2839            other => panic!("expected fill, got {other:?}"),
2840        }
2841    }
2842
2843    #[test]
2844    fn f32_expression_get_property() {
2845        let json = r##"
2846        {
2847          "version": 8,
2848          "sources": { "s": { "type": "vector" } },
2849          "layers": [
2850            {
2851              "id": "l",
2852              "type": "fill-extrusion",
2853              "source": "s",
2854              "paint": {
2855                "fill-extrusion-height": ["get", "height"],
2856                "fill-extrusion-base": ["get", "base"]
2857              }
2858            }
2859          ]
2860        }
2861        "##;
2862        let spec = parse_style_json(json).expect("parse");
2863        let mut registry = StyleSourceRegistry::new();
2864        registry.set_source(
2865            "s",
2866            StyleSource::VectorTile(VectorTileSource::new(sample_features())),
2867        );
2868        let doc = spec.resolve(&registry).expect("resolve");
2869        match &doc.layers()[0] {
2870            StyleLayer::FillExtrusion(layer) => {
2871                use crate::expression::ExprEvalContext;
2872                use crate::geometry::PropertyValue;
2873                let mut props = std::collections::HashMap::new();
2874                props.insert("height".to_string(), PropertyValue::Number(50.0));
2875                props.insert("base".to_string(), PropertyValue::Number(10.0));
2876                let ctx = ExprEvalContext::with_feature(14.0, &props);
2877                assert!((layer.height.eval_full(&ctx) - 50.0).abs() < f32::EPSILON);
2878                assert!((layer.base.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
2879            }
2880            other => panic!("expected fill-extrusion, got {other:?}"),
2881        }
2882    }
2883
2884    #[test]
2885    fn f32_expression_case_with_comparison() {
2886        let json = r##"
2887        {
2888          "version": 8,
2889          "sources": { "s": { "type": "vector" } },
2890          "layers": [
2891            {
2892              "id": "l",
2893              "type": "line",
2894              "source": "s",
2895              "paint": {
2896                "line-width": ["case", [">", ["get", "lanes"], 2], 8, 2]
2897              }
2898            }
2899          ]
2900        }
2901        "##;
2902        let spec = parse_style_json(json).expect("parse");
2903        let mut registry = StyleSourceRegistry::new();
2904        registry.set_source(
2905            "s",
2906            StyleSource::VectorTile(VectorTileSource::new(sample_features())),
2907        );
2908        let doc = spec.resolve(&registry).expect("resolve");
2909        match &doc.layers()[0] {
2910            StyleLayer::Line(layer) => {
2911                use crate::expression::ExprEvalContext;
2912                use crate::geometry::PropertyValue;
2913
2914                // lanes = 4 > 2, so width = 8
2915                let mut props = std::collections::HashMap::new();
2916                props.insert("lanes".to_string(), PropertyValue::Number(4.0));
2917                let ctx = ExprEvalContext::with_feature(14.0, &props);
2918                assert!((layer.width.eval_full(&ctx) - 8.0).abs() < f32::EPSILON);
2919
2920                // lanes = 1, not > 2, so width = 2 (fallback)
2921                props.insert("lanes".to_string(), PropertyValue::Number(1.0));
2922                let ctx = ExprEvalContext::with_feature(14.0, &props);
2923                assert!((layer.width.eval_full(&ctx) - 2.0).abs() < f32::EPSILON);
2924            }
2925            other => panic!("expected line, got {other:?}"),
2926        }
2927    }
2928}