Skip to main content

rustial_engine/
style.rs

1//! Style/runtime model for style-document-driven map construction.
2//!
3//! This module provides a small engine-owned style runtime inspired by
4//! MapLibre's style system. It covers Rustial's current source families and
5//! a broader style-layer taxonomy, while lowering geometry-centric layers onto
6//! the existing engine primitives (`BackgroundLayer`, `HillshadeLayer`,
7//! `TileLayer`, `VectorLayer`, and `ModelLayer`).
8
9use crate::camera_projection::CameraProjection;
10use crate::cluster::{ClusterOptions, PointCluster};
11use crate::geometry::{FeatureCollection, PropertyValue};
12use crate::layer::Layer;
13use crate::layers::{
14    BackgroundLayer, DynamicImageOverlayLayer, FrameProviderFactory, HillshadeLayer, LineCap,
15    LineJoin, ModelLayer, TileLayer, VectorLayer, VectorRenderMode, VectorStyle,
16};
17use crate::models::ModelInstance;
18use crate::query::FeatureState;
19use crate::symbols::{
20    SymbolAnchor, SymbolIconTextFit, SymbolPlacement, SymbolTextJustify, SymbolTextTransform,
21    SymbolWritingMode,
22};
23use crate::terrain::{ElevationSource, TerrainConfig};
24use crate::tile_manager::TileSelectionConfig;
25use crate::tile_source::TileSource;
26use rustial_math::GeoCoord;
27use std::borrow::Cow;
28use std::collections::HashMap;
29use std::fmt;
30use std::sync::Arc;
31
32/// Style/runtime errors.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum StyleError {
35    /// Attempted to add a source whose id already exists.
36    DuplicateSourceId(String),
37    /// Attempted to add a layer whose id already exists.
38    DuplicateLayerId(String),
39    /// A style layer referenced a source that does not exist.
40    MissingSource(String),
41    /// A style layer referenced a source of the wrong kind.
42    SourceKindMismatch {
43        /// Layer id being evaluated.
44        layer_id: String,
45        /// Source id being referenced.
46        source_id: String,
47        /// Expected source kind name.
48        expected: &'static str,
49        /// Actual source kind name.
50        actual: &'static str,
51    },
52    /// A vector style layer referenced a source layer that does not exist.
53    MissingSourceLayer {
54        /// Layer id being evaluated.
55        layer_id: String,
56        /// Source id being referenced.
57        source_id: String,
58        /// Requested source-layer name.
59        source_layer: String,
60    },
61}
62
63impl fmt::Display for StyleError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            StyleError::DuplicateSourceId(id) => write!(f, "duplicate style source id `{id}`"),
67            StyleError::DuplicateLayerId(id) => write!(f, "duplicate style layer id `{id}`"),
68            StyleError::MissingSource(id) => write!(f, "missing style source `{id}`"),
69            StyleError::SourceKindMismatch {
70                layer_id,
71                source_id,
72                expected,
73                actual,
74            } => write!(
75                f,
76                "style layer `{layer_id}` expected source `{source_id}` of kind `{expected}`, got `{actual}`"
77            ),
78            StyleError::MissingSourceLayer {
79                layer_id,
80                source_id,
81                source_layer,
82            } => write!(
83                f,
84                "style layer `{layer_id}` referenced missing source-layer `{source_layer}` on source `{source_id}`"
85            ),
86        }
87    }
88}
89
90impl std::error::Error for StyleError {}
91
92/// A style source identifier.
93pub type StyleSourceId = String;
94
95/// A style layer identifier.
96pub type StyleLayerId = String;
97
98/// Runtime evaluation context for paint/layout values.
99///
100/// This lightweight struct carries zoom-only state and is `Copy` so it can be
101/// passed cheaply through the style-evaluation pipeline. For feature-state-aware
102/// evaluation, use [`StyleEvalContextFull`] which borrows a [`FeatureState`]
103/// map alongside the zoom level.
104#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct StyleEvalContext {
106    /// Current map zoom level.
107    pub zoom: f32,
108}
109
110impl StyleEvalContext {
111    /// Create a new evaluation context.
112    pub fn new(zoom: f32) -> Self {
113        Self { zoom }
114    }
115
116    /// Promote to a full evaluation context that includes per-feature state.
117    ///
118    /// This is the bridge between the fast zoom-only path and the richer
119    /// feature-state-aware path used during hover/selection restyling.
120    pub fn with_feature_state(self, feature_state: &FeatureState) -> StyleEvalContextFull<'_> {
121        StyleEvalContextFull {
122            zoom: self.zoom,
123            feature_state,
124        }
125    }
126}
127
128impl Default for StyleEvalContext {
129    fn default() -> Self {
130        Self { zoom: 0.0 }
131    }
132}
133
134/// Extended evaluation context that carries per-feature mutable state alongside
135/// the current zoom level.
136///
137/// This is used when paint/layout values may depend on feature-state keys such
138/// as `"hover"` or `"selected"`. The lifetime `'a` borrows the feature-state
139/// map owned by [`MapState`](crate::MapState).
140///
141/// # Example
142///
143/// ```ignore
144/// let ctx = StyleEvalContext::new(14.0)
145///     .with_feature_state(&feature_state);
146/// let color = fill_color.evaluate_with_full_context(&ctx);
147/// ```
148#[derive(Debug, Clone, Copy, PartialEq)]
149pub struct StyleEvalContextFull<'a> {
150    /// Current map zoom level.
151    pub zoom: f32,
152    /// Per-feature mutable state (e.g. `{"hover": true, "selected": false}`).
153    pub feature_state: &'a FeatureState,
154}
155
156impl<'a> StyleEvalContextFull<'a> {
157    /// Create a full evaluation context.
158    pub fn new(zoom: f32, feature_state: &'a FeatureState) -> Self {
159        Self { zoom, feature_state }
160    }
161
162    /// Downgrade to a zoom-only context (discards feature-state).
163    pub fn to_base(&self) -> StyleEvalContext {
164        StyleEvalContext { zoom: self.zoom }
165    }
166
167    /// Look up a feature-state key, returning `None` if the key is absent.
168    pub fn get_feature_state(&self, key: &str) -> Option<&PropertyValue> {
169        self.feature_state.get(key)
170    }
171
172    /// Look up a feature-state key as a boolean, defaulting to `false`.
173    pub fn feature_state_bool(&self, key: &str) -> bool {
174        self.feature_state
175            .get(key)
176            .and_then(|v| v.as_bool())
177            .unwrap_or(false)
178    }
179
180    /// Look up a feature-state key as an f64, returning a default when absent.
181    pub fn feature_state_f64(&self, key: &str, default: f64) -> f64 {
182        self.feature_state
183            .get(key)
184            .and_then(|v| v.as_f64())
185            .unwrap_or(default)
186    }
187}
188
189/// Top-level style-owned projection selection.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
191pub enum StyleProjection {
192    /// Default Web Mercator map projection.
193    #[default]
194    Mercator,
195    /// Equirectangular / Plate Carree planar projection.
196    Equirectangular,
197    /// Globe / geocentric projection.
198    Globe,
199    /// Near-sided vertical perspective projection.
200    VerticalPerspective,
201}
202
203impl StyleProjection {
204    /// Convert the style-owned projection to the camera-facing projection.
205    pub fn to_camera_projection(self) -> CameraProjection {
206        match self {
207            StyleProjection::Mercator => CameraProjection::WebMercator,
208            StyleProjection::Equirectangular => CameraProjection::Equirectangular,
209            StyleProjection::Globe => CameraProjection::Globe,
210            StyleProjection::VerticalPerspective => {
211                CameraProjection::vertical_perspective(GeoCoord::default(), 10_000_000.0)
212            }
213        }
214    }
215}
216
217// ---------------------------------------------------------------------------
218// Fog / atmosphere configuration
219// ---------------------------------------------------------------------------
220
221/// User-facing fog/atmosphere configuration.
222///
223/// Mirrors MapLibre's `sky` / Mapbox's `fog` style properties.
224/// When attached to a [`StyleDocument`] or set directly on [`MapState`],
225/// these values override the default pitch-based atmospheric fog.
226///
227/// Omitted fields (`None`) fall back to the automatic camera-derived
228/// defaults so users can override only the aspects they care about.
229#[derive(Debug, Clone, PartialEq)]
230pub struct FogConfig {
231    /// Fog tint colour (RGBA, linear).
232    ///
233    /// When `None`, derived automatically from the background colour and
234    /// camera pitch (the existing atmospheric-clear-colour behaviour).
235    pub color: Option<[f32; 4]>,
236
237    /// Fog range as `[start, end]` — fractions of the camera visible range.
238    ///
239    /// `0.0` = at the camera eye, `1.0` = at the computed visible-range
240    /// horizon.  Default: `[0.55, 1.05]`.
241    pub range: Option<[f32; 2]>,
242
243    /// Peak fog density `[0.0, 1.0]`.
244    ///
245    /// When `None`, the density is computed from camera pitch (0 at
246    /// top-down, ramping to 0.9 near the horizon).
247    pub density: Option<f32>,
248
249    /// Horizon / sky colour (RGBA, linear).
250    ///
251    /// Used as the clear-colour background when the camera is pitched
252    /// toward the horizon.  When `None`, derived from the base
253    /// background colour.
254    pub horizon_color: Option<[f32; 4]>,
255
256    /// Horizon blend factor `[0.0, 1.0]`.
257    ///
258    /// Controls how strongly the horizon colour mixes into the
259    /// background as pitch increases.  When `None`, automatic.
260    pub horizon_blend: Option<f32>,
261}
262
263impl Default for FogConfig {
264    fn default() -> Self {
265        Self {
266            color: None,
267            range: None,
268            density: None,
269            horizon_color: None,
270            horizon_blend: None,
271        }
272    }
273}
274
275/// Pre-computed fog parameters ready for GPU uniform upload.
276///
277/// Computed by the engine each frame from camera state, the optional
278/// [`FogConfig`], and the background colour.  Both WGPU and Bevy
279/// renderers consume this struct directly instead of duplicating the
280/// fog math.
281#[derive(Debug, Clone, Copy, PartialEq)]
282pub struct ComputedFog {
283    /// Fog / horizon tint colour (RGBA, linear).
284    pub fog_color: [f32; 4],
285    /// Distance from the camera eye at which fog begins (meters).
286    pub fog_start: f32,
287    /// Distance from the camera eye at which fog reaches full density (meters).
288    pub fog_end: f32,
289    /// Peak fog density `[0.0, 1.0]`.
290    pub fog_density: f32,
291    /// Background clear colour after atmospheric tinting.
292    pub clear_color: [f32; 4],
293}
294
295impl Default for ComputedFog {
296    fn default() -> Self {
297        Self {
298            fog_color: [1.0; 4],
299            fog_start: 10_000.0,
300            fog_end: 20_000.0,
301            fog_density: 0.0,
302            clear_color: [1.0; 4],
303        }
304    }
305}
306
307/// Blend `base` toward a slightly lifted "horizon" colour as `pitch`
308/// increases past 0.25 rad.
309///
310/// This is the canonical implementation used by both WGPU and Bevy
311/// renderers.
312pub fn atmospheric_clear_color(base: [f32; 4], pitch: f64) -> [f32; 4] {
313    let t = (((pitch - 0.25) / 1.0).clamp(0.0, 1.0)) as f32;
314    let horizon = [
315        (base[0] * 0.92 + 0.05).clamp(0.0, 1.0),
316        (base[1] * 0.95 + 0.06).clamp(0.0, 1.0),
317        (base[2] * 0.98 + 0.08).clamp(0.0, 1.0),
318        base[3],
319    ];
320    [
321        base[0] * (1.0 - t) + horizon[0] * t,
322        base[1] * (1.0 - t) + horizon[1] * t,
323        base[2] * (1.0 - t) + horizon[2] * t,
324        base[3],
325    ]
326}
327
328/// Compute fog parameters from camera state and optional user config.
329///
330/// This centralises the fog math that was previously duplicated in the
331/// WGPU and Bevy renderers.
332pub fn compute_fog(
333    pitch: f64,
334    camera_distance: f64,
335    background_color: [f32; 4],
336    config: Option<&FogConfig>,
337) -> ComputedFog {
338    let auto_clear = atmospheric_clear_color(background_color, pitch);
339
340    // Fog density: ramps from 0 (top-down) to 0.9 near the horizon.
341    let auto_density = (((pitch - 0.70) / 0.55).clamp(0.0, 1.0) as f32) * 0.9;
342
343    // Visible ground range when pitched.
344    let visible_range = camera_distance / pitch.cos().max(0.05);
345    let auto_start = (visible_range * 0.55) as f32;
346    let auto_end = (visible_range * 1.05) as f32;
347
348    let (fog_start, fog_end) = match config.and_then(|c| c.range) {
349        Some([s, e]) => ((visible_range as f32) * s, (visible_range as f32) * e),
350        None => (auto_start, auto_end),
351    };
352
353    let fog_density = config
354        .and_then(|c| c.density)
355        .unwrap_or(auto_density);
356
357    let fog_color = config
358        .and_then(|c| c.color)
359        .unwrap_or(auto_clear);
360
361    let clear_color = match config.and_then(|c| c.horizon_color) {
362        Some(horizon) => {
363            let blend = config.and_then(|c| c.horizon_blend).unwrap_or_else(|| {
364                ((pitch - 0.25) / 1.0).clamp(0.0, 1.0) as f32
365            });
366            [
367                background_color[0] * (1.0 - blend) + horizon[0] * blend,
368                background_color[1] * (1.0 - blend) + horizon[1] * blend,
369                background_color[2] * (1.0 - blend) + horizon[2] * blend,
370                background_color[3],
371            ]
372        }
373        None => auto_clear,
374    };
375
376    ComputedFog {
377        fog_color,
378        fog_start,
379        fog_end,
380        fog_density,
381        clear_color,
382    }
383}
384
385/// Interpolation behaviour for style values.
386///
387/// Implementors must also provide [`FromFeatureStateProperty`] so that
388/// feature-state-driven [`StyleValue::FeatureState`] variants can attempt
389/// conversion from [`PropertyValue`] during evaluation.
390pub trait StyleInterpolatable: Clone + FromFeatureStateProperty {
391    /// Sample between two stop values.
392    fn interpolate(a: &Self, b: &Self, t: f32) -> Self;
393}
394
395impl StyleInterpolatable for f32 {
396    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
397        *a + (*b - *a) * t.clamp(0.0, 1.0)
398    }
399}
400
401impl StyleInterpolatable for [f32; 4] {
402    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
403        let t = t.clamp(0.0, 1.0);
404        [
405            a[0] + (b[0] - a[0]) * t,
406            a[1] + (b[1] - a[1]) * t,
407            a[2] + (b[2] - a[2]) * t,
408            a[3] + (b[3] - a[3]) * t,
409        ]
410    }
411}
412
413impl StyleInterpolatable for bool {
414    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
415        if t < 1.0 { *a } else { *b }
416    }
417}
418
419impl StyleInterpolatable for String {
420    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
421        if t < 1.0 { a.clone() } else { b.clone() }
422    }
423}
424
425impl StyleInterpolatable for SymbolTextJustify {
426    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
427        if t < 1.0 { *a } else { *b }
428    }
429}
430
431impl StyleInterpolatable for SymbolTextTransform {
432    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
433        if t < 1.0 { *a } else { *b }
434    }
435}
436
437impl StyleInterpolatable for SymbolIconTextFit {
438    fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
439        if t < 1.0 { *a } else { *b }
440    }
441}
442
443/// Factory for constructing a fresh raster tile source when a style document is applied.
444pub type RasterSourceFactory = Arc<dyn Fn() -> Box<dyn TileSource> + Send + Sync>;
445
446/// Factory for constructing a fresh streamed vector tile source when a style document is applied.
447pub type VectorTileSourceFactory = Arc<dyn Fn() -> Box<dyn TileSource> + Send + Sync>;
448
449/// Factory for constructing a fresh terrain elevation source when a style document is applied.
450pub type TerrainSourceFactory = Arc<dyn Fn() -> Box<dyn ElevationSource> + Send + Sync>;
451
452/// Enumerates style/runtime source families.
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
454pub enum StyleSourceKind {
455    /// Raster tile source.
456    Raster,
457    /// Terrain / raster-dem source.
458    Terrain,
459    /// In-memory GeoJSON-like feature source.
460    GeoJson,
461    /// Vector-tile-like feature source resolved into vector features by the host.
462    VectorTile,
463    /// Image source lowered onto raster rendering.
464    Image,
465    /// Video source — georeferenced dynamic overlay.
466    Video,
467    /// Canvas source — georeferenced dynamic overlay.
468    Canvas,
469    /// Model instance source.
470    Model,
471}
472
473impl StyleSourceKind {
474    /// Stable string name for diagnostics and JSON interop.
475    pub fn as_str(self) -> &'static str {
476        match self {
477            StyleSourceKind::Raster => "raster",
478            StyleSourceKind::Terrain => "terrain",
479            StyleSourceKind::GeoJson => "geojson",
480            StyleSourceKind::VectorTile => "vector",
481            StyleSourceKind::Image => "image",
482            StyleSourceKind::Video => "video",
483            StyleSourceKind::Canvas => "canvas",
484            StyleSourceKind::Model => "model",
485        }
486    }
487}
488
489/// Source registry entry.
490#[derive(Clone)]
491pub enum StyleSource {
492    /// Raster tile source.
493    Raster(RasterSource),
494    /// Terrain/elevation source.
495    Terrain(TerrainSource),
496    /// In-memory vector feature source.
497    GeoJson(GeoJsonSource),
498    /// Vector-tile-like source represented as resolved features.
499    VectorTile(VectorTileSource),
500    /// Image source lowered onto raster rendering.
501    Image(ImageSource),
502    /// Video source — georeferenced dynamic overlay.
503    Video(VideoSource),
504    /// Canvas source — georeferenced dynamic overlay.
505    Canvas(CanvasSource),
506    /// In-memory model instance source.
507    Model(ModelSource),
508}
509
510impl StyleSource {
511    /// Human-readable source kind.
512    pub fn kind_name(&self) -> &'static str {
513        self.kind().as_str()
514    }
515
516    /// Enumerated source kind.
517    pub fn kind(&self) -> StyleSourceKind {
518        match self {
519            StyleSource::Raster(_) => StyleSourceKind::Raster,
520            StyleSource::Terrain(_) => StyleSourceKind::Terrain,
521            StyleSource::GeoJson(_) => StyleSourceKind::GeoJson,
522            StyleSource::VectorTile(_) => StyleSourceKind::VectorTile,
523            StyleSource::Image(_) => StyleSourceKind::Image,
524            StyleSource::Video(_) => StyleSourceKind::Video,
525            StyleSource::Canvas(_) => StyleSourceKind::Canvas,
526            StyleSource::Model(_) => StyleSourceKind::Model,
527        }
528    }
529}
530
531impl fmt::Debug for StyleSource {
532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533        match self {
534            StyleSource::Raster(src) => f.debug_tuple("Raster").field(src).finish(),
535            StyleSource::Terrain(src) => f.debug_tuple("Terrain").field(src).finish(),
536            StyleSource::GeoJson(src) => f.debug_tuple("GeoJson").field(src).finish(),
537            StyleSource::VectorTile(src) => f.debug_tuple("VectorTile").field(src).finish(),
538            StyleSource::Image(src) => f.debug_tuple("Image").field(src).finish(),
539            StyleSource::Video(src) => f.debug_tuple("Video").field(src).finish(),
540            StyleSource::Canvas(src) => f.debug_tuple("Canvas").field(src).finish(),
541            StyleSource::Model(src) => f.debug_tuple("Model").field(src).finish(),
542        }
543    }
544}
545
546/// Raster tile source entry.
547#[derive(Clone)]
548pub struct RasterSource {
549    /// Maximum tile-cache capacity used by the created tile layer.
550    pub cache_capacity: usize,
551    /// Tile-selection policy for the created tile layer.
552    pub selection: TileSelectionConfig,
553    /// Factory used to build a fresh tile source each time the style is applied.
554    pub factory: RasterSourceFactory,
555}
556
557impl RasterSource {
558    /// Create a raster source from a tile-source factory.
559    pub fn new(factory: impl Fn() -> Box<dyn TileSource> + Send + Sync + 'static) -> Self {
560        Self {
561            cache_capacity: 256,
562            selection: TileSelectionConfig::default(),
563            factory: Arc::new(factory),
564        }
565    }
566
567    /// Set tile cache capacity.
568    pub fn with_cache_capacity(mut self, cache_capacity: usize) -> Self {
569        self.cache_capacity = cache_capacity;
570        self
571    }
572
573    /// Set tile-selection policy.
574    pub fn with_selection(mut self, selection: TileSelectionConfig) -> Self {
575        self.selection = selection;
576        self
577    }
578}
579
580impl fmt::Debug for RasterSource {
581    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
582        f.debug_struct("RasterSource")
583            .field("cache_capacity", &self.cache_capacity)
584            .field("selection", &self.selection)
585            .finish_non_exhaustive()
586    }
587}
588
589/// Terrain source entry.
590#[derive(Clone)]
591pub struct TerrainSource {
592    /// Whether terrain is enabled.
593    pub enabled: bool,
594    /// Terrain vertical exaggeration.
595    pub vertical_exaggeration: f64,
596    /// Terrain mesh resolution.
597    pub mesh_resolution: u16,
598    /// Terrain skirt depth in meters.
599    pub skirt_depth: f64,
600    /// Terrain cache capacity.
601    pub cache_capacity: usize,
602    /// Factory used to build a fresh elevation source each time the style is applied.
603    pub factory: TerrainSourceFactory,
604}
605
606impl TerrainSource {
607    /// Create a terrain source from an elevation-source factory.
608    pub fn new(factory: impl Fn() -> Box<dyn ElevationSource> + Send + Sync + 'static) -> Self {
609        Self {
610            enabled: true,
611            vertical_exaggeration: 1.0,
612            mesh_resolution: 64,
613            skirt_depth: 100.0,
614            cache_capacity: 256,
615            factory: Arc::new(factory),
616        }
617    }
618
619    /// Set terrain cache capacity.
620    pub fn with_cache_capacity(mut self, cache_capacity: usize) -> Self {
621        self.cache_capacity = cache_capacity;
622        self
623    }
624
625    /// Convert this style terrain source to a terrain config using a fresh source instance.
626    pub fn to_terrain_config(&self) -> TerrainConfig {
627        TerrainConfig {
628            enabled: self.enabled,
629            vertical_exaggeration: self.vertical_exaggeration,
630            mesh_resolution: self.mesh_resolution,
631            skirt_depth: self.skirt_depth,
632            source_max_zoom: TerrainConfig::default().source_max_zoom,
633            source: (self.factory)(),
634        }
635    }
636}
637
638impl fmt::Debug for TerrainSource {
639    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640        f.debug_struct("TerrainSource")
641            .field("enabled", &self.enabled)
642            .field("vertical_exaggeration", &self.vertical_exaggeration)
643            .field("mesh_resolution", &self.mesh_resolution)
644            .field("skirt_depth", &self.skirt_depth)
645            .field("cache_capacity", &self.cache_capacity)
646            .finish_non_exhaustive()
647    }
648}
649
650/// In-memory vector feature source with optional point clustering.
651#[derive(Clone)]
652pub struct GeoJsonSource {
653    /// Features exposed by the source.
654    pub data: FeatureCollection,
655    /// Pre-built cluster index.  When present, layers referencing this
656    /// source receive clustered features based on the current zoom level.
657    cluster_index: Option<Arc<PointCluster>>,
658}
659
660impl fmt::Debug for GeoJsonSource {
661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662        f.debug_struct("GeoJsonSource")
663            .field("features", &self.data.len())
664            .field("clustered", &self.cluster_index.is_some())
665            .finish()
666    }
667}
668
669impl GeoJsonSource {
670    /// Create a new in-memory vector source.
671    pub fn new(data: FeatureCollection) -> Self {
672        Self {
673            data,
674            cluster_index: None,
675        }
676    }
677
678    /// Enable point clustering on this source.
679    ///
680    /// Builds the cluster index immediately from the current `data`.
681    /// Layers that reference this source will receive clustered features
682    /// at the appropriate zoom level instead of the raw point data.
683    pub fn with_clustering(mut self, options: ClusterOptions) -> Self {
684        let mut cluster = PointCluster::new(options);
685        cluster.load(&self.data);
686        self.cluster_index = Some(Arc::new(cluster));
687        self
688    }
689
690    /// Returns `true` if this source has clustering enabled.
691    pub fn is_clustered(&self) -> bool {
692        self.cluster_index.is_some()
693    }
694
695    /// Resolve features for a given zoom level.
696    ///
697    /// If clustering is enabled, returns zoom-appropriate clustered features.
698    /// Otherwise returns a borrowed reference to the raw data.
699    pub fn features_at_zoom(&self, zoom: u8) -> Cow<'_, FeatureCollection> {
700        if let Some(ref cluster) = self.cluster_index {
701            Cow::Owned(cluster.get_clusters_for_zoom(zoom))
702        } else {
703            Cow::Borrowed(&self.data)
704        }
705    }
706}
707
708/// Vector-tile-like feature source.
709#[derive(Clone)]
710pub struct VectorTileSource {
711    /// Flattened features exposed by the source.
712    ///
713    /// This remains for backward compatibility with earlier runtime code that
714    /// treated vector sources as one resolved feature collection.
715    pub data: FeatureCollection,
716    /// Optional source-layer partitioning for style/runtime resolution.
717    ///
718    /// When present, vector style layers may select a specific source layer via
719    /// their `source_layer` field, mirroring MapLibre's `source-layer`
720    /// behavior. When absent, the flattened `data` collection is used.
721    pub source_layers: HashMap<String, FeatureCollection>,
722    /// Optional streamed tile source factory.
723    ///
724    /// When present, the style/runtime path builds a source-owned hidden tile
725    /// manager that fetches binary vector tiles at runtime. The in-memory
726    /// `data` and `source_layers` remain available for tests, fallbacks, and
727    /// backward-compatible resolved-feature workflows.
728    pub factory: Option<VectorTileSourceFactory>,
729    /// Maximum tile-cache capacity for the streamed vector source runtime.
730    pub cache_capacity: usize,
731    /// Tile-selection policy for the streamed vector source runtime.
732    pub selection: TileSelectionConfig,
733}
734
735impl fmt::Debug for VectorTileSource {
736    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
737        f.debug_struct("VectorTileSource")
738            .field("feature_count", &self.data.len())
739            .field("source_layer_count", &self.source_layers.len())
740            .field("streamed", &self.factory.is_some())
741            .field("cache_capacity", &self.cache_capacity)
742            .field("selection", &self.selection)
743            .finish()
744    }
745}
746
747impl VectorTileSource {
748    /// Create a new vector-tile-like source from resolved features.
749    pub fn new(data: FeatureCollection) -> Self {
750        Self {
751            data,
752            source_layers: HashMap::new(),
753            factory: None,
754            cache_capacity: 256,
755            selection: TileSelectionConfig::default(),
756        }
757    }
758
759    /// Create a new streamed vector tile source from a tile-source factory.
760    pub fn streamed(
761        factory: impl Fn() -> Box<dyn TileSource> + Send + Sync + 'static,
762    ) -> Self {
763        Self {
764            data: FeatureCollection::default(),
765            source_layers: HashMap::new(),
766            factory: Some(Arc::new(factory)),
767            cache_capacity: 256,
768            selection: TileSelectionConfig::default(),
769        }
770    }
771
772    /// Create a new vector-tile-like source from named source-layer feature sets.
773    pub fn from_source_layers(source_layers: HashMap<String, FeatureCollection>) -> Self {
774        let mut data = FeatureCollection::default();
775        for features in source_layers.values() {
776            data.features.extend(features.features.iter().cloned());
777        }
778        Self {
779            data,
780            source_layers,
781            factory: None,
782            cache_capacity: 256,
783            selection: TileSelectionConfig::default(),
784        }
785    }
786
787    /// Attach a named source layer to this source.
788    pub fn with_source_layer(
789        mut self,
790        name: impl Into<String>,
791        data: FeatureCollection,
792    ) -> Self {
793        self.source_layers.insert(name.into(), data);
794        self.rebuild_flattened_data();
795        self
796    }
797
798    /// Borrow a named source layer if present.
799    pub fn source_layer(&self, name: &str) -> Option<&FeatureCollection> {
800        self.source_layers.get(name)
801    }
802
803    /// Return `true` if this source has explicit source-layer partitioning.
804    pub fn has_source_layers(&self) -> bool {
805        !self.source_layers.is_empty()
806    }
807
808    /// Return `true` if this source should fetch vector tiles at runtime.
809    pub fn is_streamed(&self) -> bool {
810        self.factory.is_some()
811    }
812
813    /// Set streamed tile cache capacity.
814    pub fn with_cache_capacity(mut self, cache_capacity: usize) -> Self {
815        self.cache_capacity = cache_capacity;
816        self
817    }
818
819    /// Set streamed tile-selection policy.
820    pub fn with_selection(mut self, selection: TileSelectionConfig) -> Self {
821        self.selection = selection;
822        self
823    }
824
825    /// Build a fresh runtime tile source if this vector source is streamed.
826    pub fn make_tile_source(&self) -> Option<Box<dyn TileSource>> {
827        self.factory.as_ref().map(|factory| (factory)())
828    }
829
830    fn rebuild_flattened_data(&mut self) {
831        let mut data = FeatureCollection::default();
832        for features in self.source_layers.values() {
833            data.features.extend(features.features.iter().cloned());
834        }
835        self.data = data;
836    }
837}
838
839/// Image source lowered onto raster rendering.
840#[derive(Clone)]
841pub struct ImageSource {
842    /// Maximum tile-cache capacity used by the created raster layer.
843    pub cache_capacity: usize,
844    /// Tile-selection policy for the created raster layer.
845    pub selection: TileSelectionConfig,
846    /// Factory used to build a fresh tile source each time the style is applied.
847    pub factory: RasterSourceFactory,
848}
849
850impl ImageSource {
851    /// Create an image source from a tile-source factory.
852    pub fn new(factory: impl Fn() -> Box<dyn TileSource> + Send + Sync + 'static) -> Self {
853        Self {
854            cache_capacity: 16,
855            selection: TileSelectionConfig::default(),
856            factory: Arc::new(factory),
857        }
858    }
859
860    /// Set tile cache capacity.
861    pub fn with_cache_capacity(mut self, cache_capacity: usize) -> Self {
862        self.cache_capacity = cache_capacity;
863        self
864    }
865
866    /// Set tile-selection policy.
867    pub fn with_selection(mut self, selection: TileSelectionConfig) -> Self {
868        self.selection = selection;
869        self
870    }
871}
872
873impl fmt::Debug for ImageSource {
874    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
875        f.debug_struct("ImageSource")
876            .field("cache_capacity", &self.cache_capacity)
877            .field("selection", &self.selection)
878            .finish_non_exhaustive()
879    }
880}
881
882/// Video source — georeferenced dynamic overlay driven by a
883/// [`FrameProvider`](crate::layers::FrameProvider).
884///
885/// This is the Rustial equivalent of MapLibre / Mapbox `video` source.
886/// In the browser, a `<video>` element supplies frames; in Rustial the
887/// user supplies a [`FrameProviderFactory`] that creates a
888/// [`FrameProvider`](crate::layers::FrameProvider) for each style
889/// application.
890///
891/// When a raster style layer references a video source, the style
892/// evaluator produces a [`DynamicImageOverlayLayer`] instead of a
893/// raster tile layer.
894#[derive(Clone)]
895pub struct VideoSource {
896    /// Geographic corner coordinates (TL, TR, BR, BL).
897    pub coordinates: [GeoCoord; 4],
898    /// Factory used to build a fresh frame provider when the style is applied.
899    pub factory: FrameProviderFactory,
900}
901
902impl VideoSource {
903    /// Create a video source.
904    ///
905    /// `coordinates` must be in TL → TR → BR → BL order.
906    pub fn new(
907        coordinates: [GeoCoord; 4],
908        factory: impl Fn() -> Box<dyn crate::layers::FrameProvider> + Send + Sync + 'static,
909    ) -> Self {
910        Self {
911            coordinates,
912            factory: Arc::new(factory),
913        }
914    }
915}
916
917impl fmt::Debug for VideoSource {
918    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919        f.debug_struct("VideoSource")
920            .field("coordinates", &self.coordinates)
921            .finish_non_exhaustive()
922    }
923}
924
925/// Canvas source — georeferenced dynamic overlay driven by a
926/// [`FrameProvider`](crate::layers::FrameProvider).
927///
928/// This is the Rustial equivalent of MapLibre / Mapbox `canvas` source.
929/// In the browser, a `<canvas>` element supplies frames; in Rustial the
930/// user supplies a [`FrameProviderFactory`] that creates a
931/// [`FrameProvider`](crate::layers::FrameProvider) for each style
932/// application.
933///
934/// The `animate` flag controls whether the source re-reads its provider
935/// each frame.  Set it to `false` for static canvas content to avoid
936/// unnecessary GPU re-uploads.
937#[derive(Clone)]
938pub struct CanvasSource {
939    /// Geographic corner coordinates (TL, TR, BR, BL).
940    pub coordinates: [GeoCoord; 4],
941    /// Factory used to build a fresh frame provider when the style is applied.
942    pub factory: FrameProviderFactory,
943    /// Whether this canvas source animates (polls each frame).
944    /// Defaults to `true`.
945    pub animate: bool,
946}
947
948impl CanvasSource {
949    /// Create a canvas source.
950    ///
951    /// `coordinates` must be in TL → TR → BR → BL order.
952    pub fn new(
953        coordinates: [GeoCoord; 4],
954        factory: impl Fn() -> Box<dyn crate::layers::FrameProvider> + Send + Sync + 'static,
955    ) -> Self {
956        Self {
957            coordinates,
958            factory: Arc::new(factory),
959            animate: true,
960        }
961    }
962
963    /// Set the `animate` flag.
964    pub fn with_animate(mut self, animate: bool) -> Self {
965        self.animate = animate;
966        self
967    }
968}
969
970impl fmt::Debug for CanvasSource {
971    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
972        f.debug_struct("CanvasSource")
973            .field("coordinates", &self.coordinates)
974            .field("animate", &self.animate)
975            .finish_non_exhaustive()
976    }
977}
978
979/// In-memory model source.
980#[derive(Debug, Clone, Default)]
981pub struct ModelSource {
982    /// Model instances exposed by the source.
983    pub instances: Vec<ModelInstance>,
984}
985
986impl ModelSource {
987    /// Create a new in-memory model source.
988    pub fn new(instances: Vec<ModelInstance>) -> Self {
989        Self { instances }
990    }
991}
992
993/// A style document containing source registry, ordered layers, and optional terrain source.
994#[derive(Debug, Default)]
995pub struct StyleDocument {
996    sources: HashMap<StyleSourceId, StyleSource>,
997    layers: Vec<StyleLayer>,
998    terrain_source: Option<StyleSourceId>,
999    projection: StyleProjection,
1000    fog: Option<FogConfig>,
1001}
1002
1003/// Type alias for backward compatibility.
1004///
1005/// [`StyleValue<T>`] is now an alias for [`Expression<T>`], the typed
1006/// expression engine. All existing `StyleValue::Constant(...)`,
1007/// `StyleValue::ZoomStops(...)`, and `StyleValue::FeatureState { .. }`
1008/// constructions continue to work unchanged.
1009///
1010/// New code should prefer importing [`Expression`] directly and using
1011/// the richer expression variants (`GetProperty`, `Interpolate`, `Step`,
1012/// `Match`, `Case`, etc.) for data-driven styling.
1013pub type StyleValue<T> = crate::expression::Expression<T>;
1014
1015/// Conversion from a [`PropertyValue`] stored in feature-state to a concrete
1016/// style value type.
1017///
1018/// Types that have a natural representation in `PropertyValue` (f32 from
1019/// Number, bool from Bool, String from String) implement this directly.
1020/// Types without a mapping (e.g. `[f32; 4]` colour tuples, enum variants)
1021/// return `None` so the fallback value is used instead.
1022///
1023/// This trait is intentionally sealed to [`StyleInterpolatable`] implementors
1024/// and does not need to be implemented by downstream code.
1025pub trait FromFeatureStateProperty: Sized {
1026    /// Attempt to convert a property value. Returns `None` when the
1027    /// `PropertyValue` variant does not map to `Self`.
1028    fn from_feature_state_property(prop: &PropertyValue) -> Option<Self>;
1029}
1030
1031impl FromFeatureStateProperty for f32 {
1032    fn from_feature_state_property(prop: &PropertyValue) -> Option<Self> {
1033        prop.as_f64().map(|v| v as f32)
1034    }
1035}
1036
1037impl FromFeatureStateProperty for [f32; 4] {
1038    /// Colour tuples cannot be expressed in a single `PropertyValue` today.
1039    fn from_feature_state_property(_prop: &PropertyValue) -> Option<Self> {
1040        None
1041    }
1042}
1043
1044impl FromFeatureStateProperty for bool {
1045    fn from_feature_state_property(prop: &PropertyValue) -> Option<Self> {
1046        prop.as_bool()
1047    }
1048}
1049
1050impl FromFeatureStateProperty for String {
1051    fn from_feature_state_property(prop: &PropertyValue) -> Option<Self> {
1052        prop.as_str().map(|s| s.to_owned())
1053    }
1054}
1055
1056impl FromFeatureStateProperty for SymbolTextJustify {
1057    fn from_feature_state_property(_prop: &PropertyValue) -> Option<Self> {
1058        None
1059    }
1060}
1061
1062impl FromFeatureStateProperty for SymbolTextTransform {
1063    fn from_feature_state_property(_prop: &PropertyValue) -> Option<Self> {
1064        None
1065    }
1066}
1067
1068impl FromFeatureStateProperty for SymbolIconTextFit {
1069    fn from_feature_state_property(_prop: &PropertyValue) -> Option<Self> {
1070        None
1071    }
1072}
1073
1074/// Shared style-layer metadata.
1075#[derive(Debug, Clone)]
1076pub struct StyleLayerMeta {
1077    /// Stable style-layer id.
1078    pub id: StyleLayerId,
1079    /// Human-readable layer name.
1080    pub name: String,
1081    /// Whether the layer is visible.
1082    pub visible: StyleValue<bool>,
1083    /// Layer opacity in `[0, 1]`.
1084    pub opacity: StyleValue<f32>,
1085    /// Optional minimum zoom for visibility.
1086    pub min_zoom: Option<f32>,
1087    /// Optional maximum zoom for visibility.
1088    pub max_zoom: Option<f32>,
1089}
1090
1091impl StyleLayerMeta {
1092    /// Create metadata with defaults.
1093    pub fn new(id: impl Into<String>) -> Self {
1094        let id = id.into();
1095        Self {
1096            name: id.clone(),
1097            id,
1098            visible: StyleValue::Constant(true),
1099            opacity: StyleValue::Constant(1.0),
1100            min_zoom: None,
1101            max_zoom: None,
1102        }
1103    }
1104
1105    fn visible_in_context(&self, ctx: StyleEvalContext) -> bool {
1106        if let Some(min_zoom) = self.min_zoom {
1107            if ctx.zoom < min_zoom {
1108                return false;
1109            }
1110        }
1111        if let Some(max_zoom) = self.max_zoom {
1112            if ctx.zoom > max_zoom {
1113                return false;
1114            }
1115        }
1116        self.visible.evaluate_with_context(ctx)
1117    }
1118}
1119
1120/// Background layer style spec.
1121#[allow(missing_docs)]
1122#[derive(Debug, Clone)]
1123pub struct BackgroundStyleLayer {
1124    pub meta: StyleLayerMeta,
1125    pub color: StyleValue<[f32; 4]>,
1126}
1127
1128impl BackgroundStyleLayer {
1129    /// Create a background style layer with the given id and fill colour.
1130    pub fn new(id: impl Into<String>, color: impl Into<StyleValue<[f32; 4]>>) -> Self {
1131        Self {
1132            meta: StyleLayerMeta::new(id),
1133            color: color.into(),
1134        }
1135    }
1136}
1137
1138/// Hillshade layer style spec.
1139#[allow(missing_docs)]
1140#[derive(Debug, Clone)]
1141pub struct HillshadeStyleLayer {
1142    pub meta: StyleLayerMeta,
1143    pub highlight_color: StyleValue<[f32; 4]>,
1144    pub shadow_color: StyleValue<[f32; 4]>,
1145    pub accent_color: StyleValue<[f32; 4]>,
1146    pub illumination_direction_deg: StyleValue<f32>,
1147    pub illumination_altitude_deg: StyleValue<f32>,
1148    pub exaggeration: StyleValue<f32>,
1149}
1150
1151impl HillshadeStyleLayer {
1152    /// Create a hillshade style layer with default hillshade parameters.
1153    pub fn new(id: impl Into<String>) -> Self {
1154        Self {
1155            meta: StyleLayerMeta::new(id),
1156            highlight_color: [1.0, 1.0, 1.0, 1.0].into(),
1157            shadow_color: [0.0, 0.0, 0.0, 1.0].into(),
1158            accent_color: [0.42, 0.48, 0.42, 1.0].into(),
1159            illumination_direction_deg: 335.0.into(),
1160            illumination_altitude_deg: 45.0.into(),
1161            exaggeration: 1.0.into(),
1162        }
1163    }
1164}
1165
1166/// Raster/tile layer style spec.
1167#[allow(missing_docs)]
1168#[derive(Debug, Clone)]
1169pub struct RasterStyleLayer {
1170    pub meta: StyleLayerMeta,
1171    pub source: StyleSourceId,
1172}
1173
1174impl RasterStyleLayer {
1175    /// Create a raster style layer bound to the given style source id.
1176    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1177        Self {
1178            meta: StyleLayerMeta::new(id),
1179            source: source.into(),
1180        }
1181    }
1182}
1183
1184/// Legacy generic vector style spec.
1185#[allow(missing_docs)]
1186#[derive(Debug, Clone)]
1187pub struct VectorStyleLayer {
1188    pub meta: StyleLayerMeta,
1189    pub source: StyleSourceId,
1190    /// Optional source-layer name for vector-tile-like sources.
1191    pub source_layer: Option<String>,
1192    pub fill_color: StyleValue<[f32; 4]>,
1193    pub stroke_color: StyleValue<[f32; 4]>,
1194    pub stroke_width: StyleValue<f32>,
1195}
1196
1197impl VectorStyleLayer {
1198    /// Create a generic vector style layer bound to the given style source id.
1199    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1200        Self {
1201            meta: StyleLayerMeta::new(id),
1202            source: source.into(),
1203            source_layer: None,
1204            fill_color: VectorStyle::default().fill_color.into(),
1205            stroke_color: VectorStyle::default().stroke_color.into(),
1206            stroke_width: VectorStyle::default().stroke_width.into(),
1207        }
1208    }
1209}
1210
1211/// Fill layer style spec.
1212#[allow(missing_docs)]
1213#[derive(Debug, Clone)]
1214pub struct FillStyleLayer {
1215    pub meta: StyleLayerMeta,
1216    pub source: StyleSourceId,
1217    /// Optional source-layer name for vector-tile-like sources.
1218    pub source_layer: Option<String>,
1219    pub fill_color: StyleValue<[f32; 4]>,
1220    pub outline_color: StyleValue<[f32; 4]>,
1221    pub outline_width: StyleValue<f32>,
1222    /// Optional repeating pattern image for fill-pattern rendering.
1223    ///
1224    /// When set, the fill is textured with the given pattern instead
1225    /// of a solid colour.  Matches MapLibre / Mapbox `fill-pattern`.
1226    pub fill_pattern: Option<std::sync::Arc<crate::PatternImage>>,
1227}
1228
1229impl FillStyleLayer {
1230    /// Create a fill style layer bound to the given style source id.
1231    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1232        let style = VectorStyle::default();
1233        Self {
1234            meta: StyleLayerMeta::new(id),
1235            source: source.into(),
1236            source_layer: None,
1237            fill_color: style.fill_color.into(),
1238            outline_color: style.stroke_color.into(),
1239            outline_width: style.stroke_width.into(),
1240            fill_pattern: None,
1241        }
1242    }
1243}
1244
1245/// Line layer style spec.
1246#[allow(missing_docs)]
1247#[derive(Debug, Clone)]
1248pub struct LineStyleLayer {
1249    pub meta: StyleLayerMeta,
1250    pub source: StyleSourceId,
1251    /// Optional source-layer name for vector-tile-like sources.
1252    pub source_layer: Option<String>,
1253    pub color: StyleValue<[f32; 4]>,
1254    pub width: StyleValue<f32>,
1255    /// Line cap style (default: butt).
1256    pub line_cap: LineCap,
1257    /// Line join style (default: miter).
1258    pub line_join: LineJoin,
1259    /// Miter limit ratio (default: 2.0).
1260    pub miter_limit: f32,
1261    /// Optional dash pattern `[dash, gap, …]` in pixels.
1262    pub dash_array: Option<Vec<f32>>,
1263    /// Optional colour ramp evaluated along the line's length.
1264    ///
1265    /// When set, per-vertex colours are overridden by the gradient
1266    /// evaluated at each vertex's normalized distance `[0, 1]` along the
1267    /// polyline.  Replaces `line-color` (matching MapLibre / Mapbox
1268    /// `line-gradient` semantics).
1269    pub line_gradient: Option<crate::visualization::ColorRamp>,
1270    /// Optional repeating pattern image for textured line rendering.
1271    ///
1272    /// When set, the line is rendered with a repeating pattern texture
1273    /// instead of a solid colour, matching MapLibre / Mapbox
1274    /// `line-pattern` semantics.
1275    pub line_pattern: Option<std::sync::Arc<crate::PatternImage>>,
1276}
1277
1278impl LineStyleLayer {
1279    /// Create a line style layer bound to the given style source id.
1280    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1281        let style = VectorStyle::default();
1282        Self {
1283            meta: StyleLayerMeta::new(id),
1284            source: source.into(),
1285            source_layer: None,
1286            color: style.stroke_color.into(),
1287            width: style.stroke_width.into(),
1288            line_cap: LineCap::default(),
1289            line_join: LineJoin::default(),
1290            miter_limit: 2.0,
1291            dash_array: None,
1292            line_gradient: None,
1293            line_pattern: None,
1294        }
1295    }
1296}
1297
1298/// Circle layer style spec.
1299#[allow(missing_docs)]
1300#[derive(Debug, Clone)]
1301pub struct CircleStyleLayer {
1302    pub meta: StyleLayerMeta,
1303    pub source: StyleSourceId,
1304    /// Optional source-layer name for vector-tile-like sources.
1305    pub source_layer: Option<String>,
1306    pub color: StyleValue<[f32; 4]>,
1307    pub radius: StyleValue<f32>,
1308    pub stroke_color: StyleValue<[f32; 4]>,
1309    pub stroke_width: StyleValue<f32>,
1310}
1311
1312impl CircleStyleLayer {
1313    /// Create a circle style layer bound to the given style source id.
1314    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1315        let style = VectorStyle::default();
1316        Self {
1317            meta: StyleLayerMeta::new(id),
1318            source: source.into(),
1319            source_layer: None,
1320            color: style.fill_color.into(),
1321            radius: style.point_radius.into(),
1322            stroke_color: style.stroke_color.into(),
1323            stroke_width: style.stroke_width.into(),
1324        }
1325    }
1326}
1327
1328/// Heatmap layer style spec.
1329#[allow(missing_docs)]
1330#[derive(Debug, Clone)]
1331pub struct HeatmapStyleLayer {
1332    pub meta: StyleLayerMeta,
1333    pub source: StyleSourceId,
1334    /// Optional source-layer name for vector-tile-like sources.
1335    pub source_layer: Option<String>,
1336    pub color: StyleValue<[f32; 4]>,
1337    pub radius: StyleValue<f32>,
1338    pub intensity: StyleValue<f32>,
1339}
1340
1341impl HeatmapStyleLayer {
1342    /// Create a heatmap style layer bound to the given style source id.
1343    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1344        let style = VectorStyle::default();
1345        Self {
1346            meta: StyleLayerMeta::new(id),
1347            source: source.into(),
1348            source_layer: None,
1349            color: style.fill_color.into(),
1350            radius: style.heatmap_radius.into(),
1351            intensity: style.heatmap_intensity.into(),
1352        }
1353    }
1354}
1355
1356/// Fill extrusion layer style spec.
1357#[allow(missing_docs)]
1358#[derive(Debug, Clone)]
1359pub struct FillExtrusionStyleLayer {
1360    pub meta: StyleLayerMeta,
1361    pub source: StyleSourceId,
1362    /// Optional source-layer name for vector-tile-like sources.
1363    pub source_layer: Option<String>,
1364    pub color: StyleValue<[f32; 4]>,
1365    pub base: StyleValue<f32>,
1366    pub height: StyleValue<f32>,
1367}
1368
1369impl FillExtrusionStyleLayer {
1370    /// Create a fill-extrusion style layer bound to the given style source id.
1371    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1372        let style = VectorStyle::default();
1373        Self {
1374            meta: StyleLayerMeta::new(id),
1375            source: source.into(),
1376            source_layer: None,
1377            color: style.fill_color.into(),
1378            base: style.extrusion_base.into(),
1379            height: style.extrusion_height.into(),
1380        }
1381    }
1382}
1383
1384/// Symbol layer style spec.
1385#[allow(missing_docs)]
1386#[derive(Debug, Clone)]
1387pub struct SymbolStyleLayer {
1388    pub meta: StyleLayerMeta,
1389    pub source: StyleSourceId,
1390    /// Optional source-layer name for vector-tile-like sources.
1391    pub source_layer: Option<String>,
1392    pub color: StyleValue<[f32; 4]>,
1393    pub halo_color: StyleValue<[f32; 4]>,
1394    pub size: StyleValue<f32>,
1395    pub text_field: Option<StyleValue<String>>,
1396    pub icon_image: Option<StyleValue<String>>,
1397    pub font_stack: StyleValue<String>,
1398    pub padding: StyleValue<f32>,
1399    /// Shared overlap fallback for simplified callers.
1400    pub allow_overlap: StyleValue<bool>,
1401    /// Explicit text overlap control from the style specification.
1402    pub text_allow_overlap: Option<StyleValue<bool>>,
1403    /// Explicit icon overlap control from the style specification.
1404    pub icon_allow_overlap: Option<StyleValue<bool>>,
1405    /// Whether text may be dropped while keeping the icon.
1406    pub text_optional: Option<StyleValue<bool>>,
1407    /// Whether the icon may be dropped while keeping the text.
1408    pub icon_optional: Option<StyleValue<bool>>,
1409    /// Whether text may be placed without blocking later symbols.
1410    pub text_ignore_placement: Option<StyleValue<bool>>,
1411    /// Whether the icon may be placed without blocking later symbols.
1412    pub icon_ignore_placement: Option<StyleValue<bool>>,
1413    /// Radial text offset measured in text-size units.
1414    pub radial_offset: Option<StyleValue<f32>>,
1415    /// Explicit per-anchor offsets for variable anchor placement.
1416    pub variable_anchor_offsets: Option<Vec<(SymbolAnchor, [f32; 2])>>,
1417    /// Default anchor when variable anchors are not in use.
1418    pub anchor: SymbolAnchor,
1419    /// Requested text justification.
1420    pub justify: StyleValue<SymbolTextJustify>,
1421    /// Text transformation applied before shaping and measurement.
1422    pub transform: StyleValue<SymbolTextTransform>,
1423    /// Maximum point-label width before wrapping.
1424    pub max_width: Option<StyleValue<f32>>,
1425    /// Preferred wrapped text line height.
1426    pub line_height: Option<StyleValue<f32>>,
1427    /// Extra spacing between adjacent glyphs.
1428    pub letter_spacing: Option<StyleValue<f32>>,
1429    /// Icon sizing mode relative to text.
1430    pub icon_text_fit: StyleValue<SymbolIconTextFit>,
1431    /// Padding applied when fitting an icon around text.
1432    pub icon_text_fit_padding: [f32; 4],
1433    /// Placement priority for symbol ordering.
1434    pub sort_key: Option<StyleValue<f32>>,
1435    /// Whether symbols are placed at points or anchored along lines.
1436    pub placement: SymbolPlacement,
1437    /// Preferred spacing for repeated line-placed symbols.
1438    pub spacing: StyleValue<f32>,
1439    /// Maximum cumulative turn angle tolerated for a line-placed label.
1440    pub max_angle: StyleValue<f32>,
1441    /// Whether line-placed text should be flipped to remain upright.
1442    pub keep_upright: StyleValue<bool>,
1443    pub variable_anchors: Vec<SymbolAnchor>,
1444    pub writing_mode: SymbolWritingMode,
1445    pub offset: [f32; 2],
1446}
1447
1448impl SymbolStyleLayer {
1449    /// Create a symbol style layer bound to the given style source id.
1450    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1451        let style = VectorStyle::default();
1452        Self {
1453            meta: StyleLayerMeta::new(id),
1454            source: source.into(),
1455            source_layer: None,
1456            color: style.fill_color.into(),
1457            halo_color: style.symbol_halo_color.into(),
1458            size: style.symbol_size.into(),
1459            text_field: None,
1460            icon_image: None,
1461            font_stack: style.symbol_font_stack.into(),
1462            padding: style.symbol_padding.into(),
1463            allow_overlap: style.symbol_allow_overlap.into(),
1464            text_allow_overlap: None,
1465            icon_allow_overlap: None,
1466            text_optional: None,
1467            icon_optional: None,
1468            text_ignore_placement: None,
1469            icon_ignore_placement: None,
1470            radial_offset: None,
1471            variable_anchor_offsets: None,
1472            anchor: style.symbol_text_anchor,
1473            justify: style.symbol_text_justify.into(),
1474            transform: style.symbol_text_transform.into(),
1475            max_width: None,
1476            line_height: None,
1477            letter_spacing: None,
1478            icon_text_fit: style.symbol_icon_text_fit.into(),
1479            icon_text_fit_padding: style.symbol_icon_text_fit_padding,
1480            sort_key: None,
1481            placement: style.symbol_placement,
1482            spacing: style.symbol_spacing.into(),
1483            max_angle: style.symbol_max_angle.into(),
1484            keep_upright: style.symbol_keep_upright.into(),
1485            variable_anchors: style.symbol_anchors.clone(),
1486            writing_mode: style.symbol_writing_mode,
1487            offset: style.symbol_offset,
1488        }
1489    }
1490}
1491
1492/// Model layer style spec.
1493#[allow(missing_docs)]
1494#[derive(Debug, Clone)]
1495pub struct ModelStyleLayer {
1496    pub meta: StyleLayerMeta,
1497    pub source: StyleSourceId,
1498}
1499
1500impl ModelStyleLayer {
1501    /// Create a model style layer bound to the given style source id.
1502    pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
1503        Self {
1504            meta: StyleLayerMeta::new(id),
1505            source: source.into(),
1506        }
1507    }
1508}
1509
1510/// Supported style-layer variants.
1511#[derive(Debug, Clone)]
1512pub enum StyleLayer {
1513    /// Solid background style layer.
1514    Background(BackgroundStyleLayer),
1515    /// Terrain hillshade style layer.
1516    Hillshade(HillshadeStyleLayer),
1517    /// Raster imagery style layer.
1518    Raster(RasterStyleLayer),
1519    /// Generic vector style layer.
1520    Vector(VectorStyleLayer),
1521    /// Polygon fill style layer.
1522    Fill(FillStyleLayer),
1523    /// Polyline stroke style layer.
1524    Line(LineStyleLayer),
1525    /// Point circle style layer.
1526    Circle(CircleStyleLayer),
1527    /// Heatmap style layer.
1528    Heatmap(HeatmapStyleLayer),
1529    /// Extruded polygon fill style layer.
1530    FillExtrusion(FillExtrusionStyleLayer),
1531    /// Text and icon symbol style layer.
1532    Symbol(SymbolStyleLayer),
1533    /// 3D model placement style layer.
1534    Model(ModelStyleLayer),
1535}
1536
1537impl StyleLayer {
1538    /// Layer id.
1539    pub fn id(&self) -> &str {
1540        self.meta().id.as_str()
1541    }
1542
1543    /// Shared metadata.
1544    pub fn meta(&self) -> &StyleLayerMeta {
1545        match self {
1546            StyleLayer::Background(layer) => &layer.meta,
1547            StyleLayer::Hillshade(layer) => &layer.meta,
1548            StyleLayer::Raster(layer) => &layer.meta,
1549            StyleLayer::Vector(layer) => &layer.meta,
1550            StyleLayer::Fill(layer) => &layer.meta,
1551            StyleLayer::Line(layer) => &layer.meta,
1552            StyleLayer::Circle(layer) => &layer.meta,
1553            StyleLayer::Heatmap(layer) => &layer.meta,
1554            StyleLayer::FillExtrusion(layer) => &layer.meta,
1555            StyleLayer::Symbol(layer) => &layer.meta,
1556            StyleLayer::Model(layer) => &layer.meta,
1557        }
1558    }
1559
1560    /// Shared metadata, mutable.
1561    pub fn meta_mut(&mut self) -> &mut StyleLayerMeta {
1562        match self {
1563            StyleLayer::Background(layer) => &mut layer.meta,
1564            StyleLayer::Hillshade(layer) => &mut layer.meta,
1565            StyleLayer::Raster(layer) => &mut layer.meta,
1566            StyleLayer::Vector(layer) => &mut layer.meta,
1567            StyleLayer::Fill(layer) => &mut layer.meta,
1568            StyleLayer::Line(layer) => &mut layer.meta,
1569            StyleLayer::Circle(layer) => &mut layer.meta,
1570            StyleLayer::Heatmap(layer) => &mut layer.meta,
1571            StyleLayer::FillExtrusion(layer) => &mut layer.meta,
1572            StyleLayer::Symbol(layer) => &mut layer.meta,
1573            StyleLayer::Model(layer) => &mut layer.meta,
1574        }
1575    }
1576
1577    /// Evaluate into a concrete runtime layer with a context.
1578    pub fn to_runtime_layer_with_context(
1579        &self,
1580        sources: &HashMap<StyleSourceId, StyleSource>,
1581        ctx: StyleEvalContext,
1582    ) -> Result<Box<dyn Layer>, StyleError> {
1583        match self {
1584            StyleLayer::Background(layer) => Ok(Box::new(evaluate_background_layer(layer, ctx))),
1585            StyleLayer::Hillshade(layer) => Ok(Box::new(evaluate_hillshade_layer(layer, ctx))),
1586            StyleLayer::Raster(layer) => evaluate_raster_layer(layer, sources, ctx),
1587            StyleLayer::Vector(layer) => evaluate_vector_layer(layer, sources, ctx),
1588            StyleLayer::Fill(layer) => evaluate_fill_layer(layer, sources, ctx),
1589            StyleLayer::Line(layer) => evaluate_line_layer(layer, sources, ctx),
1590            StyleLayer::Circle(layer) => evaluate_circle_layer(layer, sources, ctx),
1591            StyleLayer::Heatmap(layer) => evaluate_heatmap_layer(layer, sources, ctx),
1592            StyleLayer::FillExtrusion(layer) => evaluate_fill_extrusion_layer(layer, sources, ctx),
1593            StyleLayer::Symbol(layer) => evaluate_symbol_layer(layer, sources, ctx),
1594            StyleLayer::Model(layer) => evaluate_model_layer(layer, sources, ctx),
1595        }
1596    }
1597
1598    /// Apply evaluated paint/layout state onto an existing runtime layer.
1599    pub fn apply_to_runtime_layer_with_context(
1600        &self,
1601        runtime: &mut dyn Layer,
1602        sources: &HashMap<StyleSourceId, StyleSource>,
1603        ctx: StyleEvalContext,
1604    ) -> Result<(), StyleError> {
1605        match self {
1606            StyleLayer::Background(layer) => apply_background_to_runtime(layer, runtime, ctx),
1607            StyleLayer::Hillshade(layer) => apply_hillshade_to_runtime(layer, runtime, ctx),
1608            StyleLayer::Raster(layer) => apply_raster_to_runtime(layer, runtime, sources, ctx),
1609            StyleLayer::Vector(layer) => apply_vector_to_runtime(layer, runtime, sources, ctx),
1610            StyleLayer::Fill(layer) => apply_fill_to_runtime(layer, runtime, sources, ctx),
1611            StyleLayer::Line(layer) => apply_line_to_runtime(layer, runtime, sources, ctx),
1612            StyleLayer::Circle(layer) => apply_circle_to_runtime(layer, runtime, sources, ctx),
1613            StyleLayer::Heatmap(layer) => apply_heatmap_to_runtime(layer, runtime, sources, ctx),
1614            StyleLayer::FillExtrusion(layer) => apply_fill_extrusion_to_runtime(layer, runtime, sources, ctx),
1615            StyleLayer::Symbol(layer) => apply_symbol_to_runtime(layer, runtime, sources, ctx),
1616            StyleLayer::Model(layer) => apply_model_to_runtime(layer, runtime, sources, ctx),
1617        }
1618    }
1619
1620    /// Return the referenced source id, if this layer is source-backed.
1621    pub fn source_id(&self) -> Option<&str> {
1622        match self {
1623            StyleLayer::Background(_) | StyleLayer::Hillshade(_) => None,
1624            StyleLayer::Raster(layer) => Some(layer.source.as_str()),
1625            StyleLayer::Vector(layer) => Some(layer.source.as_str()),
1626            StyleLayer::Fill(layer) => Some(layer.source.as_str()),
1627            StyleLayer::Line(layer) => Some(layer.source.as_str()),
1628            StyleLayer::Circle(layer) => Some(layer.source.as_str()),
1629            StyleLayer::Heatmap(layer) => Some(layer.source.as_str()),
1630            StyleLayer::FillExtrusion(layer) => Some(layer.source.as_str()),
1631            StyleLayer::Symbol(layer) => Some(layer.source.as_str()),
1632            StyleLayer::Model(layer) => Some(layer.source.as_str()),
1633        }
1634    }
1635
1636    /// Return the referenced source-layer name, if this layer targets a
1637    /// named source layer within a vector-tile-like source.
1638    pub fn source_layer(&self) -> Option<&str> {
1639        match self {
1640            StyleLayer::Vector(layer) => layer.source_layer.as_deref(),
1641            StyleLayer::Fill(layer) => layer.source_layer.as_deref(),
1642            StyleLayer::Line(layer) => layer.source_layer.as_deref(),
1643            StyleLayer::Circle(layer) => layer.source_layer.as_deref(),
1644            StyleLayer::Heatmap(layer) => layer.source_layer.as_deref(),
1645            StyleLayer::FillExtrusion(layer) => layer.source_layer.as_deref(),
1646            StyleLayer::Symbol(layer) => layer.source_layer.as_deref(),
1647            _ => None,
1648        }
1649    }
1650
1651    /// Return `true` if this layer references the given source id.
1652    pub fn uses_source(&self, source_id: &str) -> bool {
1653        self.source_id() == Some(source_id)
1654    }
1655
1656    /// Whether any paint property on this layer uses a `FeatureState` value.
1657    ///
1658    /// This lets callers skip per-feature re-evaluation for layers that only
1659    /// use constant or zoom-keyed paint properties.
1660    pub fn has_feature_state_driven_paint(&self) -> bool {
1661        match self {
1662            StyleLayer::Fill(l) => {
1663                l.fill_color.is_feature_state_driven()
1664                    || l.outline_color.is_feature_state_driven()
1665                    || l.outline_width.is_feature_state_driven()
1666            }
1667            StyleLayer::Line(l) => {
1668                l.color.is_feature_state_driven() || l.width.is_feature_state_driven()
1669            }
1670            StyleLayer::Circle(l) => {
1671                l.color.is_feature_state_driven()
1672                    || l.radius.is_feature_state_driven()
1673                    || l.stroke_color.is_feature_state_driven()
1674                    || l.stroke_width.is_feature_state_driven()
1675            }
1676            StyleLayer::Heatmap(l) => {
1677                l.color.is_feature_state_driven()
1678                    || l.radius.is_feature_state_driven()
1679                    || l.intensity.is_feature_state_driven()
1680            }
1681            StyleLayer::FillExtrusion(l) => {
1682                l.color.is_feature_state_driven()
1683                    || l.base.is_feature_state_driven()
1684                    || l.height.is_feature_state_driven()
1685            }
1686            StyleLayer::Symbol(l) => {
1687                l.color.is_feature_state_driven()
1688                    || l.halo_color.is_feature_state_driven()
1689                    || l.size.is_feature_state_driven()
1690            }
1691            StyleLayer::Vector(l) => {
1692                l.fill_color.is_feature_state_driven()
1693                    || l.stroke_color.is_feature_state_driven()
1694                    || l.stroke_width.is_feature_state_driven()
1695            }
1696            // Non-vector layers do not support feature-state-driven paint.
1697            StyleLayer::Background(_)
1698            | StyleLayer::Hillshade(_)
1699            | StyleLayer::Raster(_)
1700            | StyleLayer::Model(_) => false,
1701        }
1702    }
1703
1704    /// Resolve the layer's paint properties against a full evaluation context
1705    /// that includes per-feature mutable state.
1706    ///
1707    /// Returns `Some(VectorStyle)` for vector-backed layer families, or `None`
1708    /// for layer types that do not produce a `VectorStyle` (background,
1709    /// hillshade, raster, model).
1710    ///
1711    /// This is the primary entry point for hover/selection-driven restyling:
1712    /// the caller builds a [`StyleEvalContextFull`] with the target feature's
1713    /// current state, then receives a resolved `VectorStyle` that can be
1714    /// compared or applied to the feature's visual representation.
1715    pub fn resolve_style_with_feature_state(
1716        &self,
1717        ctx: &StyleEvalContextFull<'_>,
1718    ) -> Option<VectorStyle> {
1719        match self {
1720            StyleLayer::Fill(l) => Some(fill_style_with_state(l, ctx)),
1721            StyleLayer::Line(l) => Some(line_style_with_state(l, ctx)),
1722            StyleLayer::Circle(l) => Some(circle_style_with_state(l, ctx)),
1723            StyleLayer::Heatmap(l) => Some(heatmap_style_with_state(l, ctx)),
1724            StyleLayer::FillExtrusion(l) => Some(fill_extrusion_style_with_state(l, ctx)),
1725            StyleLayer::Symbol(l) => Some(symbol_style_with_state(l, ctx)),
1726            StyleLayer::Vector(l) => Some(vector_style_with_state(l, ctx)),
1727            // Non-vector layers do not produce a VectorStyle.
1728            StyleLayer::Background(_)
1729            | StyleLayer::Hillshade(_)
1730            | StyleLayer::Raster(_)
1731            | StyleLayer::Model(_) => None,
1732        }
1733    }
1734}
1735
1736impl StyleDocument {
1737    /// Create an empty style document.
1738    pub fn new() -> Self {
1739        Self::default()
1740    }
1741
1742    /// Register a named source.
1743    pub fn add_source(
1744        &mut self,
1745        id: impl Into<String>,
1746        source: StyleSource,
1747    ) -> Result<(), StyleError> {
1748        let id = id.into();
1749        if self.sources.contains_key(&id) {
1750            return Err(StyleError::DuplicateSourceId(id));
1751        }
1752        self.sources.insert(id, source);
1753        Ok(())
1754    }
1755
1756    /// Upsert a named source.
1757    pub fn set_source(&mut self, id: impl Into<String>, source: StyleSource) {
1758        self.sources.insert(id.into(), source);
1759    }
1760
1761    /// Remove a source, returning it if present.
1762    pub fn remove_source(&mut self, id: &str) -> Option<StyleSource> {
1763        if self.terrain_source.as_deref() == Some(id) {
1764            self.terrain_source = None;
1765        }
1766        self.sources.remove(id)
1767    }
1768
1769    /// Look up a source by id.
1770    pub fn source(&self, id: &str) -> Option<&StyleSource> {
1771        self.sources.get(id)
1772    }
1773
1774    /// Iterate registered sources.
1775    pub fn sources(&self) -> impl Iterator<Item = (&str, &StyleSource)> {
1776        self.sources.iter().map(|(id, source)| (id.as_str(), source))
1777    }
1778
1779    /// Set the terrain source id used to configure `MapState` terrain.
1780    pub fn set_terrain_source(&mut self, source_id: Option<impl Into<String>>) {
1781        self.terrain_source = source_id.map(Into::into);
1782    }
1783
1784    /// Return the configured terrain source id, if any.
1785    pub fn terrain_source(&self) -> Option<&str> {
1786        self.terrain_source.as_deref()
1787    }
1788
1789    /// Set the top-level style projection.
1790    pub fn set_projection(&mut self, projection: StyleProjection) {
1791        self.projection = projection;
1792    }
1793
1794    /// Return the top-level style projection.
1795    pub fn projection(&self) -> StyleProjection {
1796        self.projection
1797    }
1798
1799    /// Set the fog/atmosphere configuration.
1800    pub fn set_fog(&mut self, fog: Option<FogConfig>) {
1801        self.fog = fog;
1802    }
1803
1804    /// Return the fog/atmosphere configuration, if any.
1805    pub fn fog(&self) -> Option<&FogConfig> {
1806        self.fog.as_ref()
1807    }
1808
1809    /// Append a style layer to the ordered layer stack.
1810    pub fn add_layer(&mut self, layer: StyleLayer) -> Result<(), StyleError> {
1811        if self.layers.iter().any(|existing| existing.id() == layer.id()) {
1812            return Err(StyleError::DuplicateLayerId(layer.id().to_owned()));
1813        }
1814        self.layers.push(layer);
1815        Ok(())
1816    }
1817
1818    /// Insert a style layer before another style layer id.
1819    pub fn insert_layer_before(
1820        &mut self,
1821        before_id: &str,
1822        layer: StyleLayer,
1823    ) -> Result<(), StyleError> {
1824        if self.layers.iter().any(|existing| existing.id() == layer.id()) {
1825            return Err(StyleError::DuplicateLayerId(layer.id().to_owned()));
1826        }
1827        if let Some(index) = self.layer_index(before_id) {
1828            self.layers.insert(index, layer);
1829        } else {
1830            self.layers.push(layer);
1831        }
1832        Ok(())
1833    }
1834
1835    /// Move an existing style layer before another style layer.
1836    pub fn move_layer_before(&mut self, layer_id: &str, before_id: &str) -> bool {
1837        let Some(from) = self.layer_index(layer_id) else {
1838            return false;
1839        };
1840        let layer = self.layers.remove(from);
1841        let to = self.layer_index(before_id).unwrap_or(self.layers.len());
1842        self.layers.insert(to, layer);
1843        true
1844    }
1845
1846    /// Remove a style layer by id.
1847    pub fn remove_layer(&mut self, layer_id: &str) -> Option<StyleLayer> {
1848        self.layer_index(layer_id).map(|index| self.layers.remove(index))
1849    }
1850
1851    /// Get a style layer by id.
1852    pub fn layer(&self, layer_id: &str) -> Option<&StyleLayer> {
1853        self.layers.iter().find(|layer| layer.id() == layer_id)
1854    }
1855
1856    /// Get a mutable style layer by id.
1857    pub fn layer_mut(&mut self, layer_id: &str) -> Option<&mut StyleLayer> {
1858        self.layers.iter_mut().find(|layer| layer.id() == layer_id)
1859    }
1860
1861    /// Iterate style layers in render order.
1862    pub fn layers(&self) -> &[StyleLayer] {
1863        &self.layers
1864    }
1865
1866    /// Evaluate the style document to a concrete runtime layer stack.
1867    pub fn to_runtime_layers(&self) -> Result<Vec<Box<dyn Layer>>, StyleError> {
1868        self.to_runtime_layers_with_context(StyleEvalContext::default())
1869    }
1870
1871    /// Evaluate the style document to a concrete runtime layer stack using a context.
1872    pub fn to_runtime_layers_with_context(
1873        &self,
1874        ctx: StyleEvalContext,
1875    ) -> Result<Vec<Box<dyn Layer>>, StyleError> {
1876        self.layers
1877            .iter()
1878            .map(|layer| layer.to_runtime_layer_with_context(&self.sources, ctx))
1879            .collect()
1880    }
1881
1882    /// Evaluate the configured terrain source to a concrete terrain config.
1883    pub fn to_terrain_config(&self) -> Result<Option<(TerrainConfig, usize)>, StyleError> {
1884        let Some(source_id) = self.terrain_source.as_deref() else {
1885            return Ok(None);
1886        };
1887        let Some(source) = self.sources.get(source_id) else {
1888            return Err(StyleError::MissingSource(source_id.to_owned()));
1889        };
1890        match source {
1891            StyleSource::Terrain(terrain) => Ok(Some((terrain.to_terrain_config(), terrain.cache_capacity))),
1892            other => Err(StyleError::SourceKindMismatch {
1893                layer_id: "<terrain>".to_owned(),
1894                source_id: source_id.to_owned(),
1895                expected: "terrain",
1896                actual: other.kind_name(),
1897            }),
1898        }
1899    }
1900
1901    #[allow(dead_code)]
1902    pub(crate) fn apply_runtime_layers_with_context(
1903        &self,
1904        layers: &mut crate::layers::LayerStack,
1905        ctx: StyleEvalContext,
1906    ) -> Result<(), StyleError> {
1907        if layers.len() != self.layers.len() {
1908            return Ok(());
1909        }
1910        for (style_layer, runtime_layer) in self.layers.iter().zip(layers.iter_mut()) {
1911            style_layer.apply_to_runtime_layer_with_context(runtime_layer.as_mut(), &self.sources, ctx)?;
1912        }
1913        Ok(())
1914    }
1915
1916    fn layer_index(&self, layer_id: &str) -> Option<usize> {
1917        self.layers.iter().position(|layer| layer.id() == layer_id)
1918    }
1919
1920    /// Return `true` if any style layer or terrain configuration uses the
1921    /// given source id.
1922    pub fn source_is_used(&self, source_id: &str) -> bool {
1923        self.terrain_source.as_deref() == Some(source_id)
1924            || self.layers.iter().any(|layer| layer.uses_source(source_id))
1925    }
1926
1927    /// Return the ordered list of style layer ids that reference the given
1928    /// source id.
1929    pub fn layer_ids_using_source(&self, source_id: &str) -> Vec<&str> {
1930        self.layers
1931            .iter()
1932            .filter(|layer| layer.uses_source(source_id))
1933            .map(|layer| layer.id())
1934            .collect()
1935    }
1936}
1937
1938fn evaluate_background_layer(layer: &BackgroundStyleLayer, ctx: StyleEvalContext) -> BackgroundLayer {
1939    let mut runtime = BackgroundLayer::new(layer.meta.name.clone(), layer.color.evaluate_with_context(ctx));
1940    apply_shared_meta(&mut runtime, &layer.meta, ctx);
1941    runtime
1942}
1943
1944fn evaluate_hillshade_layer(layer: &HillshadeStyleLayer, ctx: StyleEvalContext) -> HillshadeLayer {
1945    let mut runtime = HillshadeLayer::new(layer.meta.name.clone());
1946    apply_shared_meta(&mut runtime, &layer.meta, ctx);
1947    runtime.set_highlight_color(layer.highlight_color.evaluate_with_context(ctx));
1948    runtime.set_shadow_color(layer.shadow_color.evaluate_with_context(ctx));
1949    runtime.set_accent_color(layer.accent_color.evaluate_with_context(ctx));
1950    runtime.set_illumination_direction_deg(layer.illumination_direction_deg.evaluate_with_context(ctx));
1951    runtime.set_illumination_altitude_deg(layer.illumination_altitude_deg.evaluate_with_context(ctx));
1952    runtime.set_exaggeration(layer.exaggeration.evaluate_with_context(ctx));
1953    runtime
1954}
1955
1956fn evaluate_raster_layer(
1957    layer: &RasterStyleLayer,
1958    sources: &HashMap<StyleSourceId, StyleSource>,
1959    ctx: StyleEvalContext,
1960) -> Result<Box<dyn Layer>, StyleError> {
1961    // Video/canvas sources produce a DynamicImageOverlayLayer instead of
1962    // a raster tile layer.
1963    if let Some(result) = try_dynamic_overlay_from_source(&layer.meta.name, &layer.source, sources, ctx) {
1964        return result.map(|mut runtime| {
1965            apply_shared_meta(runtime.as_mut(), &layer.meta, ctx);
1966            runtime
1967        });
1968    }
1969
1970    let (factory, cache_capacity, selection) = require_raster_source(&layer.meta.id, &layer.source, sources)?;
1971    let mut runtime = TileLayer::new_with_selection_config(
1972        layer.meta.name.clone(),
1973        (factory)(),
1974        cache_capacity,
1975        selection.clone(),
1976    );
1977    apply_shared_meta(&mut runtime, &layer.meta, ctx);
1978    Ok(Box::new(runtime))
1979}
1980
1981fn evaluate_vector_runtime_layer(
1982    meta: &StyleLayerMeta,
1983    source_id: &str,
1984    source_layer: Option<&str>,
1985    layer_id: &str,
1986    style: VectorStyle,
1987    sources: &HashMap<StyleSourceId, StyleSource>,
1988    ctx: StyleEvalContext,
1989) -> Result<Box<dyn Layer>, StyleError> {
1990    let features = match sources.get(source_id) {
1991        Some(StyleSource::VectorTile(source)) if source.is_streamed() => FeatureCollection::default(),
1992        _ => require_vector_source(layer_id, source_id, source_layer, sources, ctx.zoom as u8)?.into_owned(),
1993    };
1994    let mut runtime = VectorLayer::new(meta.name.clone(), features, style)
1995        .with_query_metadata(Some(layer_id.to_owned()), Some(source_id.to_owned()))
1996        .with_source_layer(source_layer.map(str::to_owned));
1997    apply_shared_meta(&mut runtime, meta, ctx);
1998    Ok(Box::new(runtime))
1999}
2000
2001fn evaluate_vector_layer(
2002    layer: &VectorStyleLayer,
2003    sources: &HashMap<StyleSourceId, StyleSource>,
2004    ctx: StyleEvalContext,
2005) -> Result<Box<dyn Layer>, StyleError> {
2006    evaluate_vector_runtime_layer(
2007        &layer.meta,
2008        &layer.source,
2009        layer.source_layer.as_deref(),
2010        &layer.meta.id,
2011        vector_style_from_vector_layer(layer, ctx),
2012        sources,
2013        ctx,
2014    )
2015}
2016
2017fn evaluate_fill_layer(
2018    layer: &FillStyleLayer,
2019    sources: &HashMap<StyleSourceId, StyleSource>,
2020    ctx: StyleEvalContext,
2021) -> Result<Box<dyn Layer>, StyleError> {
2022    evaluate_vector_runtime_layer(
2023        &layer.meta,
2024        &layer.source,
2025        layer.source_layer.as_deref(),
2026        &layer.meta.id,
2027        vector_style_from_fill_layer(layer, ctx),
2028        sources,
2029        ctx,
2030    )
2031}
2032
2033fn evaluate_line_layer(
2034    layer: &LineStyleLayer,
2035    sources: &HashMap<StyleSourceId, StyleSource>,
2036    ctx: StyleEvalContext,
2037) -> Result<Box<dyn Layer>, StyleError> {
2038    evaluate_vector_runtime_layer(
2039        &layer.meta,
2040        &layer.source,
2041        layer.source_layer.as_deref(),
2042        &layer.meta.id,
2043        vector_style_from_line_layer(layer, ctx),
2044        sources,
2045        ctx,
2046    )
2047}
2048
2049fn evaluate_circle_layer(
2050    layer: &CircleStyleLayer,
2051    sources: &HashMap<StyleSourceId, StyleSource>,
2052    ctx: StyleEvalContext,
2053) -> Result<Box<dyn Layer>, StyleError> {
2054    evaluate_vector_runtime_layer(
2055        &layer.meta,
2056        &layer.source,
2057        layer.source_layer.as_deref(),
2058        &layer.meta.id,
2059        vector_style_from_circle_layer(layer, ctx),
2060        sources,
2061        ctx,
2062    )
2063}
2064
2065fn evaluate_heatmap_layer(
2066    layer: &HeatmapStyleLayer,
2067    sources: &HashMap<StyleSourceId, StyleSource>,
2068    ctx: StyleEvalContext,
2069) -> Result<Box<dyn Layer>, StyleError> {
2070    evaluate_vector_runtime_layer(
2071        &layer.meta,
2072        &layer.source,
2073        layer.source_layer.as_deref(),
2074        &layer.meta.id,
2075        vector_style_from_heatmap_layer(layer, ctx),
2076        sources,
2077        ctx,
2078    )
2079}
2080
2081fn evaluate_fill_extrusion_layer(
2082    layer: &FillExtrusionStyleLayer,
2083    sources: &HashMap<StyleSourceId, StyleSource>,
2084    ctx: StyleEvalContext,
2085) -> Result<Box<dyn Layer>, StyleError> {
2086    evaluate_vector_runtime_layer(
2087        &layer.meta,
2088        &layer.source,
2089        layer.source_layer.as_deref(),
2090        &layer.meta.id,
2091        vector_style_from_fill_extrusion_layer(layer, ctx),
2092        sources,
2093        ctx,
2094    )
2095}
2096
2097fn evaluate_symbol_layer(
2098    layer: &SymbolStyleLayer,
2099    sources: &HashMap<StyleSourceId, StyleSource>,
2100    ctx: StyleEvalContext,
2101) -> Result<Box<dyn Layer>, StyleError> {
2102    evaluate_vector_runtime_layer(
2103        &layer.meta,
2104        &layer.source,
2105        layer.source_layer.as_deref(),
2106        &layer.meta.id,
2107        vector_style_from_symbol_layer(layer, ctx),
2108        sources,
2109        ctx,
2110    )
2111}
2112
2113fn evaluate_model_layer(
2114    layer: &ModelStyleLayer,
2115    sources: &HashMap<StyleSourceId, StyleSource>,
2116    ctx: StyleEvalContext,
2117) -> Result<Box<dyn Layer>, StyleError> {
2118    let model = require_model_source(&layer.meta.id, &layer.source, sources)?;
2119    let mut runtime = ModelLayer::new(layer.meta.name.clone())
2120        .with_query_metadata(Some(layer.meta.id.clone()), Some(layer.source.clone()));
2121    apply_shared_meta(&mut runtime, &layer.meta, ctx);
2122    runtime.instances.extend(model.instances.iter().cloned());
2123    Ok(Box::new(runtime))
2124}
2125
2126fn apply_background_to_runtime(
2127    layer: &BackgroundStyleLayer,
2128    runtime: &mut dyn Layer,
2129    ctx: StyleEvalContext,
2130) -> Result<(), StyleError> {
2131    let background = runtime
2132        .as_any_mut()
2133        .downcast_mut::<BackgroundLayer>()
2134        .expect("style/runtime layer mismatch: expected BackgroundLayer");
2135    apply_shared_meta(background, &layer.meta, ctx);
2136    background.set_color(layer.color.evaluate_with_context(ctx));
2137    Ok(())
2138}
2139
2140fn apply_hillshade_to_runtime(
2141    layer: &HillshadeStyleLayer,
2142    runtime: &mut dyn Layer,
2143    ctx: StyleEvalContext,
2144) -> Result<(), StyleError> {
2145    let hillshade = runtime
2146        .as_any_mut()
2147        .downcast_mut::<HillshadeLayer>()
2148        .expect("style/runtime layer mismatch: expected HillshadeLayer");
2149    apply_shared_meta(hillshade, &layer.meta, ctx);
2150    hillshade.set_highlight_color(layer.highlight_color.evaluate_with_context(ctx));
2151    hillshade.set_shadow_color(layer.shadow_color.evaluate_with_context(ctx));
2152    hillshade.set_accent_color(layer.accent_color.evaluate_with_context(ctx));
2153    hillshade.set_illumination_direction_deg(layer.illumination_direction_deg.evaluate_with_context(ctx));
2154    hillshade.set_illumination_altitude_deg(layer.illumination_altitude_deg.evaluate_with_context(ctx));
2155    hillshade.set_exaggeration(layer.exaggeration.evaluate_with_context(ctx));
2156    Ok(())
2157}
2158
2159fn apply_raster_to_runtime(
2160    layer: &RasterStyleLayer,
2161    runtime: &mut dyn Layer,
2162    sources: &HashMap<StyleSourceId, StyleSource>,
2163    ctx: StyleEvalContext,
2164) -> Result<(), StyleError> {
2165    let _ = require_raster_source(&layer.meta.id, &layer.source, sources)?;
2166    let tile = runtime
2167        .as_any_mut()
2168        .downcast_mut::<TileLayer>()
2169        .expect("style/runtime layer mismatch: expected TileLayer");
2170    apply_shared_meta(tile, &layer.meta, ctx);
2171    Ok(())
2172}
2173
2174fn apply_vector_style_to_runtime(
2175    runtime: &mut dyn Layer,
2176    meta: &StyleLayerMeta,
2177    source_id: &str,
2178    source_layer: Option<&str>,
2179    layer_id: &str,
2180    style: VectorStyle,
2181    sources: &HashMap<StyleSourceId, StyleSource>,
2182    ctx: StyleEvalContext,
2183) -> Result<(), StyleError> {
2184    let vector = require_vector_source(layer_id, source_id, source_layer, sources, ctx.zoom as u8)?;
2185    let layer = runtime
2186        .as_any_mut()
2187        .downcast_mut::<VectorLayer>()
2188        .expect("style/runtime layer mismatch: expected VectorLayer");
2189    apply_shared_meta(layer, meta, ctx);
2190    layer.style = style;
2191    layer.set_query_metadata(Some(layer_id.to_owned()), Some(source_id.to_owned()));
2192    if layer.features.len() != vector.len() {
2193        layer.features = vector.into_owned();
2194    }
2195    Ok(())
2196}
2197
2198fn apply_vector_to_runtime(
2199    layer: &VectorStyleLayer,
2200    runtime: &mut dyn Layer,
2201    sources: &HashMap<StyleSourceId, StyleSource>,
2202    ctx: StyleEvalContext,
2203) -> Result<(), StyleError> {
2204    apply_vector_style_to_runtime(
2205        runtime,
2206        &layer.meta,
2207        &layer.source,
2208        layer.source_layer.as_deref(),
2209        &layer.meta.id,
2210        vector_style_from_vector_layer(layer, ctx),
2211        sources,
2212        ctx,
2213    )
2214}
2215
2216fn apply_fill_to_runtime(
2217    layer: &FillStyleLayer,
2218    runtime: &mut dyn Layer,
2219    sources: &HashMap<StyleSourceId, StyleSource>,
2220    ctx: StyleEvalContext,
2221) -> Result<(), StyleError> {
2222    apply_vector_style_to_runtime(
2223        runtime,
2224        &layer.meta,
2225        &layer.source,
2226        layer.source_layer.as_deref(),
2227        &layer.meta.id,
2228        vector_style_from_fill_layer(layer, ctx),
2229        sources,
2230        ctx,
2231    )
2232}
2233
2234fn apply_line_to_runtime(
2235    layer: &LineStyleLayer,
2236    runtime: &mut dyn Layer,
2237    sources: &HashMap<StyleSourceId, StyleSource>,
2238    ctx: StyleEvalContext,
2239) -> Result<(), StyleError> {
2240    apply_vector_style_to_runtime(
2241        runtime,
2242        &layer.meta,
2243        &layer.source,
2244        layer.source_layer.as_deref(),
2245        &layer.meta.id,
2246        vector_style_from_line_layer(layer, ctx),
2247        sources,
2248        ctx,
2249    )
2250}
2251
2252fn apply_circle_to_runtime(
2253    layer: &CircleStyleLayer,
2254    runtime: &mut dyn Layer,
2255    sources: &HashMap<StyleSourceId, StyleSource>,
2256    ctx: StyleEvalContext,
2257) -> Result<(), StyleError> {
2258    apply_vector_style_to_runtime(
2259        runtime,
2260        &layer.meta,
2261        &layer.source,
2262        layer.source_layer.as_deref(),
2263        &layer.meta.id,
2264        vector_style_from_circle_layer(layer, ctx),
2265        sources,
2266        ctx,
2267    )
2268}
2269
2270fn apply_heatmap_to_runtime(
2271    layer: &HeatmapStyleLayer,
2272    runtime: &mut dyn Layer,
2273    sources: &HashMap<StyleSourceId, StyleSource>,
2274    ctx: StyleEvalContext,
2275) -> Result<(), StyleError> {
2276    apply_vector_style_to_runtime(
2277        runtime,
2278        &layer.meta,
2279        &layer.source,
2280        layer.source_layer.as_deref(),
2281        &layer.meta.id,
2282        vector_style_from_heatmap_layer(layer, ctx),
2283        sources,
2284        ctx,
2285    )
2286}
2287
2288fn apply_fill_extrusion_to_runtime(
2289    layer: &FillExtrusionStyleLayer,
2290    runtime: &mut dyn Layer,
2291    sources: &HashMap<StyleSourceId, StyleSource>,
2292    ctx: StyleEvalContext,
2293) -> Result<(), StyleError> {
2294    apply_vector_style_to_runtime(
2295        runtime,
2296        &layer.meta,
2297        &layer.source,
2298        layer.source_layer.as_deref(),
2299        &layer.meta.id,
2300        vector_style_from_fill_extrusion_layer(layer, ctx),
2301        sources,
2302        ctx,
2303    )
2304}
2305
2306fn apply_symbol_to_runtime(
2307    layer: &SymbolStyleLayer,
2308    runtime: &mut dyn Layer,
2309    sources: &HashMap<StyleSourceId, StyleSource>,
2310    ctx: StyleEvalContext,
2311) -> Result<(), StyleError> {
2312    apply_vector_style_to_runtime(
2313        runtime,
2314        &layer.meta,
2315        &layer.source,
2316        layer.source_layer.as_deref(),
2317        &layer.meta.id,
2318        vector_style_from_symbol_layer(layer, ctx),
2319        sources,
2320        ctx,
2321    )
2322}
2323
2324fn apply_model_to_runtime(
2325    layer: &ModelStyleLayer,
2326    runtime: &mut dyn Layer,
2327    sources: &HashMap<StyleSourceId, StyleSource>,
2328    ctx: StyleEvalContext,
2329) -> Result<(), StyleError> {
2330    let model = require_model_source(&layer.meta.id, &layer.source, sources)?;
2331    let runtime = runtime
2332        .as_any_mut()
2333        .downcast_mut::<ModelLayer>()
2334        .expect("style/runtime layer mismatch: expected ModelLayer");
2335    apply_shared_meta(runtime, &layer.meta, ctx);
2336    runtime.set_query_metadata(Some(layer.meta.id.clone()), Some(layer.source.clone()));
2337    runtime.instances.clear();
2338    runtime.instances.extend(model.instances.iter().cloned());
2339    Ok(())
2340}
2341
2342fn vector_style_from_vector_layer(layer: &VectorStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
2343    VectorStyle {
2344        render_mode: VectorRenderMode::Generic,
2345        fill_color: layer.fill_color.evaluate_with_context(ctx),
2346        stroke_color: layer.stroke_color.evaluate_with_context(ctx),
2347        stroke_width: layer.stroke_width.evaluate_with_context(ctx),
2348        ..VectorStyle::default()
2349    }
2350}
2351
2352fn vector_style_from_fill_layer(layer: &FillStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
2353    let mut style = VectorStyle::fill(
2354        layer.fill_color.evaluate_with_context(ctx),
2355        layer.outline_color.evaluate_with_context(ctx),
2356        layer.outline_width.evaluate_with_context(ctx),
2357    );
2358    style.fill_pattern = layer.fill_pattern.clone();
2359    style
2360}
2361
2362fn vector_style_from_line_layer(layer: &LineStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
2363    let mut style = VectorStyle::line_styled(
2364        layer.color.evaluate_with_context(ctx),
2365        layer.width.evaluate_with_context(ctx),
2366        layer.line_cap,
2367        layer.line_join,
2368        layer.miter_limit,
2369        layer.dash_array.clone(),
2370    );
2371    // Carry the original expressions so tessellation can evaluate per feature.
2372    if layer.width.is_data_driven() {
2373        style.width_expr = Some(layer.width.clone());
2374    }
2375    if layer.color.is_data_driven() {
2376        style.stroke_color_expr = Some(layer.color.clone());
2377    }
2378    style.eval_zoom = ctx.zoom;
2379    style.line_gradient = layer.line_gradient.clone();
2380    style.line_pattern = layer.line_pattern.clone();
2381    style
2382}
2383
2384fn vector_style_from_circle_layer(layer: &CircleStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
2385    VectorStyle::circle(
2386        layer.color.evaluate_with_context(ctx),
2387        layer.radius.evaluate_with_context(ctx),
2388        layer.stroke_color.evaluate_with_context(ctx),
2389        layer.stroke_width.evaluate_with_context(ctx),
2390    )
2391}
2392
2393fn vector_style_from_heatmap_layer(layer: &HeatmapStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
2394    VectorStyle::heatmap(
2395        layer.color.evaluate_with_context(ctx),
2396        layer.radius.evaluate_with_context(ctx),
2397        layer.intensity.evaluate_with_context(ctx),
2398    )
2399}
2400
2401fn vector_style_from_fill_extrusion_layer(
2402    layer: &FillExtrusionStyleLayer,
2403    ctx: StyleEvalContext,
2404) -> VectorStyle {
2405    VectorStyle::fill_extrusion(
2406        layer.color.evaluate_with_context(ctx),
2407        layer.base.evaluate_with_context(ctx),
2408        layer.height.evaluate_with_context(ctx),
2409    )
2410}
2411
2412fn vector_style_from_symbol_layer(layer: &SymbolStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
2413    let mut style = VectorStyle::symbol(
2414        layer.color.evaluate_with_context(ctx),
2415        layer.halo_color.evaluate_with_context(ctx),
2416        layer.size.evaluate_with_context(ctx),
2417    );
2418    style.symbol_text_field = layer.text_field.as_ref().map(|value| value.evaluate_with_context(ctx));
2419    style.symbol_icon_image = layer.icon_image.as_ref().map(|value| value.evaluate_with_context(ctx));
2420    style.symbol_font_stack = layer.font_stack.evaluate_with_context(ctx);
2421    style.symbol_padding = layer.padding.evaluate_with_context(ctx);
2422    let shared_overlap = layer.allow_overlap.evaluate_with_context(ctx);
2423    style.symbol_allow_overlap = shared_overlap;
2424    style.symbol_text_allow_overlap = layer
2425        .text_allow_overlap
2426        .as_ref()
2427        .map(|value| value.evaluate_with_context(ctx))
2428        .unwrap_or(shared_overlap);
2429    style.symbol_icon_allow_overlap = layer
2430        .icon_allow_overlap
2431        .as_ref()
2432        .map(|value| value.evaluate_with_context(ctx))
2433        .unwrap_or(shared_overlap);
2434    style.symbol_text_optional = layer
2435        .text_optional
2436        .as_ref()
2437        .map(|value| value.evaluate_with_context(ctx))
2438        .unwrap_or(false);
2439    style.symbol_icon_optional = layer
2440        .icon_optional
2441        .as_ref()
2442        .map(|value| value.evaluate_with_context(ctx))
2443        .unwrap_or(false);
2444    style.symbol_text_ignore_placement = layer
2445        .text_ignore_placement
2446        .as_ref()
2447        .map(|value| value.evaluate_with_context(ctx))
2448        .unwrap_or(false);
2449    style.symbol_icon_ignore_placement = layer
2450        .icon_ignore_placement
2451        .as_ref()
2452        .map(|value| value.evaluate_with_context(ctx))
2453        .unwrap_or(false);
2454    style.symbol_text_radial_offset = layer
2455        .radial_offset
2456        .as_ref()
2457        .map(|value| value.evaluate_with_context(ctx));
2458    style.symbol_variable_anchor_offsets = layer.variable_anchor_offsets.clone();
2459    style.symbol_text_anchor = layer.anchor;
2460    style.symbol_text_justify = effective_symbol_text_justify(
2461        layer.justify.evaluate_with_context(ctx),
2462        layer.anchor,
2463    );
2464    style.symbol_text_transform = layer.transform.evaluate_with_context(ctx);
2465    style.symbol_text_max_width = layer
2466        .max_width
2467        .as_ref()
2468        .map(|value| value.evaluate_with_context(ctx));
2469    style.symbol_text_line_height = layer
2470        .line_height
2471        .as_ref()
2472        .map(|value| value.evaluate_with_context(ctx));
2473    style.symbol_text_letter_spacing = layer
2474        .letter_spacing
2475        .as_ref()
2476        .map(|value| value.evaluate_with_context(ctx));
2477    style.symbol_icon_text_fit = layer.icon_text_fit.evaluate_with_context(ctx);
2478    style.symbol_icon_text_fit_padding = layer.icon_text_fit_padding;
2479    style.symbol_sort_key = layer
2480        .sort_key
2481        .as_ref()
2482        .map(|value| value.evaluate_with_context(ctx));
2483    style.symbol_placement = layer.placement;
2484    style.symbol_spacing = layer.spacing.evaluate_with_context(ctx);
2485    style.symbol_max_angle = layer.max_angle.evaluate_with_context(ctx);
2486    style.symbol_keep_upright = layer.keep_upright.evaluate_with_context(ctx);
2487    style.symbol_anchors = effective_symbol_anchor_order(
2488        layer.anchor,
2489        &layer.variable_anchors,
2490        layer.variable_anchor_offsets.as_deref(),
2491    );
2492    style.symbol_writing_mode = layer.writing_mode;
2493    style.symbol_offset = layer.offset;
2494    style
2495}
2496
2497fn effective_symbol_anchor_order(
2498    anchor: SymbolAnchor,
2499    variable_anchors: &[SymbolAnchor],
2500    variable_anchor_offsets: Option<&[(SymbolAnchor, [f32; 2])]>,
2501) -> Vec<SymbolAnchor> {
2502    if let Some(anchor_offsets) = variable_anchor_offsets {
2503        return anchor_offsets.iter().map(|(anchor, _)| *anchor).collect();
2504    }
2505    if variable_anchors.is_empty() {
2506        vec![anchor]
2507    } else {
2508        variable_anchors.to_vec()
2509    }
2510}
2511
2512// -------------------------------------------------------------------------
2513// Full-context variants for feature-state-driven paint resolution.
2514//
2515// Each function mirrors its `vector_style_from_*` counterpart but accepts a
2516// `StyleEvalContextFull` and calls `evaluate_with_full_context` so that
2517// `StyleValue::FeatureState` variants resolve against the per-feature state
2518// map instead of always returning the fallback.
2519//
2520// These are used by the public `resolve_style_with_feature_state` entry
2521// point when the engine needs to re-evaluate a layer's paint properties for
2522// a single feature whose state changed (e.g. hover on / hover off).
2523// -------------------------------------------------------------------------
2524
2525/// Resolve a [`FillStyleLayer`]'s paint properties with per-feature state.
2526pub fn fill_style_with_state(layer: &FillStyleLayer, ctx: &StyleEvalContextFull<'_>) -> VectorStyle {
2527    let mut style = VectorStyle::fill(
2528        layer.fill_color.evaluate_with_full_context(ctx),
2529        layer.outline_color.evaluate_with_full_context(ctx),
2530        layer.outline_width.evaluate_with_full_context(ctx),
2531    );
2532    style.fill_pattern = layer.fill_pattern.clone();
2533    style
2534}
2535
2536/// Resolve a [`LineStyleLayer`]'s paint properties with per-feature state.
2537pub fn line_style_with_state(layer: &LineStyleLayer, ctx: &StyleEvalContextFull<'_>) -> VectorStyle {
2538    let mut style = VectorStyle::line_styled(
2539        layer.color.evaluate_with_full_context(ctx),
2540        layer.width.evaluate_with_full_context(ctx),
2541        layer.line_cap,
2542        layer.line_join,
2543        layer.miter_limit,
2544        layer.dash_array.clone(),
2545    );
2546    style.line_gradient = layer.line_gradient.clone();
2547    style.line_pattern = layer.line_pattern.clone();
2548    style
2549}
2550
2551/// Resolve a [`CircleStyleLayer`]'s paint properties with per-feature state.
2552pub fn circle_style_with_state(layer: &CircleStyleLayer, ctx: &StyleEvalContextFull<'_>) -> VectorStyle {
2553    VectorStyle::circle(
2554        layer.color.evaluate_with_full_context(ctx),
2555        layer.radius.evaluate_with_full_context(ctx),
2556        layer.stroke_color.evaluate_with_full_context(ctx),
2557        layer.stroke_width.evaluate_with_full_context(ctx),
2558    )
2559}
2560
2561/// Resolve a [`HeatmapStyleLayer`]'s paint properties with per-feature state.
2562pub fn heatmap_style_with_state(layer: &HeatmapStyleLayer, ctx: &StyleEvalContextFull<'_>) -> VectorStyle {
2563    VectorStyle::heatmap(
2564        layer.color.evaluate_with_full_context(ctx),
2565        layer.radius.evaluate_with_full_context(ctx),
2566        layer.intensity.evaluate_with_full_context(ctx),
2567    )
2568}
2569
2570/// Resolve a [`FillExtrusionStyleLayer`]'s paint properties with per-feature state.
2571pub fn fill_extrusion_style_with_state(
2572    layer: &FillExtrusionStyleLayer,
2573    ctx: &StyleEvalContextFull<'_>,
2574) -> VectorStyle {
2575    VectorStyle::fill_extrusion(
2576        layer.color.evaluate_with_full_context(ctx),
2577        layer.base.evaluate_with_full_context(ctx),
2578        layer.height.evaluate_with_full_context(ctx),
2579    )
2580}
2581
2582/// Resolve a [`VectorStyleLayer`]'s paint properties with per-feature state.
2583pub fn vector_style_with_state(layer: &VectorStyleLayer, ctx: &StyleEvalContextFull<'_>) -> VectorStyle {
2584    VectorStyle {
2585        render_mode: VectorRenderMode::Generic,
2586        fill_color: layer.fill_color.evaluate_with_full_context(ctx),
2587        stroke_color: layer.stroke_color.evaluate_with_full_context(ctx),
2588        stroke_width: layer.stroke_width.evaluate_with_full_context(ctx),
2589        ..VectorStyle::default()
2590    }
2591}
2592
2593/// Resolve a [`SymbolStyleLayer`]'s paint properties with per-feature state.
2594///
2595/// Layout properties that do not have `FeatureState` variants (anchors,
2596/// placement mode, writing mode, etc.) resolve identically to the zoom-only
2597/// path and are included for completeness.
2598pub fn symbol_style_with_state(layer: &SymbolStyleLayer, ctx: &StyleEvalContextFull<'_>) -> VectorStyle {
2599    let base = ctx.to_base();
2600    let mut style = VectorStyle::symbol(
2601        layer.color.evaluate_with_full_context(ctx),
2602        layer.halo_color.evaluate_with_full_context(ctx),
2603        layer.size.evaluate_with_full_context(ctx),
2604    );
2605    // Text / icon fields -- use full context so feature-state can toggle
2606    // text-field or icon-image dynamically if desired.
2607    style.symbol_text_field = layer.text_field.as_ref().map(|v| v.evaluate_with_full_context(ctx));
2608    style.symbol_icon_image = layer.icon_image.as_ref().map(|v| v.evaluate_with_full_context(ctx));
2609    style.symbol_font_stack = layer.font_stack.evaluate_with_full_context(ctx);
2610    style.symbol_padding = layer.padding.evaluate_with_full_context(ctx);
2611    let shared_overlap = layer.allow_overlap.evaluate_with_full_context(ctx);
2612    style.symbol_allow_overlap = shared_overlap;
2613    style.symbol_text_allow_overlap = layer
2614        .text_allow_overlap.as_ref()
2615        .map(|v| v.evaluate_with_full_context(ctx))
2616        .unwrap_or(shared_overlap);
2617    style.symbol_icon_allow_overlap = layer
2618        .icon_allow_overlap.as_ref()
2619        .map(|v| v.evaluate_with_full_context(ctx))
2620        .unwrap_or(shared_overlap);
2621    style.symbol_text_optional = layer
2622        .text_optional.as_ref()
2623        .map(|v| v.evaluate_with_full_context(ctx))
2624        .unwrap_or(false);
2625    style.symbol_icon_optional = layer
2626        .icon_optional.as_ref()
2627        .map(|v| v.evaluate_with_full_context(ctx))
2628        .unwrap_or(false);
2629    style.symbol_text_ignore_placement = layer
2630        .text_ignore_placement.as_ref()
2631        .map(|v| v.evaluate_with_full_context(ctx))
2632        .unwrap_or(false);
2633    style.symbol_icon_ignore_placement = layer
2634        .icon_ignore_placement.as_ref()
2635        .map(|v| v.evaluate_with_full_context(ctx))
2636        .unwrap_or(false);
2637    style.symbol_text_radial_offset = layer
2638        .radial_offset.as_ref()
2639        .map(|v| v.evaluate_with_full_context(ctx));
2640    style.symbol_variable_anchor_offsets = layer.variable_anchor_offsets.clone();
2641    style.symbol_text_anchor = layer.anchor;
2642    style.symbol_text_justify = effective_symbol_text_justify(
2643        layer.justify.evaluate_with_context(base),
2644        layer.anchor,
2645    );
2646    style.symbol_text_transform = layer.transform.evaluate_with_full_context(ctx);
2647    style.symbol_text_max_width = layer
2648        .max_width.as_ref()
2649        .map(|v| v.evaluate_with_full_context(ctx));
2650    style.symbol_text_line_height = layer
2651        .line_height.as_ref()
2652        .map(|v| v.evaluate_with_full_context(ctx));
2653    style.symbol_text_letter_spacing = layer
2654        .letter_spacing.as_ref()
2655        .map(|v| v.evaluate_with_full_context(ctx));
2656    style.symbol_icon_text_fit = layer.icon_text_fit.evaluate_with_full_context(ctx);
2657    style.symbol_icon_text_fit_padding = layer.icon_text_fit_padding;
2658    style.symbol_sort_key = layer
2659        .sort_key.as_ref()
2660        .map(|v| v.evaluate_with_full_context(ctx));
2661    style.symbol_placement = layer.placement;
2662    style.symbol_spacing = layer.spacing.evaluate_with_full_context(ctx);
2663    style.symbol_max_angle = layer.max_angle.evaluate_with_full_context(ctx);
2664    style.symbol_keep_upright = layer.keep_upright.evaluate_with_full_context(ctx);
2665    style.symbol_anchors = effective_symbol_anchor_order(
2666        layer.anchor,
2667        &layer.variable_anchors,
2668        layer.variable_anchor_offsets.as_deref(),
2669    );
2670    style.symbol_writing_mode = layer.writing_mode;
2671    style.symbol_offset = layer.offset;
2672    style
2673}
2674
2675// MapLibre treats `text-justify: auto` as "match the effective anchor" for
2676// point labels. The engine does not yet run full text shaping, but deriving the
2677// effective justification here preserves the requested style state for future
2678// shaping work and keeps it consistent with the chosen anchor defaults.
2679fn effective_symbol_text_justify(
2680    justify: SymbolTextJustify,
2681    anchor: SymbolAnchor,
2682) -> SymbolTextJustify {
2683    match justify {
2684        SymbolTextJustify::Auto => anchor_justification(anchor),
2685        explicit => explicit,
2686    }
2687}
2688
2689fn anchor_justification(anchor: SymbolAnchor) -> SymbolTextJustify {
2690    match anchor {
2691        SymbolAnchor::Left | SymbolAnchor::TopLeft | SymbolAnchor::BottomLeft => {
2692            SymbolTextJustify::Left
2693        }
2694        SymbolAnchor::Right | SymbolAnchor::TopRight | SymbolAnchor::BottomRight => {
2695            SymbolTextJustify::Right
2696        }
2697        _ => SymbolTextJustify::Center,
2698    }
2699}
2700
2701/// Mutable runtime wrapper around a [`StyleDocument`].
2702#[derive(Debug, Default)]
2703pub struct MapStyle {
2704    document: StyleDocument,
2705}
2706
2707impl MapStyle {
2708    /// Create an empty map style.
2709    pub fn new() -> Self {
2710        Self::default()
2711    }
2712
2713    /// Wrap an existing style document.
2714    pub fn from_document(document: StyleDocument) -> Self {
2715        Self { document }
2716    }
2717
2718    /// Access the underlying document.
2719    pub fn document(&self) -> &StyleDocument {
2720        &self.document
2721    }
2722
2723    /// Mutable access to the underlying document.
2724    pub fn document_mut(&mut self) -> &mut StyleDocument {
2725        &mut self.document
2726    }
2727
2728    /// Consume and return the underlying document.
2729    pub fn into_document(self) -> StyleDocument {
2730        self.document
2731    }
2732}
2733
2734#[cfg(test)]
2735mod tests {
2736    use super::*;
2737    use crate::geometry::{Feature, FeatureCollection, Geometry, Point};
2738    use crate::tile_source::{TileError, TileResponse, TileSource};
2739    use std::collections::HashMap;
2740
2741    struct EmptyTileSource;
2742
2743    impl TileSource for EmptyTileSource {
2744        fn request(&self, _id: rustial_math::TileId) {}
2745
2746        fn poll(&self) -> Vec<(rustial_math::TileId, Result<TileResponse, TileError>)> {
2747            Vec::new()
2748        }
2749    }
2750
2751    fn feature_at(lat: f64, lon: f64) -> Feature {
2752        Feature {
2753            geometry: Geometry::Point(Point {
2754                coord: GeoCoord::from_lat_lon(lat, lon),
2755            }),
2756            properties: HashMap::new(),
2757        }
2758    }
2759
2760    fn collection_with_point(lat: f64, lon: f64) -> FeatureCollection {
2761        FeatureCollection {
2762            features: vec![feature_at(lat, lon)],
2763        }
2764    }
2765
2766    #[test]
2767    fn vector_tile_source_can_be_partitioned_by_source_layer() {
2768        let mut source_layers = HashMap::new();
2769        source_layers.insert("roads".to_string(), collection_with_point(1.0, 2.0));
2770        source_layers.insert("water".to_string(), collection_with_point(3.0, 4.0));
2771
2772        let source = VectorTileSource::from_source_layers(source_layers);
2773        assert!(source.has_source_layers());
2774        assert_eq!(source.source_layer("roads").map(|fc| fc.len()), Some(1));
2775        assert_eq!(source.source_layer("water").map(|fc| fc.len()), Some(1));
2776        assert_eq!(source.data.len(), 2);
2777    }
2778
2779    #[test]
2780    fn vector_style_layer_resolves_requested_source_layer() {
2781        let mut document = StyleDocument::new();
2782        let mut source_layers = HashMap::new();
2783        source_layers.insert("roads".to_string(), collection_with_point(10.0, 20.0));
2784        source_layers.insert("water".to_string(), collection_with_point(30.0, 40.0));
2785        document
2786            .add_source(
2787                "vector",
2788                StyleSource::VectorTile(VectorTileSource::from_source_layers(source_layers)),
2789            )
2790            .expect("source added");
2791
2792        let mut layer = LineStyleLayer::new("roads-line", "vector");
2793        layer.source_layer = Some("roads".to_string());
2794        document
2795            .add_layer(StyleLayer::Line(layer))
2796            .expect("layer added");
2797
2798        let runtime = document.to_runtime_layers().expect("runtime layers");
2799        let vector = runtime[0]
2800            .as_any()
2801            .downcast_ref::<VectorLayer>()
2802            .expect("vector runtime layer");
2803        assert_eq!(vector.features.len(), 1);
2804        match &vector.features.features[0].geometry {
2805            Geometry::Point(point) => {
2806                assert!((point.coord.lat - 10.0).abs() < 1e-9);
2807                assert!((point.coord.lon - 20.0).abs() < 1e-9);
2808            }
2809            other => panic!("expected point geometry, got {other:?}"),
2810        }
2811    }
2812
2813    #[test]
2814    fn missing_source_layer_returns_style_error() {
2815        let mut document = StyleDocument::new();
2816        document
2817            .add_source(
2818                "vector",
2819                StyleSource::VectorTile(VectorTileSource::new(collection_with_point(0.0, 0.0))),
2820            )
2821            .expect("source added");
2822
2823        let mut layer = FillStyleLayer::new("water-fill", "vector");
2824        layer.source_layer = Some("water".to_string());
2825        document
2826            .add_layer(StyleLayer::Fill(layer))
2827            .expect("layer added");
2828
2829        let err = document.to_runtime_layers().expect_err("missing source-layer should fail");
2830        assert!(matches!(err, StyleError::MissingSourceLayer { .. }));
2831    }
2832
2833    #[test]
2834    fn streamed_vector_source_allows_runtime_layer_creation_without_resolved_features() {
2835        let mut document = StyleDocument::new();
2836        document
2837            .add_source(
2838                "vector",
2839                StyleSource::VectorTile(
2840                    VectorTileSource::streamed(|| Box::new(EmptyTileSource))
2841                        .with_cache_capacity(8),
2842                ),
2843            )
2844            .expect("source added");
2845
2846        let mut layer = CircleStyleLayer::new("labels", "vector");
2847        layer.source_layer = Some("poi".to_string());
2848        document
2849            .add_layer(StyleLayer::Circle(layer))
2850            .expect("layer added");
2851
2852        let runtime = document.to_runtime_layers().expect("runtime layers");
2853        let vector = runtime[0]
2854            .as_any()
2855            .downcast_ref::<VectorLayer>()
2856            .expect("vector runtime layer");
2857        assert!(vector.features.is_empty());
2858        assert_eq!(vector.query_source_layer.as_deref(), Some("poi"));
2859    }
2860
2861    #[test]
2862    fn style_document_reports_source_usage() {
2863        let mut document = StyleDocument::new();
2864        document
2865            .add_source("places", StyleSource::GeoJson(GeoJsonSource::new(collection_with_point(0.0, 0.0))))
2866            .expect("source added");
2867        document
2868            .add_source(
2869                "labels",
2870                StyleSource::VectorTile(VectorTileSource::new(collection_with_point(1.0, 1.0))),
2871            )
2872            .expect("source added");
2873        document.set_terrain_source(Some("labels"));
2874
2875        document
2876            .add_layer(StyleLayer::Fill(FillStyleLayer::new("fill", "places")))
2877            .expect("fill layer added");
2878        document
2879            .add_layer(StyleLayer::Line(LineStyleLayer::new("line", "labels")))
2880            .expect("line layer added");
2881
2882        assert!(document.source_is_used("places"));
2883        assert!(document.source_is_used("labels"));
2884        assert!(!document.source_is_used("missing"));
2885
2886        let layer_ids = document.layer_ids_using_source("labels");
2887        assert_eq!(layer_ids, vec!["line"]);
2888    }
2889
2890    // -----------------------------------------------------------------------
2891    // Feature-state-driven style evaluation tests
2892    // -----------------------------------------------------------------------
2893
2894    #[test]
2895    fn feature_state_value_returns_fallback_with_zoom_only_context() {
2896        // When evaluating a FeatureState value without a full context, the
2897        // fallback is returned because there is no feature-state map to query.
2898        let value = StyleValue::<f32>::feature_state_key("opacity", 0.5);
2899        let result = value.evaluate_with_context(StyleEvalContext::new(10.0));
2900        assert!((result - 0.5).abs() < f32::EPSILON);
2901    }
2902
2903    #[test]
2904    fn feature_state_value_resolves_with_full_context() {
2905        // When the feature-state map contains the requested key, the stored
2906        // PropertyValue is converted to the target type (f32 in this case).
2907        let mut state = HashMap::new();
2908        state.insert("opacity".to_string(), PropertyValue::Number(0.8));
2909
2910        let value = StyleValue::<f32>::feature_state_key("opacity", 0.5);
2911        let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
2912        let result = value.evaluate_with_full_context(&ctx);
2913        assert!((result - 0.8).abs() < f32::EPSILON);
2914    }
2915
2916    #[test]
2917    fn feature_state_value_falls_back_when_key_absent() {
2918        // When the feature-state map does not contain the requested key,
2919        // the fallback value is used.
2920        let state: FeatureState = HashMap::new();
2921
2922        let value = StyleValue::<f32>::feature_state_key("opacity", 0.5);
2923        let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
2924        let result = value.evaluate_with_full_context(&ctx);
2925        assert!((result - 0.5).abs() < f32::EPSILON);
2926    }
2927
2928    #[test]
2929    fn feature_state_bool_resolves_hover_flag() {
2930        // The most common Mapbox pattern: a boolean "hover" flag toggled
2931        // by the interaction manager.
2932        let mut state = HashMap::new();
2933        state.insert("hover".to_string(), PropertyValue::Bool(true));
2934
2935        let value = StyleValue::<bool>::feature_state_key("hover", false);
2936        let ctx = StyleEvalContext::new(14.0).with_feature_state(&state);
2937        assert!(value.evaluate_with_full_context(&ctx));
2938    }
2939
2940    #[test]
2941    fn feature_state_color_array_always_returns_fallback() {
2942        // Colour tuples ([f32; 4]) cannot be expressed in a single
2943        // PropertyValue today, so the fallback is always used.
2944        let mut state = HashMap::new();
2945        state.insert("color".to_string(), PropertyValue::Number(1.0));
2946
2947        let fallback = [0.1, 0.2, 0.3, 1.0];
2948        let value = StyleValue::<[f32; 4]>::feature_state_key("color", fallback);
2949        let ctx = StyleEvalContext::new(14.0).with_feature_state(&state);
2950        assert_eq!(value.evaluate_with_full_context(&ctx), fallback);
2951    }
2952
2953    #[test]
2954    fn is_feature_state_driven_flag() {
2955        let constant: StyleValue<f32> = StyleValue::Constant(1.0);
2956        assert!(!constant.is_feature_state_driven());
2957
2958        let driven: StyleValue<f32> = StyleValue::feature_state_key("opacity", 1.0);
2959        assert!(driven.is_feature_state_driven());
2960    }
2961
2962    #[test]
2963    fn constant_and_zoom_stops_unchanged_with_full_context() {
2964        // Existing Constant and ZoomStops variants must continue to work
2965        // identically when evaluated through the full-context path.
2966        let state: FeatureState = HashMap::new();
2967        let ctx = StyleEvalContext::new(5.0).with_feature_state(&state);
2968
2969        let constant = StyleValue::Constant(42.0_f32);
2970        assert!((constant.evaluate_with_full_context(&ctx) - 42.0).abs() < f32::EPSILON);
2971
2972        let stops = StyleValue::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
2973        let result = stops.evaluate_with_full_context(&ctx);
2974        assert!((result - 50.0).abs() < f32::EPSILON);
2975    }
2976
2977    #[test]
2978    fn full_context_helpers_return_expected_values() {
2979        let mut state = HashMap::new();
2980        state.insert("hover".to_string(), PropertyValue::Bool(true));
2981        state.insert("width".to_string(), PropertyValue::Number(3.5));
2982
2983        let ctx = StyleEvalContextFull::new(14.0, &state);
2984        assert!(ctx.feature_state_bool("hover"));
2985        assert!(!ctx.feature_state_bool("missing"));
2986        assert!((ctx.feature_state_f64("width", 1.0) - 3.5).abs() < f64::EPSILON);
2987        assert!((ctx.feature_state_f64("missing", 1.0) - 1.0).abs() < f64::EPSILON);
2988    }
2989
2990    // -----------------------------------------------------------------------
2991    // Full-context style resolution and has_feature_state_driven_paint tests
2992    // -----------------------------------------------------------------------
2993
2994    #[test]
2995    fn fill_layer_resolves_with_feature_state() {
2996        // A fill layer whose fill_color depends on feature-state("hover")
2997        // should resolve to the fallback when hover is false, and to the
2998        // state-provided value when hover triggers a numeric override.
2999        let mut layer = FillStyleLayer::new("buildings", "source");
3000        // Use a feature-state-driven outline width so we can verify resolution.
3001        layer.outline_width = StyleValue::feature_state_key("width", 1.0);
3002
3003        // Without hover state: fallback (1.0).
3004        let empty_state: FeatureState = HashMap::new();
3005        let ctx = StyleEvalContext::new(14.0).with_feature_state(&empty_state);
3006        let style = fill_style_with_state(&layer, &ctx);
3007        assert!((style.stroke_width - 1.0).abs() < f32::EPSILON);
3008
3009        // With hover state: overridden value (4.0).
3010        let mut hover_state = HashMap::new();
3011        hover_state.insert("width".to_string(), PropertyValue::Number(4.0));
3012        let ctx = StyleEvalContext::new(14.0).with_feature_state(&hover_state);
3013        let style = fill_style_with_state(&layer, &ctx);
3014        assert!((style.stroke_width - 4.0).abs() < f32::EPSILON);
3015    }
3016
3017    #[test]
3018    fn line_layer_resolves_with_feature_state() {
3019        let mut layer = LineStyleLayer::new("roads", "source");
3020        layer.width = StyleValue::feature_state_key("highlight_width", 2.0);
3021
3022        let mut state = HashMap::new();
3023        state.insert("highlight_width".to_string(), PropertyValue::Number(6.0));
3024        let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
3025        let style = line_style_with_state(&layer, &ctx);
3026        assert!((style.stroke_width - 6.0).abs() < f32::EPSILON);
3027    }
3028
3029    #[test]
3030    fn circle_layer_resolves_with_feature_state() {
3031        let mut layer = CircleStyleLayer::new("points", "source");
3032        layer.radius = StyleValue::feature_state_key("size", 5.0);
3033
3034        let mut state = HashMap::new();
3035        state.insert("size".to_string(), PropertyValue::Number(12.0));
3036        let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
3037        let style = circle_style_with_state(&layer, &ctx);
3038        assert!((style.point_radius - 12.0).abs() < f32::EPSILON);
3039    }
3040
3041    #[test]
3042    fn has_feature_state_driven_paint_detects_driven_fields() {
3043        let mut fill = FillStyleLayer::new("buildings", "source");
3044        let fill_layer = StyleLayer::Fill(fill.clone());
3045        assert!(!fill_layer.has_feature_state_driven_paint());
3046
3047        // Make outline_width feature-state-driven.
3048        fill.outline_width = StyleValue::feature_state_key("width", 1.0);
3049        let fill_layer = StyleLayer::Fill(fill);
3050        assert!(fill_layer.has_feature_state_driven_paint());
3051    }
3052
3053    #[test]
3054    fn has_feature_state_driven_paint_false_for_non_vector_layers() {
3055        let bg = BackgroundStyleLayer::new("bg", [0.0, 0.0, 0.0, 1.0]);
3056        assert!(!StyleLayer::Background(bg).has_feature_state_driven_paint());
3057    }
3058
3059    #[test]
3060    fn resolve_style_with_feature_state_returns_none_for_background() {
3061        let bg = BackgroundStyleLayer::new("bg", [0.0, 0.0, 0.0, 1.0]);
3062        let state: FeatureState = HashMap::new();
3063        let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
3064        assert!(StyleLayer::Background(bg).resolve_style_with_feature_state(&ctx).is_none());
3065    }
3066
3067    #[test]
3068    fn resolve_style_with_feature_state_dispatches_fill() {
3069        let mut fill = FillStyleLayer::new("buildings", "source");
3070        fill.outline_width = StyleValue::feature_state_key("width", 1.0);
3071
3072        let mut state = HashMap::new();
3073        state.insert("width".to_string(), PropertyValue::Number(5.0));
3074        let ctx = StyleEvalContext::new(14.0).with_feature_state(&state);
3075
3076        let style = StyleLayer::Fill(fill)
3077            .resolve_style_with_feature_state(&ctx)
3078            .expect("fill layer should produce VectorStyle");
3079        assert!((style.stroke_width - 5.0).abs() < f32::EPSILON);
3080    }
3081
3082    #[test]
3083    fn non_driven_fields_unchanged_through_full_context() {
3084        // When a fill layer has only constant paint values, the full-context
3085        // evaluation should produce the same results as the zoom-only path.
3086        let layer = FillStyleLayer::new("buildings", "source");
3087        let state: FeatureState = HashMap::new();
3088        let full_ctx = StyleEvalContext::new(14.0).with_feature_state(&state);
3089        let zoom_ctx = StyleEvalContext::new(14.0);
3090
3091        let via_full = fill_style_with_state(&layer, &full_ctx);
3092        let via_zoom = vector_style_from_fill_layer(&layer, zoom_ctx);
3093        assert_eq!(via_full.fill_color, via_zoom.fill_color);
3094        assert_eq!(via_full.stroke_color, via_zoom.stroke_color);
3095        assert!((via_full.stroke_width - via_zoom.stroke_width).abs() < f32::EPSILON);
3096    }
3097
3098    // -----------------------------------------------------------------------
3099    // GeoJSON source clustering integration tests
3100    // -----------------------------------------------------------------------
3101
3102    #[test]
3103    fn geojson_source_with_clustering_returns_clustered_features_at_low_zoom() {
3104        // Place 10 tightly-packed points — they should cluster at low zoom.
3105        let features = FeatureCollection {
3106            features: (0..10)
3107                .map(|i| feature_at(48.858 + i as f64 * 0.0001, 2.294))
3108                .collect(),
3109        };
3110        let source = GeoJsonSource::new(features).with_clustering(ClusterOptions {
3111            radius: 80.0,
3112            max_zoom: 16,
3113            min_points: 2,
3114            ..Default::default()
3115        });
3116        assert!(source.is_clustered());
3117
3118        // At zoom 2, all 10 points should merge into a single cluster.
3119        let clustered = source.features_at_zoom(2);
3120        assert!(
3121            clustered.len() < 10,
3122            "Expected fewer features at zoom 2 (got {})",
3123            clustered.len(),
3124        );
3125
3126        // At zoom 20 (above max_zoom), all originals should be returned.
3127        let unclustered = source.features_at_zoom(20);
3128        assert_eq!(unclustered.len(), 10);
3129    }
3130
3131    #[test]
3132    fn geojson_source_without_clustering_returns_raw_data() {
3133        let source = GeoJsonSource::new(FeatureCollection {
3134            features: vec![feature_at(51.5, -0.12), feature_at(51.51, -0.13)],
3135        });
3136        assert!(!source.is_clustered());
3137        let result = source.features_at_zoom(5);
3138        assert_eq!(result.len(), 2);
3139    }
3140
3141    #[test]
3142    fn clustered_geojson_circle_layer_resolves_to_runtime_layer() {
3143        let features = FeatureCollection {
3144            features: (0..20)
3145                .map(|i| feature_at(48.858 + i as f64 * 0.0001, 2.294))
3146                .collect(),
3147        };
3148        let source = GeoJsonSource::new(features).with_clustering(Default::default());
3149
3150        let mut doc = StyleDocument::new();
3151        doc.add_source("points", StyleSource::GeoJson(source))
3152            .expect("source added");
3153        doc.add_layer(StyleLayer::Circle(CircleStyleLayer::new("dots", "points")))
3154            .expect("layer added");
3155
3156        // Evaluate at zoom 3 — should produce a runtime layer with clustered features.
3157        let ctx = StyleEvalContext::new(3.0);
3158        let layers = doc.to_runtime_layers_with_context(ctx).expect("layers ok");
3159        assert_eq!(layers.len(), 1, "expected 1 circle layer");
3160    }
3161
3162    #[test]
3163    fn video_source_produces_dynamic_image_overlay_layer() {
3164        use crate::layers::{FrameData, FrameProvider};
3165
3166        struct TestProvider;
3167        impl FrameProvider for TestProvider {
3168            fn next_frame(&mut self) -> Option<FrameData> {
3169                Some(FrameData {
3170                    width: 4,
3171                    height: 4,
3172                    data: vec![255; 64],
3173                })
3174            }
3175        }
3176
3177        let corners = [
3178            GeoCoord::from_lat_lon(40.0, -74.0),
3179            GeoCoord::from_lat_lon(40.0, -73.0),
3180            GeoCoord::from_lat_lon(39.0, -73.0),
3181            GeoCoord::from_lat_lon(39.0, -74.0),
3182        ];
3183        let source = VideoSource::new(corners, || Box::new(TestProvider));
3184
3185        let mut doc = StyleDocument::new();
3186        doc.add_source("video", StyleSource::Video(source))
3187            .expect("source added");
3188        doc.add_layer(StyleLayer::Raster(RasterStyleLayer::new(
3189            "video-layer",
3190            "video",
3191        )))
3192        .expect("layer added");
3193
3194        let layers = doc.to_runtime_layers().expect("runtime layers");
3195        assert_eq!(layers.len(), 1);
3196        assert!(
3197            layers[0].as_any().downcast_ref::<DynamicImageOverlayLayer>().is_some(),
3198            "video source should produce a DynamicImageOverlayLayer"
3199        );
3200    }
3201
3202    #[test]
3203    fn canvas_source_produces_dynamic_image_overlay_layer() {
3204        use crate::layers::{FrameData, FrameProvider};
3205
3206        struct StaticCanvas;
3207        impl FrameProvider for StaticCanvas {
3208            fn next_frame(&mut self) -> Option<FrameData> {
3209                Some(FrameData {
3210                    width: 8,
3211                    height: 8,
3212                    data: vec![128; 256],
3213                })
3214            }
3215            fn is_animating(&self) -> bool {
3216                false
3217            }
3218        }
3219
3220        let corners = [
3221            GeoCoord::from_lat_lon(51.0, -1.0),
3222            GeoCoord::from_lat_lon(51.0, 0.0),
3223            GeoCoord::from_lat_lon(50.0, 0.0),
3224            GeoCoord::from_lat_lon(50.0, -1.0),
3225        ];
3226        let source = CanvasSource::new(corners, || Box::new(StaticCanvas)).with_animate(false);
3227
3228        let mut doc = StyleDocument::new();
3229        doc.add_source("canvas", StyleSource::Canvas(source))
3230            .expect("source added");
3231        doc.add_layer(StyleLayer::Raster(RasterStyleLayer::new(
3232            "canvas-layer",
3233            "canvas",
3234        )))
3235        .expect("layer added");
3236
3237        let layers = doc.to_runtime_layers().expect("runtime layers");
3238        assert_eq!(layers.len(), 1);
3239        let dyn_layer = layers[0]
3240            .as_any()
3241            .downcast_ref::<DynamicImageOverlayLayer>()
3242            .expect("canvas source should produce a DynamicImageOverlayLayer");
3243        // Canvas was set to non-animating.
3244        assert!(!dyn_layer.provider().is_animating());
3245    }
3246}
3247
3248fn apply_shared_meta(layer: &mut dyn Layer, meta: &StyleLayerMeta, ctx: StyleEvalContext) {
3249    layer.set_visible(meta.visible_in_context(ctx));
3250    layer.set_opacity(meta.opacity.evaluate_with_context(ctx));
3251}
3252
3253fn require_raster_source<'a>(
3254    layer_id: &str,
3255    source_id: &str,
3256    sources: &'a HashMap<StyleSourceId, StyleSource>,
3257) -> Result<(&'a RasterSourceFactory, usize, &'a TileSelectionConfig), StyleError> {
3258    let Some(source) = sources.get(source_id) else {
3259        return Err(StyleError::MissingSource(source_id.to_owned()));
3260    };
3261    match source {
3262        StyleSource::Raster(raster) => Ok((&raster.factory, raster.cache_capacity, &raster.selection)),
3263        StyleSource::Image(image) => Ok((&image.factory, image.cache_capacity, &image.selection)),
3264        other => Err(StyleError::SourceKindMismatch {
3265            layer_id: layer_id.to_owned(),
3266            source_id: source_id.to_owned(),
3267            expected: "raster|image",
3268            actual: other.kind_name(),
3269        }),
3270    }
3271}
3272
3273/// Try to resolve a video or canvas source for a raster-type style layer.
3274///
3275/// Returns `Some(DynamicImageOverlayLayer)` if the referenced source is a
3276/// `Video` or `Canvas` variant.  Returns `None` if the source is a normal
3277/// raster/image source (caller should fall through to `require_raster_source`).
3278fn try_dynamic_overlay_from_source(
3279    layer_name: &str,
3280    source_id: &str,
3281    sources: &HashMap<StyleSourceId, StyleSource>,
3282    ctx: StyleEvalContext,
3283) -> Option<Result<Box<dyn Layer>, StyleError>> {
3284    let source = sources.get(source_id)?;
3285    match source {
3286        StyleSource::Video(video) => {
3287            let provider = (video.factory)();
3288            let mut layer = DynamicImageOverlayLayer::new(
3289                layer_name.to_owned(),
3290                video.coordinates,
3291                provider,
3292            );
3293            layer.set_opacity(ctx.zoom.fract() as f32); // placeholder; real opacity comes from apply_shared_meta
3294            Some(Ok(Box::new(layer)))
3295        }
3296        StyleSource::Canvas(canvas) => {
3297            let provider = (canvas.factory)();
3298            let mut layer = DynamicImageOverlayLayer::new(
3299                layer_name.to_owned(),
3300                canvas.coordinates,
3301                provider,
3302            );
3303            layer.set_opacity(ctx.zoom.fract() as f32);
3304            Some(Ok(Box::new(layer)))
3305        }
3306        _ => None,
3307    }
3308}
3309
3310fn require_vector_source<'a>(
3311    layer_id: &str,
3312    source_id: &str,
3313    source_layer: Option<&str>,
3314    sources: &'a HashMap<StyleSourceId, StyleSource>,
3315    zoom: u8,
3316) -> Result<Cow<'a, FeatureCollection>, StyleError> {
3317    let Some(source) = sources.get(source_id) else {
3318        return Err(StyleError::MissingSource(source_id.to_owned()));
3319    };
3320    match source {
3321        StyleSource::GeoJson(source) => Ok(source.features_at_zoom(zoom)),
3322        StyleSource::VectorTile(source) => {
3323            if let Some(source_layer) = source_layer {
3324                source
3325                    .source_layer(source_layer)
3326                    .map(Cow::Borrowed)
3327                    .ok_or_else(|| StyleError::MissingSourceLayer {
3328                        layer_id: layer_id.to_owned(),
3329                        source_id: source_id.to_owned(),
3330                        source_layer: source_layer.to_owned(),
3331                    })
3332            } else {
3333                Ok(Cow::Borrowed(&source.data))
3334            }
3335        }
3336        other => Err(StyleError::SourceKindMismatch {
3337            layer_id: layer_id.to_owned(),
3338            source_id: source_id.to_owned(),
3339            expected: "geojson|vector",
3340            actual: other.kind_name(),
3341        }),
3342    }
3343}
3344
3345fn require_model_source<'a>(
3346    layer_id: &str,
3347    source_id: &str,
3348    sources: &'a HashMap<StyleSourceId, StyleSource>,
3349) -> Result<&'a ModelSource, StyleError> {
3350    let Some(source) = sources.get(source_id) else {
3351        return Err(StyleError::MissingSource(source_id.to_owned()));
3352    };
3353    match source {
3354        StyleSource::Model(model) => Ok(model),
3355        other => Err(StyleError::SourceKindMismatch {
3356            layer_id: layer_id.to_owned(),
3357            source_id: source_id.to_owned(),
3358            expected: "model",
3359            actual: other.kind_name(),
3360        }),
3361    }
3362}