Skip to main content

rustial_engine/
style_json.rs

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