Skip to main content

rustial_engine/map_state/
mod.rs

1// ---------------------------------------------------------------------------
2//! # Map state -- the central per-frame state object
3//!
4//! [`MapState`] owns every piece of data the engine needs todescribe a
5//! single frame of the map: camera, layers, terrain, animation, and
6//! precomputed derived values (zoom level, viewport bounds, frustum).
7//!
8//! ## Lifecycle (host <-> engine <-> renderer)
9//!
10//! ```text
11//!  Host application                    Engine (MapState)           Renderer
12//!  +----------------+                  +--------------+           +----------+
13//!  | winit / Bevy   |-- handle_input ->|              |           |          |
14//!  | event loop     |-- fly_to ------->|  update()    |           |          |
15//!  |                |                  |  update_     |           |          |
16//!  |                |                  |  with_dt()   |           |          |
17//!  |                |                  |              |           |          |
18//!  |                |                  | frame_output +---------->| render() |
19//!  +----------------+                  +--------------+           +----------+
20//! ```
21//!
22//! 1. **Input** -- the host feeds [`InputEvent`]s via
23//!    [`handle_input`](MapState::handle_input).
24//! 2. **Update** -- the host calls [`update`](MapState::update) (or
25//!    [`update_with_dt`](MapState::update_with_dt)) once per frame.
26//!    This ticks the camera animator, recomputes the zoom level and
27//!    viewport bounds, rebuilds terrain meshes, and iterates the layer
28//!    stack to update tile, vector, and model data.
29//! 3. **Read** -- the renderer reads derived state either through direct
30//!    field access (`camera`, `vector_meshes`, `model_instances`) or
31//!    through accessor methods (`visible_tiles()`, `terrain_meshes()`,
32//!    `zoom_level()`).  Alternatively, call
33//!    [`frame_output`](MapState::frame_output) to obtain a detached
34//!    snapshot that can be sent to a render thread.
35//! - **Derived state**:
36//!    - `zoom_level` -- Current integer zoom level (0-22).
37//!    - `viewport_bounds` -- Viewport bounding box in Web Mercator world space.
38//!    - `scene_viewport_bounds` -- Viewport bounding box in the active planar scene projection.
39//!    - `frustum` -- View frustum planes (derived from the camera VP matrix).
40//!    - `terrain_meshes` -- Terrain meshes produced during this frame's `update()`.
41//!    - `hillshade_rasters` -- Prepared hillshade rasters for the last `update()`.
42//!    - `visible_tiles` -- Visible tiles produced by the tile layer (or set externally).
43//! - **Per-frame layer output**:
44//!    - `vector_meshes` -- Tessellated vector meshes from the last `update()`.
45//!    - `model_instances` -- 3D model instances collected from all visible
46//!      [`ModelLayer`](crate::layers::ModelLayer)s during the last `update()`.
47//!    - `placed_symbols` -- Placed symbols after collision resolution.
48//!
49//! ## Thread safety
50//!
51//! `MapState` is `Send + Sync`.  The facade crate's [`MapHandle`]
52//! wraps it in an `RwLock`, giving shared read access for renderers
53//! and exclusive write access for the input / update path.
54//!
55//! ## Coordinate conventions
56//!
57//! All world-space positions are in **Web Mercator meters**, and the
58//! camera uses a **camera-relative** origin to avoid f32 jitter at
59//! large coordinates.  See the [`camera`](crate::camera) module docs
60//! for the full coordinate-system description.
61//!
62//! [`MapHandle`]: https://docs.rs/rustial/latest/rustial/struct.MapHandle.html
63// ---------------------------------------------------------------------------
64
65use crate::async_data::{
66    AsyncDataPipeline, DataTaskPool, TerrainTaskInput, VectorCacheKey, VectorTaskInput,
67};
68use crate::camera::{Camera, CameraConstraints, CameraController, CameraMode};
69use crate::camera_animator::CameraAnimator;
70use crate::camera_projection::CameraProjection;
71use crate::geo_wrap::wrap_lon_180;
72use crate::geometry::{FeatureCollection, PropertyValue};
73use crate::input::InputEvent;
74use crate::layer::Layer;
75use crate::layer::LayerId;
76use crate::layers::{FeatureProvenance, LayerStack, VectorMeshData, VectorStyle};
77use crate::loading_placeholder::{LoadingPlaceholder, PlaceholderGenerator, PlaceholderStyle};
78use crate::models::ModelInstance;
79use crate::picking::{HitCategory, HitProvenance, PickHit, PickOptions, PickQuery, PickResult};
80use crate::query::{
81    feature_id_for_feature, geometry_hit_distance, FeatureState, FeatureStateId, QueriedFeature,
82    QueryOptions,
83};
84use crate::streamed_payload::{
85    collect_affected_symbol_payloads, prune_affected_symbol_payloads,
86    resolve_streamed_vector_layer_refresh, symbol_query_payloads_from_optional,
87    StreamedPayloadView, StreamedSymbolPayloadKey, StreamedVectorLayerRefreshSpec,
88    SymbolDependencyPayload, SymbolQueryPayload, TileQueryPayload, VisiblePlacedSymbolView,
89};
90use crate::style::{MapStyle, StyleDocument, StyleError};
91use crate::symbols::{PlacedSymbol, SymbolAssetRegistry, SymbolPlacementEngine};
92use crate::terrain::{PreparedHillshadeRaster, TerrainConfig, TerrainManager, TerrainMeshData};
93use crate::tile_cache::TileCacheStats;
94use crate::tile_lifecycle::TileLifecycleDiagnostics;
95use crate::tile_manager::{TileManagerCounters, TileSelectionStats};
96use crate::tile_manager::{VisibleTile, ZoomPrefetchDirection};
97use crate::tile_request_coordinator::{
98    CoordinatorConfig, CoordinatorStats, SourcePriority, TileRequestCoordinator,
99};
100use crate::tile_source::TileSourceDiagnostics;
101use rustial_math::{
102    visible_tiles, visible_tiles_flat_view_with_config, FlatTileSelectionConfig, Frustum,
103    GeoBounds, GeoCoord, TileId, WebMercator, WorldBounds, WorldCoord, MAX_ZOOM,
104};
105use std::collections::{HashMap, HashSet, VecDeque};
106use std::sync::Arc;
107
108mod async_pipeline;
109mod heavy_layers;
110mod picking;
111#[cfg(test)]
112mod tests;
113mod tile_selection;
114
115// ---------------------------------------------------------------------------
116// Constants
117// ---------------------------------------------------------------------------
118
119/// Earth's equatorial circumference in meters (2*pi * WGS-84 semi-major axis).
120///
121/// Used to convert between camera `meters_per_pixel` and slippy-map zoom
122/// levels.  Matches the constant embedded in [`WebMercator::world_size()`].
123const WGS84_CIRCUMFERENCE: f64 = 2.0 * std::f64::consts::PI * 6_378_137.0;
124
125/// Standard raster tile edge length in pixels (universal slippy-map
126/// convention).
127const TILE_PX: f64 = 256.0;
128
129/// Fraction of a source budget that may be spent on speculative prefetch.
130const SPECULATIVE_PREFETCH_BUDGET_FRACTION: f64 = 0.25;
131
132/// Fallback speculative request cap when global coordination is disabled.
133const DEFAULT_SPECULATIVE_PREFETCH_REQUEST_BUDGET: usize = 8;
134
135/// Minimum fractional zoom change treated as intentional zoom motion.
136const ZOOM_DIRECTION_PREFETCH_THRESHOLD: f64 = 0.01;
137
138fn viewport_sample_points(width: f64, height: f64) -> Vec<(f64, f64)> {
139    const FRACTIONS: [f64; 7] = [0.0, 1.0 / 6.0, 2.0 / 6.0, 0.5, 4.0 / 6.0, 5.0 / 6.0, 1.0];
140    let mut samples = Vec::with_capacity(FRACTIONS.len() * FRACTIONS.len());
141    for fy in FRACTIONS {
142        for fx in FRACTIONS {
143            samples.push((width * fx, height * fy));
144        }
145    }
146    samples
147}
148
149fn perspective_viewport_overscan(pitch: f64) -> f64 {
150    let normalized_pitch = (pitch / std::f64::consts::FRAC_PI_2).clamp(0.0, 1.0);
151    1.3 + 0.5 * normalized_pitch
152}
153
154fn terrain_base_tile_budget(required_tiles: usize) -> usize {
155    required_tiles.clamp(80, 256)
156}
157
158fn terrain_horizon_tile_budget(base_budget: usize, pitch: f64) -> usize {
159    if pitch <= 0.5 {
160        0
161    } else {
162        (base_budget / 3).clamp(24, 96)
163    }
164}
165
166// ---------------------------------------------------------------------------
167// FitBoundsOptions
168// ---------------------------------------------------------------------------
169
170/// Padding in logical pixels for [`MapState::fit_bounds`].
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub struct FitBoundsPadding {
173    /// Top padding in logical pixels.
174    pub top: f64,
175    /// Bottom padding in logical pixels.
176    pub bottom: f64,
177    /// Left padding in logical pixels.
178    pub left: f64,
179    /// Right padding in logical pixels.
180    pub right: f64,
181}
182
183impl Default for FitBoundsPadding {
184    fn default() -> Self {
185        Self {
186            top: 0.0,
187            bottom: 0.0,
188            left: 0.0,
189            right: 0.0,
190        }
191    }
192}
193
194impl FitBoundsPadding {
195    /// Uniform padding on all sides.
196    pub fn uniform(px: f64) -> Self {
197        Self {
198            top: px,
199            bottom: px,
200            left: px,
201            right: px,
202        }
203    }
204}
205
206/// Options for [`MapState::fit_bounds`].
207///
208/// Mirrors MapLibre's `fitBounds` options.
209#[derive(Debug, Clone)]
210pub struct FitBoundsOptions {
211    /// Padding in logical pixels.
212    pub padding: FitBoundsPadding,
213    /// Maximum zoom level to use. `None` = no cap.
214    pub max_zoom: Option<f64>,
215    /// Whether to animate the transition (fly-to). Default: `true`.
216    pub animate: bool,
217    /// Explicit animation duration in seconds. Only used when `animate` is `true`.
218    pub duration: Option<f64>,
219    /// Target bearing (yaw) in radians. `None` = keep current.
220    pub bearing: Option<f64>,
221    /// Target pitch in radians. `None` = keep current.
222    pub pitch: Option<f64>,
223}
224
225impl Default for FitBoundsOptions {
226    fn default() -> Self {
227        Self {
228            padding: FitBoundsPadding::default(),
229            max_zoom: None,
230            animate: true,
231            duration: None,
232            bearing: None,
233            pitch: None,
234        }
235    }
236}
237
238// ---------------------------------------------------------------------------
239// Sync-path vector tessellation cache
240// ---------------------------------------------------------------------------
241
242/// Cache key for the sync-path per-layer vector tessellation cache.
243///
244/// When no async pipeline is active, `update_heavy_layers` uses this cache
245/// to skip re-tessellation when neither the style, the feature data, nor the
246/// projection have changed since the last frame.
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
248struct SyncVectorCacheKey {
249    /// Stable layer identity.
250    layer_id: LayerId,
251    /// Style fingerprint from [`VectorStyle::tessellation_fingerprint`].
252    style_fingerprint: u64,
253    /// Feature data generation counter.
254    data_generation: u64,
255    /// Projection at tessellation time.
256    projection: CameraProjection,
257}
258
259/// Cached tessellation result for a single vector layer in the sync path.
260#[derive(Clone)]
261struct SyncVectorCacheEntry {
262    mesh: VectorMeshData,
263}
264
265// ---------------------------------------------------------------------------
266// FrameOutput
267// ---------------------------------------------------------------------------
268
269/// Bundled per-frame snapshot produced by [`MapState::update`] for renderers.
270///
271/// Contains **everything** a renderer needs to draw a single frame,
272/// avoiding the need to reach into `MapState` internals.  All data is
273/// owned (`Clone`d from `MapState`), so the struct can be sent to a
274/// render thread without holding a lock.
275///
276/// # Usage
277///
278/// ```rust,ignore
279/// state.update();
280/// let frame = state.frame_output();
281/// // Send `frame` to the GPU thread ...
282/// ```
283#[derive(Debug, Default)]
284pub struct FrameOutput {
285    /// View-projection matrix (f64 precision, camera-relative origin).
286    ///
287    /// Cast to `glam::Mat4` (f32) when uploading to a GPU uniform buffer.
288    /// The f64 source preserves precision for CPU-side picking and culling.
289    pub view_projection: glam::DMat4,
290
291    /// View frustum planes for CPU-side culling.
292    ///
293    /// `None` only before the first call to `update()`.
294    pub frustum: Option<Frustum>,
295
296    /// Visible tiles (loaded imagery or parent-tile fallbacks).
297    ///
298    /// Wrapped in `Arc` for zero-copy sharing with render threads.
299    pub tiles: Arc<Vec<VisibleTile>>,
300
301    /// Terrain meshes to render (one per visible tile with elevation data).
302    ///
303    /// Wrapped in `Arc` for zero-copy sharing with render threads.
304    pub terrain: Arc<Vec<TerrainMeshData>>,
305
306    /// Prepared DEM-derived hillshade rasters aligned to the visible terrain set.
307    pub hillshade: Arc<Vec<PreparedHillshadeRaster>>,
308
309    /// Tessellated vector meshes (polygons, lines, points).
310    ///
311    /// Wrapped in `Arc` for zero-copy sharing with render threads.
312    pub vectors: Arc<Vec<VectorMeshData>>,
313
314    /// Placed 3D model instances to render.
315    ///
316    /// Wrapped in `Arc` for zero-copy sharing with render threads.
317    pub models: Arc<Vec<ModelInstance>>,
318
319    /// Placed symbol instances after collision resolution.
320    pub symbols: Arc<Vec<PlacedSymbol>>,
321
322    /// Visualization overlay data (grids, columns, etc.) from the last update.
323    pub visualization: Arc<Vec<crate::visualization::VisualizationOverlay>>,
324
325    /// Loading placeholders for visible tiles that have no data yet.
326    ///
327    /// Renderers should draw styled rectangles at these world bounds
328    /// before or behind the opaque tile pass so that loading areas
329    /// never appear as blank gaps.
330    pub placeholders: Arc<Vec<LoadingPlaceholder>>,
331
332    /// Georeferenced image overlays (image/video/canvas sources).
333    ///
334    /// Each entry is a textured quad defined by four world-space corners
335    /// plus RGBA8 pixel data.  Renderers should draw these between the
336    /// tile and vector passes.
337    pub image_overlays: Arc<Vec<crate::layers::ImageOverlayData>>,
338
339    /// Current integer zoom level (0-22).
340    pub zoom_level: u8,
341}
342
343/// Snapshot of the active tile-layer pipeline for debug/telemetry use.
344#[derive(Debug, Clone, Default)]
345pub struct TilePipelineDiagnostics {
346    /// Name of the tile layer providing the snapshot.
347    pub layer_name: String,
348    /// Number of visible tiles in the layer's last visible set.
349    pub visible_tiles: usize,
350    /// Number of visible tiles with exact or fallback data loaded.
351    pub visible_loaded_tiles: usize,
352    /// Number of visible tiles currently using fallback imagery.
353    pub visible_fallback_tiles: usize,
354    /// Number of visible tiles with no imagery currently available.
355    pub visible_missing_tiles: usize,
356    /// Number of visible tiles rendered as overzoomed.
357    pub visible_overzoomed_tiles: usize,
358    /// Per-frame tile-selection stats from the last update.
359    pub selection_stats: TileSelectionStats,
360    /// Cumulative tile-manager counters.
361    pub counters: TileManagerCounters,
362    /// Current cache state counts.
363    pub cache_stats: TileCacheStats,
364    /// Optional source transport diagnostics.
365    pub source_diagnostics: Option<TileSourceDiagnostics>,
366}
367
368/// Configuration for camera-motion sampling and look-ahead prediction.
369#[derive(Debug, Clone, PartialEq)]
370pub struct CameraVelocityConfig {
371    /// Number of recent motion samples to retain.
372    ///
373    /// The effective velocity is computed from the oldest and newest retained
374    /// samples, so larger windows smooth short spikes while smaller windows
375    /// react more quickly to abrupt changes.
376    pub sample_window: usize,
377    /// Look-ahead time in seconds used to project the current pan motion.
378    pub look_ahead_seconds: f64,
379}
380
381impl Default for CameraVelocityConfig {
382    fn default() -> Self {
383        Self {
384            sample_window: 6,
385            look_ahead_seconds: 0.5,
386        }
387    }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq)]
391struct CameraMotionSample {
392    time_seconds: f64,
393    target_world: glam::DVec2,
394}
395
396/// Smoothed camera pan state derived from recent update frames.
397#[derive(Debug, Clone, PartialEq)]
398pub struct CameraMotionState {
399    /// Smoothed pan velocity in Web Mercator meters per second.
400    pub pan_velocity_world: glam::DVec2,
401    /// Predicted camera target in Web Mercator meters after the configured
402    /// look-ahead interval.
403    pub predicted_target_world: glam::DVec2,
404    /// Predicted viewport bounds translated by the same look-ahead motion.
405    pub predicted_viewport_bounds: WorldBounds,
406}
407
408impl Default for CameraMotionState {
409    fn default() -> Self {
410        let zero = WorldCoord::new(0.0, 0.0, 0.0);
411        Self {
412            pan_velocity_world: glam::DVec2::ZERO,
413            predicted_target_world: glam::DVec2::ZERO,
414            predicted_viewport_bounds: WorldBounds::new(zero, zero),
415        }
416    }
417}
418
419// ---------------------------------------------------------------------------
420// MapState
421// ---------------------------------------------------------------------------
422
423/// The central state object consumed by renderers each frame.
424///
425/// Owns the camera, layer stack, terrain manager, camera animator,
426/// and all per-frame derived data (zoom level, viewport bounds,
427/// frustum, mesh caches).
428///
429/// # Field visibility
430///
431/// All fields are private.  Use accessor methods for reads and
432/// targeted setters for writes:
433///
434/// | Need | Method |
435/// |------|--------|
436/// | Read camera | [`camera()`](Self::camera) |
437/// | Set viewport size | [`set_viewport`](Self::set_viewport) |
438/// | Set camera target | [`set_camera_target`](Self::set_camera_target) |
439/// | Set camera distance | [`set_camera_distance`](Self::set_camera_distance) |
440/// | Set camera pitch/yaw | [`set_camera_pitch`](Self::set_camera_pitch), [`set_camera_yaw`](Self::set_camera_yaw) |
441/// | Toggle projection | [`set_camera_mode`](Self::set_camera_mode) |
442/// | Set camera FOV | [`set_camera_fov_y`](Self::set_camera_fov_y) |
443/// | Read constraints | [`constraints()`](Self::constraints) |
444/// | Set max pitch | [`set_max_pitch`](Self::set_max_pitch) |
445/// | Add a layer | [`push_layer`](Self::push_layer) |
446/// | Read layers | [`layers()`](Self::layers) |
447/// | Read vector meshes | [`vector_meshes()`](Self::vector_meshes) |
448/// | Read model instances | [`model_instances()`](Self::model_instances) |
449/// | Dispatch input | [`handle_input`](Self::handle_input) |
450/// | Animate camera | [`fly_to`](Self::fly_to) |
451/// | Check animator | [`animator()`](Self::animator) |
452/// | Replace terrain | [`set_terrain`](Self::set_terrain) |
453/// | Read terrain state | [`terrain()`](Self::terrain) |
454pub struct MapState {
455    // -- Inputs -----------------------------------------------------------
456    /// Camera controlling the view (target, orbit params, projection).
457    camera: Camera,
458
459    /// Per-frame clamps applied to the camera by [`CameraController`](crate::CameraController).
460    constraints: CameraConstraints,
461
462    /// Ordered layer stack (rendered bottom-to-top).
463    pub(crate) layers: LayerStack,
464
465    /// Optional style/runtime document that produced the current layer stack.
466    style: Option<MapStyle>,
467
468    /// Terrain elevation manager.
469    pub(crate) terrain: TerrainManager,
470
471    /// Camera animator for smooth zoom, rotation, and pan momentum.
472    animator: CameraAnimator,
473
474    // -- Animation-aware data update throttle ------------------------------
475    /// Minimum interval (seconds) between heavy layer updates (terrain
476    /// meshing, vector tessellation, symbol placement, model collection)
477    /// while a coordinated camera animation (fly-to / ease-to) is active.
478    ///
479    /// Tile layer updates (HTTP response polling, visible tile selection,
480    /// and tile request issuing) always run every frame regardless of this
481    /// interval, since they are lightweight and essential for keeping the
482    /// visible tile set in sync with the camera.
483    ///
484    /// Default: `0.15` (~6-7 heavy data updates per second during animation).
485    /// Set to `0.0` to disable throttling (matches the MapLibre model of
486    /// updating every frame, but may cause stutter with heavy layers).
487    ///
488    /// **Note:** When an async task pool is active (see
489    /// [`set_task_pool`](Self::set_task_pool)), this interval is ignored
490    /// because dispatch + poll is cheap regardless of animation state.
491    data_update_interval: f64,
492
493    /// Accumulated time since the last full data update during animation.
494    data_update_elapsed: f64,
495
496    // -- Async retained data pipeline ------------------------------------
497    /// Optional async data pipeline for offloading heavy CPU work.
498    ///
499    /// When set, `update_with_dt` dispatches terrain/vector/symbol work
500    /// to background tasks and polls completed results each frame, matching
501    /// the MapLibre/Mapbox web worker model.
502    ///
503    /// When `None`, the engine falls back to synchronous inline execution.
504    async_pipeline: Option<AsyncDataPipeline>,
505
506    // -- Per-frame derived state (private, recomputed by update) -----------
507    /// Current integer zoom level (derived from `camera.meters_per_pixel()`).
508    zoom_level: u8,
509
510    /// Viewport bounding box in Web Mercator world space.
511    viewport_bounds: WorldBounds,
512
513    /// Viewport bounding box in the active planar scene projection.
514    scene_viewport_bounds: WorldBounds,
515
516    /// View frustum planes (derived from the camera VP matrix).
517    frustum: Option<Frustum>,
518
519    /// Terrain meshes produced during this frame's `update()`.
520    terrain_meshes: Arc<Vec<TerrainMeshData>>,
521
522    /// Manual terrain output override to apply after the next update cycle.
523    pending_terrain_meshes: Option<Arc<Vec<TerrainMeshData>>>,
524
525    /// Prepared hillshade rasters for the last `update()`.
526    hillshade_rasters: Arc<Vec<PreparedHillshadeRaster>>,
527
528    /// Visible tiles produced by the tile layer (or set externally).
529    visible_tiles: Arc<Vec<VisibleTile>>,
530
531    // -- Per-frame layer output -------------------------------------------
532    /// Tessellated vector meshes from the last `update()`.
533    pub(crate) vector_meshes: Arc<Vec<VectorMeshData>>,
534
535    /// Manual vector output override to apply after the next update cycle.
536    pending_vector_meshes: Option<Arc<Vec<VectorMeshData>>>,
537
538    /// 3D model instances collected from all visible
539    /// [`ModelLayer`](crate::layers::ModelLayer)s during the last `update()`.
540    pub(crate) model_instances: Arc<Vec<ModelInstance>>,
541
542    /// Manual model output override to apply after the next update cycle.
543    pending_model_instances: Option<Arc<Vec<ModelInstance>>>,
544
545    /// Placed symbols after collision resolution.
546    placed_symbols: Arc<Vec<PlacedSymbol>>,
547
548    /// Symbol asset dependency state derived from placed symbols.
549    symbol_assets: SymbolAssetRegistry,
550
551    /// Stateful symbol placement engine.
552    symbol_placement: SymbolPlacementEngine,
553
554    /// Mutable per-feature state keyed by source id and feature id.
555    feature_state: HashMap<FeatureStateId, FeatureState>,
556
557    /// Hidden style-owned streamed vector source runtimes keyed by source id.
558    streamed_vector_sources: HashMap<String, crate::layers::TileLayer>,
559
560    /// Last per-style-layer streamed vector fingerprint used to detect data changes.
561    streamed_vector_layer_fingerprints: HashMap<String, u64>,
562
563    /// Tile-owned query payloads for streamed vector style layers.
564    streamed_vector_query_payloads: HashMap<String, Vec<TileQueryPayload>>,
565
566    /// Tile-owned symbol query payloads grouped by style layer.
567    streamed_symbol_query_payloads: HashMap<String, Vec<SymbolQueryPayload>>,
568
569    /// Tile-owned symbol dependency payloads grouped by style layer.
570    streamed_symbol_dependency_payloads: HashMap<String, Vec<SymbolDependencyPayload>>,
571
572    /// Streamed symbol layers that must regenerate on the next update.
573    dirty_streamed_symbol_layers: HashSet<String>,
574
575    /// Streamed symbol tiles that must regenerate on the next update.
576    dirty_streamed_symbol_tiles: HashMap<String, HashSet<TileId>>,
577
578    /// Visualization overlays collected from the layer stack.
579    visualization_overlays: Arc<Vec<crate::visualization::VisualizationOverlay>>,
580
581    /// Image overlays collected from the layer stack.
582    image_overlays: Arc<Vec<crate::layers::ImageOverlayData>>,
583
584    /// Active placeholder style applied to loading tiles.
585    placeholder_style: PlaceholderStyle,
586
587    /// Loading placeholders from the last update.
588    loading_placeholders: Arc<Vec<LoadingPlaceholder>>,
589
590    /// Monotonic time accumulator (seconds) for placeholder animation.
591    placeholder_time: f64,
592
593    /// Optional engine-owned interaction manager for automatic hover/leave/click
594    /// lifecycle bookkeeping. Set via [`set_interaction_manager`](Self::set_interaction_manager).
595    interaction_manager: Option<crate::interaction_manager::InteractionManager>,
596
597    /// Sync-path per-layer vector tessellation cache.
598    ///
599    /// When no async pipeline is active, [`update_heavy_layers`](Self::update_heavy_layers)
600    /// stores the most recent tessellation result per layer here and reuses it
601    /// when neither the style, the feature data, nor the projection have changed.
602    sync_vector_cache: HashMap<SyncVectorCacheKey, SyncVectorCacheEntry>,
603
604    /// Cross-source tile request coordinator.
605    ///
606    /// Distributes a global per-frame request budget among raster, vector,
607    /// and terrain tile sources so they don't compete for bandwidth.
608    /// See [`TileRequestCoordinator`] for details.
609    request_coordinator: TileRequestCoordinator,
610
611    /// Camera-motion sampling and look-ahead configuration.
612    camera_velocity_config: CameraVelocityConfig,
613
614    /// Monotonic clock used for camera-motion sampling.
615    camera_motion_time_seconds: f64,
616
617    /// Recent camera-target samples in Web Mercator world space.
618    camera_motion_samples: VecDeque<CameraMotionSample>,
619
620    /// Smoothed camera pan velocity and translated viewport prediction.
621    camera_motion_state: CameraMotionState,
622
623    /// Per-frame fractional zoom delta used for zoom-direction prefetch.
624    camera_zoom_delta: f64,
625
626    /// Previous fractional zoom sample for zoom-direction tracking.
627    previous_fractional_zoom: Option<f64>,
628
629    /// Optional navigation route polyline for route-aware tile prefetch.
630    ///
631    /// When set, the tile update loop speculatively prefetches tiles along
632    /// this route ahead of the camera position, spending any remaining
633    /// speculative budget after viewport and zoom-direction prefetch.
634    prefetch_route: Option<Vec<GeoCoord>>,
635
636    /// Callback-based event subscription emitter.
637    ///
638    /// Events drained from the [`InteractionManager`] are dispatched to
639    /// registered listeners each frame, in addition to being available
640    /// via the poll-based [`drain_events`] path.
641    event_emitter: crate::event_emitter::EventEmitter,
642
643    /// Multi-touch gesture recognizer.
644    ///
645    /// Converts raw [`TouchContact`](crate::input::TouchContact) events
646    /// into pan / zoom / rotate [`InputEvent`]s.
647    gesture_recognizer: crate::gesture::GestureRecognizer,
648
649    /// Optional user fog/atmosphere override.
650    fog_config: Option<crate::style::FogConfig>,
651
652    /// Pre-computed fog parameters for the current frame.
653    computed_fog: crate::style::ComputedFog,
654
655    /// Optional user lighting override.
656    light_config: Option<crate::style::LightConfig>,
657
658    /// Pre-computed lighting parameters for the current frame.
659    computed_lighting: crate::style::ComputedLighting,
660
661    /// Pre-computed shadow cascade parameters for the current frame.
662    computed_shadow: crate::style::ComputedShadow,
663
664    /// Optional user sky/atmosphere override.
665    sky_config: Option<crate::style::SkyConfig>,
666
667    /// Pre-computed sky parameters for the current frame.
668    computed_sky: crate::style::ComputedSky,
669
670    // -- Style transitions ------------------------------------------------
671    /// Monotonic accumulated time (seconds) for style transitions.
672    style_time: f64,
673
674    /// Per-layer transition state for paint-property interpolation.
675    layer_transitions:
676        std::collections::HashMap<crate::style::StyleLayerId, crate::style::LayerTransitionState>,
677}
678
679// ---------------------------------------------------------------------------
680// Default
681// ---------------------------------------------------------------------------
682
683impl Default for MapState {
684    fn default() -> Self {
685        let zero = WorldCoord::new(0.0, 0.0, 0.0);
686        Self {
687            camera: Camera::default(),
688            constraints: CameraConstraints::default(),
689            layers: LayerStack::new(),
690            style: None,
691            terrain: TerrainManager::new(TerrainConfig::default(), 256),
692            animator: CameraAnimator::new(),
693            data_update_interval: 0.15,
694            data_update_elapsed: 0.0,
695            async_pipeline: None,
696            zoom_level: 0,
697            viewport_bounds: WorldBounds::new(zero, zero),
698            scene_viewport_bounds: WorldBounds::new(zero, zero),
699            frustum: None,
700            terrain_meshes: Arc::new(Vec::new()),
701            pending_terrain_meshes: None,
702            hillshade_rasters: Arc::new(Vec::new()),
703            visible_tiles: Arc::new(Vec::new()),
704            vector_meshes: Arc::new(Vec::new()),
705            pending_vector_meshes: None,
706            model_instances: Arc::new(Vec::new()),
707            pending_model_instances: None,
708            placed_symbols: Arc::new(Vec::new()),
709            symbol_assets: SymbolAssetRegistry::new(),
710            symbol_placement: SymbolPlacementEngine::new(),
711            feature_state: HashMap::new(),
712            streamed_vector_sources: HashMap::new(),
713            streamed_vector_layer_fingerprints: HashMap::new(),
714            streamed_vector_query_payloads: HashMap::new(),
715            streamed_symbol_query_payloads: HashMap::new(),
716            streamed_symbol_dependency_payloads: HashMap::new(),
717            dirty_streamed_symbol_layers: HashSet::new(),
718            dirty_streamed_symbol_tiles: HashMap::new(),
719            visualization_overlays: Arc::new(Vec::new()),
720            image_overlays: Arc::new(Vec::new()),
721            placeholder_style: PlaceholderStyle::default(),
722            loading_placeholders: Arc::new(Vec::new()),
723            placeholder_time: 0.0,
724            interaction_manager: None,
725            sync_vector_cache: HashMap::new(),
726            request_coordinator: TileRequestCoordinator::default(),
727            camera_velocity_config: CameraVelocityConfig::default(),
728            camera_motion_time_seconds: 0.0,
729            camera_motion_samples: VecDeque::new(),
730            camera_motion_state: CameraMotionState::default(),
731            camera_zoom_delta: 0.0,
732            previous_fractional_zoom: None,
733            prefetch_route: None,
734            event_emitter: crate::event_emitter::EventEmitter::new(),
735            gesture_recognizer: crate::gesture::GestureRecognizer::new(),
736            fog_config: None,
737            computed_fog: crate::style::ComputedFog::default(),
738            light_config: None,
739            computed_lighting: crate::style::ComputedLighting::default(),
740            computed_shadow: crate::style::ComputedShadow::default(),
741            sky_config: None,
742            computed_sky: crate::style::ComputedSky::default(),
743            style_time: 0.0,
744            layer_transitions: std::collections::HashMap::new(),
745        }
746    }
747}
748
749// ---------------------------------------------------------------------------
750// Construction
751// ---------------------------------------------------------------------------
752
753impl MapState {
754    /// Create a new map state with default camera and no terrain.
755    pub fn new() -> Self {
756        Self::default()
757    }
758
759    /// Create a map state with terrain support.
760    ///
761    /// # Arguments
762    ///
763    /// - `terrain_config` -- Terrain settings (enabled flag, mesh resolution,
764    ///   elevation source, vertical exaggeration, skirt depth).
765    /// - `terrain_cache_size` -- Maximum number of elevation grids to keep
766    ///   in the LRU cache.
767    pub fn with_terrain(terrain_config: TerrainConfig, terrain_cache_size: usize) -> Self {
768        let zero = WorldCoord::new(0.0, 0.0, 0.0);
769        Self {
770            camera: Camera::default(),
771            constraints: CameraConstraints::default(),
772            layers: LayerStack::new(),
773            style: None,
774            terrain: TerrainManager::new(terrain_config, terrain_cache_size),
775            animator: CameraAnimator::new(),
776            data_update_interval: 0.15,
777            data_update_elapsed: 0.0,
778            async_pipeline: None,
779            zoom_level: 0,
780            viewport_bounds: WorldBounds::new(zero, zero),
781            scene_viewport_bounds: WorldBounds::new(zero, zero),
782            frustum: None,
783            terrain_meshes: Arc::new(Vec::new()),
784            pending_terrain_meshes: None,
785            hillshade_rasters: Arc::new(Vec::new()),
786            visible_tiles: Arc::new(Vec::new()),
787            vector_meshes: Arc::new(Vec::new()),
788            pending_vector_meshes: None,
789            model_instances: Arc::new(Vec::new()),
790            pending_model_instances: None,
791            placed_symbols: Arc::new(Vec::new()),
792            symbol_assets: SymbolAssetRegistry::new(),
793            symbol_placement: SymbolPlacementEngine::new(),
794            feature_state: HashMap::new(),
795            streamed_vector_sources: HashMap::new(),
796            streamed_vector_layer_fingerprints: HashMap::new(),
797            streamed_vector_query_payloads: HashMap::new(),
798            streamed_symbol_query_payloads: HashMap::new(),
799            streamed_symbol_dependency_payloads: HashMap::new(),
800            dirty_streamed_symbol_layers: HashSet::new(),
801            dirty_streamed_symbol_tiles: HashMap::new(),
802            visualization_overlays: Arc::new(Vec::new()),
803            image_overlays: Arc::new(Vec::new()),
804            placeholder_style: PlaceholderStyle::default(),
805            loading_placeholders: Arc::new(Vec::new()),
806            placeholder_time: 0.0,
807            interaction_manager: None,
808            sync_vector_cache: HashMap::new(),
809            request_coordinator: TileRequestCoordinator::default(),
810            camera_velocity_config: CameraVelocityConfig::default(),
811            camera_motion_time_seconds: 0.0,
812            camera_motion_samples: VecDeque::new(),
813            camera_motion_state: CameraMotionState::default(),
814            camera_zoom_delta: 0.0,
815            previous_fractional_zoom: None,
816            prefetch_route: None,
817            event_emitter: crate::event_emitter::EventEmitter::new(),
818            gesture_recognizer: crate::gesture::GestureRecognizer::new(),
819            fog_config: None,
820            computed_fog: crate::style::ComputedFog::default(),
821            light_config: None,
822            computed_lighting: crate::style::ComputedLighting::default(),
823            computed_shadow: crate::style::ComputedShadow::default(),
824            sky_config: None,
825            computed_sky: crate::style::ComputedSky::default(),
826            style_time: 0.0,
827            layer_transitions: std::collections::HashMap::new(),
828        }
829    }
830
831    // -- Accessors (derived state) ----------------------------------------
832
833    /// Current integer zoom level (0-22).
834    ///
835    /// Derived from [`Camera::meters_per_pixel`] during [`update`](Self::update).
836    /// Returns `0` before the first update.
837    #[inline]
838    pub fn zoom_level(&self) -> u8 {
839        self.zoom_level
840    }
841
842    /// Continuous fractional zoom level (e.g. `8.73`).
843    ///
844    /// Derived from [`Camera::near_meters_per_pixel`] using the same
845    /// formula as [`zoom_level`](Self::zoom_level), but without rounding
846    /// to an integer.  Useful for debug displays and smooth zoom
847    /// interpolation.
848    #[inline]
849    pub fn fractional_zoom(&self) -> f64 {
850        let mpp = self.camera.near_meters_per_pixel();
851        if mpp <= 0.0 || !mpp.is_finite() {
852            return MAX_ZOOM as f64;
853        }
854        (WGS84_CIRCUMFERENCE / (mpp * TILE_PX))
855            .log2()
856            .clamp(0.0, MAX_ZOOM as f64)
857    }
858
859    /// Viewport bounding box in Web Mercator world space from the last
860    /// [`update`](Self::update).
861    ///
862    /// Includes an overscan margin for tile prefetching.
863    #[inline]
864    pub fn viewport_bounds(&self) -> &WorldBounds {
865        &self.viewport_bounds
866    }
867
868    /// Read-only access to the camera-motion prediction configuration.
869    #[inline]
870    pub fn camera_velocity_config(&self) -> &CameraVelocityConfig {
871        &self.camera_velocity_config
872    }
873
874    /// Replace the camera-motion prediction configuration.
875    pub fn set_camera_velocity_config(&mut self, config: CameraVelocityConfig) {
876        self.camera_velocity_config = config;
877        self.trim_camera_motion_samples();
878        self.recompute_camera_motion_state();
879    }
880
881    /// Smoothed camera pan velocity and translated viewport prediction.
882    #[inline]
883    pub fn camera_motion_state(&self) -> &CameraMotionState {
884        &self.camera_motion_state
885    }
886
887    /// Predicted viewport bounds after the configured look-ahead interval.
888    #[inline]
889    pub fn predicted_viewport_bounds(&self) -> &WorldBounds {
890        &self.camera_motion_state.predicted_viewport_bounds
891    }
892
893    /// Viewport bounding box in the active planar scene projection.
894    #[inline]
895    pub fn scene_viewport_bounds(&self) -> &WorldBounds {
896        &self.scene_viewport_bounds
897    }
898
899    /// View frustum from the last [`update`](Self::update).
900    ///
901    /// `None` only before the first `update()` call.
902    #[inline]
903    pub fn frustum(&self) -> Option<&Frustum> {
904        self.frustum.as_ref()
905    }
906
907    /// Renderer/world origin used by the current render compatibility path.
908    ///
909    /// This remains Web Mercator backed for the current raster/terrain stack,
910    /// even when the camera itself uses a different planar projection.
911    #[inline]
912    pub fn renderer_world_origin(&self) -> glam::DVec3 {
913        let (x, y) = self.mercator_camera_world();
914        glam::DVec3::new(x, y, 0.0)
915    }
916
917    /// Active scene origin in the camera's currently selected planar
918    /// projection.
919    ///
920    /// Projection-specialized geometry paths such as terrain, vectors,
921    /// symbols, models, and Bevy geo-entities should use this origin so
922    /// their world coordinates remain aligned under non-Mercator planar
923    /// projections.
924    #[inline]
925    pub fn scene_world_origin(&self) -> glam::DVec3 {
926        self.camera.target_world()
927    }
928
929    /// Terrain meshes produced during the last [`update`](Self::update).
930    ///
931    /// Empty when terrain is disabled or no elevation data is cached.
932    #[inline]
933    pub fn terrain_meshes(&self) -> &[TerrainMeshData] {
934        &self.terrain_meshes
935    }
936
937    /// Prepared DEM-derived hillshade rasters from the last update.
938    #[inline]
939    pub fn hillshade_rasters(&self) -> &[PreparedHillshadeRaster] {
940        &self.hillshade_rasters
941    }
942
943    /// Visible tiles from the last [`update`](Self::update).
944    #[inline]
945    pub fn visible_tiles(&self) -> &[VisibleTile] {
946        &self.visible_tiles
947    }
948
949    /// Loading placeholders from the last [`update`](Self::update).
950    ///
951    /// One entry per visible tile that has no data yet.  Empty when all
952    /// visible tiles are loaded.
953    #[inline]
954    pub fn loading_placeholders(&self) -> &[LoadingPlaceholder] {
955        &self.loading_placeholders
956    }
957
958    /// Active placeholder style.
959    #[inline]
960    pub fn placeholder_style(&self) -> &PlaceholderStyle {
961        &self.placeholder_style
962    }
963
964    /// Replace the placeholder style.
965    pub fn set_placeholder_style(&mut self, style: PlaceholderStyle) {
966        self.placeholder_style = style;
967    }
968
969    /// Read-only access to the cross-source tile request coordinator's
970    /// most recent diagnostics.
971    pub fn coordinator_stats(&self) -> &CoordinatorStats {
972        self.request_coordinator.stats()
973    }
974
975    /// Read-only access to the cross-source coordinator configuration.
976    pub fn coordinator_config(&self) -> &CoordinatorConfig {
977        self.request_coordinator.config()
978    }
979
980    /// Replace the cross-source coordinator configuration.
981    ///
982    /// Takes effect on the next frame.  Set `global_request_budget` to
983    /// 0 to disable coordination entirely.
984    pub fn set_coordinator_config(&mut self, config: CoordinatorConfig) {
985        self.request_coordinator.set_config(config);
986    }
987
988    /// Set an optional navigation route for route-aware tile prefetch.
989    ///
990    /// When set, the tile update loop will speculatively prefetch tiles
991    /// along the route ahead of the current camera position, spending
992    /// remaining speculative budget after viewport-prediction and
993    /// zoom-direction prefetch.
994    ///
995    /// The route should be a polyline of geographic coordinates ordered
996    /// from origin to destination.  Pass an empty slice or call
997    /// [`clear_prefetch_route`](Self::clear_prefetch_route) to disable.
998    pub fn set_prefetch_route(&mut self, route: Vec<GeoCoord>) {
999        if route.len() < 2 {
1000            self.prefetch_route = None;
1001        } else {
1002            self.prefetch_route = Some(route);
1003        }
1004    }
1005
1006    /// Clear the navigation route used for route-aware tile prefetch.
1007    pub fn clear_prefetch_route(&mut self) {
1008        self.prefetch_route = None;
1009    }
1010
1011    /// Read-only access to the active prefetch route, if any.
1012    #[inline]
1013    pub fn prefetch_route(&self) -> Option<&[GeoCoord]> {
1014        self.prefetch_route.as_deref()
1015    }
1016
1017    /// Return a diagnostics snapshot for the first visible tile layer, if any.
1018    pub fn tile_pipeline_diagnostics(&self) -> Option<TilePipelineDiagnostics> {
1019        use crate::layers::TileLayer;
1020
1021        for layer in self.layers.iter() {
1022            if !layer.visible() {
1023                continue;
1024            }
1025            let Some(tile_layer) = layer.as_any().downcast_ref::<TileLayer>() else {
1026                continue;
1027            };
1028
1029            let visible = tile_layer.visible_tiles();
1030            let visible_loaded_tiles = visible
1031                .tiles
1032                .iter()
1033                .filter(|tile| tile.data.is_some())
1034                .count();
1035            let visible_fallback_tiles = visible
1036                .tiles
1037                .iter()
1038                .filter(|tile| tile.is_fallback() && tile.data.is_some())
1039                .count();
1040            let visible_missing_tiles = visible
1041                .tiles
1042                .iter()
1043                .filter(|tile| tile.data.is_none())
1044                .count();
1045            let visible_overzoomed_tiles = visible
1046                .tiles
1047                .iter()
1048                .filter(|tile| tile.is_overzoomed())
1049                .count();
1050
1051            return Some(TilePipelineDiagnostics {
1052                layer_name: tile_layer.name().to_owned(),
1053                visible_tiles: visible.len(),
1054                visible_loaded_tiles,
1055                visible_fallback_tiles,
1056                visible_missing_tiles,
1057                visible_overzoomed_tiles,
1058                selection_stats: tile_layer.last_selection_stats().clone(),
1059                counters: tile_layer.counters().clone(),
1060                cache_stats: tile_layer.cache_stats(),
1061                source_diagnostics: tile_layer.source_diagnostics(),
1062            });
1063        }
1064
1065        None
1066    }
1067
1068    /// Return recent tile lifecycle diagnostics for the first visible tile layer, if any.
1069    pub fn tile_lifecycle_diagnostics(&self) -> Option<TileLifecycleDiagnostics> {
1070        use crate::layers::TileLayer;
1071
1072        for layer in self.layers.iter() {
1073            if !layer.visible() {
1074                continue;
1075            }
1076            let Some(tile_layer) = layer.as_any().downcast_ref::<TileLayer>() else {
1077                continue;
1078            };
1079            return Some(tile_layer.lifecycle_diagnostics());
1080        }
1081
1082        None
1083    }
1084
1085    /// Return the effective background Colour from the top-most visible
1086    /// [`BackgroundLayer`](crate::layers::BackgroundLayer), if any.
1087    ///
1088    /// This gives renderers a backend-owned clear colour similar to
1089    /// MapLibre's background layer instead of relying only on hard-coded
1090    /// renderer defaults.
1091    pub fn background_color(&self) -> Option<[f32; 4]> {
1092        use crate::layers::BackgroundLayer;
1093
1094        let mut color = None;
1095        for layer in self.layers.iter() {
1096            if !layer.visible() {
1097                continue;
1098            }
1099            if let Some(background) = layer.as_any().downcast_ref::<BackgroundLayer>() {
1100                color = Some(background.effective_color());
1101            }
1102        }
1103        color
1104    }
1105
1106    /// Return the effective hillshade parameters from the top-most visible
1107    /// [`HillshadeLayer`](crate::layers::HillshadeLayer), if any.
1108    pub fn hillshade(&self) -> Option<crate::layers::HillshadeParams> {
1109        use crate::layers::HillshadeLayer;
1110
1111        let mut params = None;
1112        for layer in self.layers.iter() {
1113            if !layer.visible() {
1114                continue;
1115            }
1116            if let Some(hillshade) = layer.as_any().downcast_ref::<HillshadeLayer>() {
1117                params = Some(hillshade.effective_params());
1118            }
1119        }
1120        params
1121    }
1122
1123    /// Set the fog/atmosphere configuration override.
1124    ///
1125    /// When set, the provided [`FogConfig`](crate::style::FogConfig) values
1126    /// override the automatic pitch-based fog computation.  `None` fields
1127    /// in the config fall back to the auto-computed defaults.
1128    ///
1129    /// Pass `None` to revert to fully automatic fog.
1130    pub fn set_fog(&mut self, config: Option<crate::style::FogConfig>) {
1131        self.fog_config = config;
1132    }
1133
1134    /// Return the current fog configuration override, if any.
1135    pub fn fog(&self) -> Option<&crate::style::FogConfig> {
1136        self.fog_config.as_ref()
1137    }
1138
1139    /// Return the pre-computed fog parameters for the current frame.
1140    ///
1141    /// These are recomputed each frame during [`update()`](Self::update)
1142    /// from camera state, the optional [`FogConfig`](crate::style::FogConfig),
1143    /// and the background colour.  Renderers should read these values
1144    /// directly instead of duplicating the fog math.
1145    pub fn computed_fog(&self) -> &crate::style::ComputedFog {
1146        &self.computed_fog
1147    }
1148
1149    /// Set the lighting configuration override.
1150    ///
1151    /// When set, the provided [`LightConfig`](crate::style::LightConfig)
1152    /// values override the style document's `"lights"` configuration and
1153    /// the engine defaults.
1154    ///
1155    /// Pass `None` to revert to the style document's or default lighting.
1156    pub fn set_lights(&mut self, config: Option<crate::style::LightConfig>) {
1157        self.light_config = config;
1158    }
1159
1160    /// Return the current lighting configuration override, if any.
1161    pub fn lights(&self) -> Option<&crate::style::LightConfig> {
1162        self.light_config.as_ref()
1163    }
1164
1165    /// Return the pre-computed lighting parameters for the current frame.
1166    ///
1167    /// Recomputed each frame during [`update()`](Self::update).
1168    /// Renderers should read these values directly instead of hardcoding
1169    /// light directions.
1170    pub fn computed_lighting(&self) -> &crate::style::ComputedLighting {
1171        &self.computed_lighting
1172    }
1173
1174    /// Return the pre-computed shadow cascade parameters for the current frame.
1175    ///
1176    /// [`ComputedShadow::enabled`](crate::style::ComputedShadow::enabled) is
1177    /// `true` only when the directional light has `cast_shadows = true` and
1178    /// lighting is not in flat mode.
1179    pub fn computed_shadow(&self) -> &crate::style::ComputedShadow {
1180        &self.computed_shadow
1181    }
1182
1183    /// Set the sky / atmosphere configuration override.
1184    ///
1185    /// When set, the provided [`SkyConfig`](crate::style::SkyConfig)
1186    /// enables a procedural sky background.  Pass `None` to disable.
1187    pub fn set_sky(&mut self, config: Option<crate::style::SkyConfig>) {
1188        self.sky_config = config;
1189    }
1190
1191    /// Return the current sky configuration override, if any.
1192    pub fn sky(&self) -> Option<&crate::style::SkyConfig> {
1193        self.sky_config.as_ref()
1194    }
1195
1196    /// Return the pre-computed sky parameters for the current frame.
1197    ///
1198    /// Recomputed each frame during [`update()`](Self::update).
1199    pub fn computed_sky(&self) -> &crate::style::ComputedSky {
1200        &self.computed_sky
1201    }
1202
1203    /// Current monotonic style time (seconds), used for transition
1204    /// interpolation.
1205    pub fn style_time(&self) -> f64 {
1206        self.style_time
1207    }
1208
1209    /// Per-layer transition state map.
1210    pub fn layer_transitions(
1211        &self,
1212    ) -> &std::collections::HashMap<crate::style::StyleLayerId, crate::style::LayerTransitionState>
1213    {
1214        &self.layer_transitions
1215    }
1216
1217    /// Resolve current transitioned property values for a layer.
1218    ///
1219    /// Returns `None` if no transition state has been recorded for the
1220    /// given layer yet.
1221    pub fn resolved_transitions(
1222        &self,
1223        layer_id: &str,
1224    ) -> Option<crate::style::ResolvedTransitions> {
1225        self.layer_transitions
1226            .get(layer_id)
1227            .map(|ts| ts.resolve(self.style_time))
1228    }
1229
1230    /// Override the visible tile set for this frame.
1231    pub fn set_visible_tiles(&mut self, tiles: Vec<VisibleTile>) {
1232        self.visible_tiles = Arc::new(tiles);
1233    }
1234
1235    /// Override the terrain meshes for this frame (useful for testing).
1236    pub fn set_terrain_meshes(&mut self, meshes: Vec<TerrainMeshData>) {
1237        let meshes = Arc::new(meshes);
1238        self.pending_terrain_meshes = Some(meshes.clone());
1239        self.terrain_meshes = meshes;
1240    }
1241
1242    /// Override the prepared hillshade rasters for this frame (useful for testing).
1243    pub fn set_hillshade_rasters(&mut self, rasters: Vec<PreparedHillshadeRaster>) {
1244        self.hillshade_rasters = Arc::new(rasters);
1245    }
1246
1247    // -- Camera accessors -------------------------------------------------
1248
1249    /// Immutable reference to the camera.
1250    #[inline]
1251    pub fn camera(&self) -> &Camera {
1252        &self.camera
1253    }
1254
1255    /// Set the camera viewport dimensions (logical pixels).
1256    pub fn set_viewport(&mut self, width: u32, height: u32) {
1257        self.camera.set_viewport(width, height);
1258    }
1259
1260    /// Set the camera target geographic coordinate.
1261    ///
1262    /// Longitude is normalized to `[-180, 180)` to prevent the camera
1263    /// from drifting into a different world copy, which would cause
1264    /// tile selection to diverge and tiles to stop rendering.
1265    pub fn set_camera_target(&mut self, target: GeoCoord) {
1266        if !target.lat.is_finite() || !target.lon.is_finite() {
1267            return;
1268        }
1269        let wrapped = GeoCoord::from_lat_lon(target.lat, wrap_lon_180(target.lon));
1270        self.camera.set_target(wrapped);
1271    }
1272
1273    /// Set the camera distance from the target in meters.
1274    pub fn set_camera_distance(&mut self, distance: f64) {
1275        if !distance.is_finite() || distance <= 0.0 {
1276            return;
1277        }
1278        self.camera.set_distance(distance);
1279    }
1280
1281    /// Set the camera pitch in radians.
1282    pub fn set_camera_pitch(&mut self, pitch: f64) {
1283        if !pitch.is_finite() {
1284            return;
1285        }
1286        self.camera.set_pitch(pitch);
1287    }
1288
1289    /// Set the camera yaw / bearing in radians.
1290    pub fn set_camera_yaw(&mut self, yaw: f64) {
1291        if !yaw.is_finite() {
1292            return;
1293        }
1294        self.camera.set_yaw(yaw);
1295    }
1296
1297    /// Set the camera projection mode (perspective / orthographic).
1298    pub fn set_camera_mode(&mut self, mode: CameraMode) {
1299        self.camera.set_mode(mode);
1300    }
1301
1302    /// Set the camera geographic projection used by camera/world helpers.
1303    pub fn set_camera_projection(&mut self, projection: CameraProjection) {
1304        self.camera.set_projection(projection);
1305    }
1306
1307    /// Set the vertical field-of-view in radians (perspective mode).
1308    pub fn set_camera_fov_y(&mut self, fov_y: f64) {
1309        if !fov_y.is_finite() || fov_y <= 0.0 {
1310            return;
1311        }
1312        self.camera.set_fov_y(fov_y);
1313    }
1314
1315    // -- Constraint accessors ---------------------------------------------
1316
1317    /// Immutable reference to the camera constraints.
1318    #[inline]
1319    pub fn constraints(&self) -> &CameraConstraints {
1320        &self.constraints
1321    }
1322
1323    /// Set the maximum camera pitch in radians.
1324    pub fn set_max_pitch(&mut self, max_pitch: f64) {
1325        if !max_pitch.is_finite() || max_pitch <= 0.0 {
1326            return;
1327        }
1328        self.constraints.max_pitch = max_pitch;
1329    }
1330
1331    /// Set the minimum camera distance in meters.
1332    pub fn set_min_distance(&mut self, min_distance: f64) {
1333        if !min_distance.is_finite() || min_distance <= 0.0 {
1334            return;
1335        }
1336        self.constraints.min_distance = min_distance;
1337    }
1338
1339    /// Set the maximum camera distance in meters.
1340    pub fn set_max_distance(&mut self, max_distance: f64) {
1341        if !max_distance.is_finite() || max_distance <= 0.0 {
1342            return;
1343        }
1344        self.constraints.max_distance = max_distance;
1345    }
1346
1347    // -- Layer accessors---------------------------------------------------
1348
1349    /// Immutable reference to the layer stack.
1350    #[inline]
1351    pub fn layers(&self) -> &LayerStack {
1352        &self.layers
1353    }
1354
1355    /// Add a layer to the top of the stack.
1356    pub fn push_layer(&mut self, layer: Box<dyn crate::layer::Layer>) {
1357        self.layers.push(layer);
1358        self.style = None;
1359    }
1360
1361    /// Insert or replace a named grid-scalar visualization layer.
1362    pub fn set_grid_scalar(
1363        &mut self,
1364        name: impl Into<String>,
1365        grid: crate::visualization::GeoGrid,
1366        field: crate::visualization::ScalarField2D,
1367        ramp: crate::visualization::ColorRamp,
1368    ) {
1369        let name = name.into();
1370        if let Some(index) = self.layers.index_of(&name) {
1371            if let Some(layer) = self.layers.get_mut(index) {
1372                if let Some(existing) = layer
1373                    .as_any_mut()
1374                    .downcast_mut::<crate::visualization::GridScalarLayer>()
1375                {
1376                    existing.grid = grid;
1377                    existing.field = field;
1378                    existing.ramp = ramp;
1379                    self.style = None;
1380                    return;
1381                }
1382            }
1383        }
1384        let layer = Box::new(crate::visualization::GridScalarLayer::new(
1385            name.clone(),
1386            grid,
1387            field,
1388            ramp,
1389        ));
1390        self.replace_or_push_named_layer(&name, layer);
1391    }
1392
1393    /// Insert or replace a named grid-extrusion visualization layer.
1394    pub fn set_grid_extrusion(
1395        &mut self,
1396        name: impl Into<String>,
1397        grid: crate::visualization::GeoGrid,
1398        field: crate::visualization::ScalarField2D,
1399        ramp: crate::visualization::ColorRamp,
1400        params: crate::visualization::ExtrusionParams,
1401    ) {
1402        let name = name.into();
1403        if let Some(index) = self.layers.index_of(&name) {
1404            if let Some(layer) = self.layers.get_mut(index) {
1405                if let Some(existing) = layer
1406                    .as_any_mut()
1407                    .downcast_mut::<crate::visualization::GridExtrusionLayer>()
1408                {
1409                    existing.grid = grid;
1410                    existing.field = field;
1411                    existing.ramp = ramp;
1412                    existing.params = params;
1413                    self.style = None;
1414                    return;
1415                }
1416            }
1417        }
1418        let layer = Box::new(
1419            crate::visualization::GridExtrusionLayer::new(name.clone(), grid, field, ramp)
1420                .with_params(params),
1421        );
1422        self.replace_or_push_named_layer(&name, layer);
1423    }
1424
1425    /// Insert or replace a named instanced-column visualization layer.
1426    pub fn set_instanced_columns(
1427        &mut self,
1428        name: impl Into<String>,
1429        columns: crate::visualization::ColumnInstanceSet,
1430        ramp: crate::visualization::ColorRamp,
1431    ) {
1432        let name = name.into();
1433        if let Some(index) = self.layers.index_of(&name) {
1434            if let Some(layer) = self.layers.get_mut(index) {
1435                if let Some(existing) = layer
1436                    .as_any_mut()
1437                    .downcast_mut::<crate::visualization::InstancedColumnLayer>()
1438                {
1439                    existing.columns = columns;
1440                    existing.ramp = ramp;
1441                    self.style = None;
1442                    return;
1443                }
1444            }
1445        }
1446        let layer = Box::new(crate::visualization::InstancedColumnLayer::new(
1447            name.clone(),
1448            columns,
1449            ramp,
1450        ));
1451        self.replace_or_push_named_layer(&name, layer);
1452    }
1453
1454    /// Insert or replace a named point-cloud visualization layer.
1455    pub fn set_point_cloud(
1456        &mut self,
1457        name: impl Into<String>,
1458        points: crate::visualization::PointInstanceSet,
1459        ramp: crate::visualization::ColorRamp,
1460    ) {
1461        let name = name.into();
1462        if let Some(index) = self.layers.index_of(&name) {
1463            if let Some(layer) = self.layers.get_mut(index) {
1464                if let Some(existing) = layer
1465                    .as_any_mut()
1466                    .downcast_mut::<crate::visualization::PointCloudLayer>()
1467                {
1468                    existing.points = points;
1469                    existing.ramp = ramp;
1470                    self.style = None;
1471                    return;
1472                }
1473            }
1474        }
1475        let layer = Box::new(crate::visualization::PointCloudLayer::new(
1476            name.clone(),
1477            points,
1478            ramp,
1479        ));
1480        self.replace_or_push_named_layer(&name, layer);
1481    }
1482
1483    /// Return the currently attached style, if any.
1484    #[inline]
1485    pub fn style(&self) -> Option<&MapStyle> {
1486        self.style.as_ref()
1487    }
1488
1489    /// Return the currently attached style document, if any.
1490    #[inline]
1491    pub fn style_document(&self) -> Option<&StyleDocument> {
1492        self.style.as_ref().map(MapStyle::document)
1493    }
1494
1495    /// Replace the current style runtime and re-evaluate terrain + layer state.
1496    pub fn set_style(&mut self, style: MapStyle) -> Result<(), StyleError> {
1497        self.apply_style_document(style.document())?;
1498        self.style = Some(style);
1499        Ok(())
1500    }
1501
1502    /// Replace the current style document and re-evaluate terrain + layer state.
1503    pub fn set_style_document(&mut self, document: StyleDocument) -> Result<(), StyleError> {
1504        self.set_style(MapStyle::from_document(document))
1505    }
1506
1507    /// Mutate the current style document in place and then re-apply it.
1508    pub fn with_style_mut<R>(
1509        &mut self,
1510        mutate: impl FnOnce(&mut StyleDocument) -> R,
1511    ) -> Result<Option<R>, StyleError> {
1512        let mut style: MapStyle = match self.style.take() {
1513            Some(s) => s,
1514            None => return Ok(None),
1515        };
1516        let result = mutate(style.document_mut());
1517        self.apply_style_document(style.document())?;
1518        self.style = Some(style);
1519        Ok(Some(result))
1520    }
1521
1522    /// Re-evaluate the currently attached style document, if any.
1523    pub fn reapply_style(&mut self) -> Result<bool, StyleError> {
1524        let style: MapStyle = match self.style.take() {
1525            Some(s) => s,
1526            None => return Ok(false),
1527        };
1528        self.apply_style_document(style.document())?;
1529        self.style = Some(style);
1530        Ok(true)
1531    }
1532
1533    // -- Terrain accessors ------------------------------------------------
1534
1535    /// Immutable reference to the terrain manager.
1536    #[inline]
1537    pub fn terrain(&self) -> &TerrainManager {
1538        &self.terrain
1539    }
1540
1541    /// Replace the terrain manager with a new configuration.
1542    pub fn set_terrain(&mut self, terrain: TerrainManager) {
1543        self.terrain = terrain;
1544        if self.style.is_some() {
1545            self.style = None;
1546        }
1547    }
1548
1549    // -- Animator accessor ------------------------------------------------
1550
1551    /// Immutable reference to the camera animator.
1552    #[inline]
1553    pub fn animator(&self) -> &CameraAnimator {
1554        &self.animator
1555    }
1556
1557    /// Whether any camera animation is currently active.
1558    #[inline]
1559    pub fn is_animating(&self) -> bool {
1560        self.animator.is_active()
1561    }
1562
1563    // -- Data update throttle during animation ----------------------------
1564
1565    /// Set the minimum interval (seconds) between full terrain + layer
1566    /// updates while a coordinated camera animation is active.
1567    pub fn set_data_update_interval(&mut self, interval: f64) {
1568        self.data_update_interval = interval.max(0.0);
1569    }
1570
1571    /// Current data-update throttle interval in seconds.
1572    #[inline]
1573    pub fn data_update_interval(&self) -> f64 {
1574        self.data_update_interval
1575    }
1576
1577    // -- Async task pool --------------------------------------------------
1578
1579    /// Inject an async task pool for offloading heavy data pipeline work.
1580    pub fn set_task_pool(&mut self, pool: Arc<dyn DataTaskPool>) {
1581        self.async_pipeline = Some(AsyncDataPipeline::new(pool));
1582    }
1583
1584    /// Remove the async task pool, reverting to synchronous execution.
1585    pub fn clear_task_pool(&mut self) {
1586        self.async_pipeline = None;
1587    }
1588
1589    /// Whether an async task pool is currently active.
1590    #[inline]
1591    pub fn has_async_pipeline(&self) -> bool {
1592        self.async_pipeline.is_some()
1593    }
1594
1595    // -- Frame output accessors -------------------------------------------
1596
1597    /// Tessellated vector meshes from the last update.
1598    #[inline]
1599    pub fn vector_meshes(&self) -> &[VectorMeshData] {
1600        &self.vector_meshes
1601    }
1602
1603    /// 3D model instances from the last update.
1604    #[inline]
1605    pub fn model_instances(&self) -> &[ModelInstance] {
1606        &self.model_instances
1607    }
1608
1609    /// Placed symbols from the last update.
1610    #[inline]
1611    pub fn placed_symbols(&self) -> &[PlacedSymbol] {
1612        &self.placed_symbols
1613    }
1614
1615    /// Symbol asset dependency state for the last update.
1616    #[inline]
1617    pub fn symbol_assets(&self) -> &SymbolAssetRegistry {
1618        &self.symbol_assets
1619    }
1620
1621    /// Look up feature state for a source-local feature id.
1622    pub fn feature_state(&self, source_id: &str, feature_id: &str) -> Option<&FeatureState> {
1623        self.feature_state
1624            .get(&FeatureStateId::new(source_id, feature_id))
1625    }
1626
1627    /// Replace feature state for a source-local feature id.
1628    pub fn set_feature_state(
1629        &mut self,
1630        source_id: impl Into<String>,
1631        feature_id: impl Into<String>,
1632        state: FeatureState,
1633    ) {
1634        self.feature_state
1635            .insert(FeatureStateId::new(source_id, feature_id), state);
1636    }
1637
1638    /// Set a single feature-state property.
1639    pub fn set_feature_state_property(
1640        &mut self,
1641        source_id: impl Into<String>,
1642        feature_id: impl Into<String>,
1643        key: impl Into<String>,
1644        value: crate::geometry::PropertyValue,
1645    ) {
1646        self.feature_state
1647            .entry(FeatureStateId::new(source_id, feature_id))
1648            .or_default()
1649            .insert(key.into(), value);
1650    }
1651
1652    /// Remove feature state for a source-local feature id.
1653    pub fn remove_feature_state(
1654        &mut self,
1655        source_id: &str,
1656        feature_id: &str,
1657    ) -> Option<FeatureState> {
1658        self.feature_state
1659            .remove(&FeatureStateId::new(source_id, feature_id))
1660    }
1661
1662    /// Clear all stored feature state.
1663    pub fn clear_feature_state(&mut self) {
1664        self.feature_state.clear();
1665    }
1666
1667    /// Resolve a style layer's paint properties for a specific feature's
1668    /// current state.
1669    ///
1670    /// This is the primary convenience method for hover/selection restyling:
1671    /// it looks up the named style layer, retrieves the feature's current
1672    /// state from the internal store, and evaluates all paint properties
1673    /// through the [`StyleEvalContextFull`] path.
1674    ///
1675    /// Returns `None` when:
1676    /// - no style document is attached
1677    /// - the layer id does not exist in the style document
1678    /// - the layer type does not produce a [`VectorStyle`] (background,
1679    ///   hillshade, raster, model)
1680    ///
1681    /// # Example
1682    ///
1683    /// ```ignore
1684    /// map.set_feature_state_property("source", "feat-42", "hover", PropertyValue::Bool(true));
1685    /// if let Some(style) = map.resolve_feature_style("buildings", "source", "feat-42") {
1686    ///     // `style` has paint values resolved with hover=true.
1687    /// }
1688    /// ```
1689    pub fn resolve_feature_style(
1690        &self,
1691        style_layer_id: &str,
1692        source_id: &str,
1693        feature_id: &str,
1694    ) -> Option<VectorStyle> {
1695        use crate::style::StyleEvalContextFull;
1696
1697        let document = self.style_document()?;
1698        let style_layer = document.layer(style_layer_id)?;
1699        let empty_state: FeatureState = HashMap::new();
1700        let state = self
1701            .feature_state
1702            .get(&FeatureStateId::new(source_id, feature_id))
1703            .unwrap_or(&empty_state);
1704        let ctx = StyleEvalContextFull::new(self.fractional_zoom() as f32, state);
1705        style_layer.resolve_style_with_feature_state(&ctx)
1706    }
1707
1708    /// Attach an interaction manager for automatic hover/leave/click lifecycle.
1709    ///
1710    /// Once set, the host feeds raw pointer events into the manager (via
1711    /// [`interaction_manager_mut`](Self::interaction_manager_mut)) and drains
1712    /// high-level [`InteractionEvent`](crate::InteractionEvent)s each frame.
1713    pub fn set_interaction_manager(
1714        &mut self,
1715        manager: crate::interaction_manager::InteractionManager,
1716    ) {
1717        self.interaction_manager = Some(manager);
1718    }
1719
1720    /// Remove the attached interaction manager, returning it if present.
1721    pub fn take_interaction_manager(
1722        &mut self,
1723    ) -> Option<crate::interaction_manager::InteractionManager> {
1724        self.interaction_manager.take()
1725    }
1726
1727    /// Read-only access to the attached interaction manager.
1728    pub fn interaction_manager(&self) -> Option<&crate::interaction_manager::InteractionManager> {
1729        self.interaction_manager.as_ref()
1730    }
1731
1732    /// Mutable access to the attached interaction manager.
1733    ///
1734    /// Use this to feed pointer events and drain emitted interaction events.
1735    pub fn interaction_manager_mut(
1736        &mut self,
1737    ) -> Option<&mut crate::interaction_manager::InteractionManager> {
1738        self.interaction_manager.as_mut()
1739    }
1740
1741    // -- Event subscription (callback-based) ------------------------------
1742
1743    /// Subscribe to interaction events of a given kind.
1744    ///
1745    /// Returns a [`ListenerId`](crate::event_emitter::ListenerId) for
1746    /// later removal via [`off`](Self::off).
1747    pub fn on<F>(
1748        &mut self,
1749        kind: crate::interaction::InteractionEventKind,
1750        callback: F,
1751    ) -> crate::event_emitter::ListenerId
1752    where
1753        F: Fn(&crate::interaction::InteractionEvent) + Send + Sync + 'static,
1754    {
1755        self.event_emitter.on(kind, callback)
1756    }
1757
1758    /// Subscribe to a single occurrence of an interaction event kind.
1759    ///
1760    /// The listener is automatically removed after the first dispatch.
1761    pub fn once<F>(
1762        &mut self,
1763        kind: crate::interaction::InteractionEventKind,
1764        callback: F,
1765    ) -> crate::event_emitter::ListenerId
1766    where
1767        F: Fn(&crate::interaction::InteractionEvent) + Send + Sync + 'static,
1768    {
1769        self.event_emitter.once(kind, callback)
1770    }
1771
1772    /// Unsubscribe a previously registered event listener.
1773    pub fn off(&mut self, id: crate::event_emitter::ListenerId) -> bool {
1774        self.event_emitter.off(id)
1775    }
1776
1777    /// Dispatch interaction events to registered listeners.
1778    ///
1779    /// Call this after draining events from the interaction manager.
1780    /// Events are forwarded to all matching `on` / `once` callbacks.
1781    pub fn dispatch_events(&mut self, events: &[crate::interaction::InteractionEvent]) {
1782        self.event_emitter.dispatch(events);
1783    }
1784
1785    /// Mutable access to the event emitter for advanced use cases.
1786    pub fn event_emitter_mut(&mut self) -> &mut crate::event_emitter::EventEmitter {
1787        &mut self.event_emitter
1788    }
1789
1790    /// Invalidate placed-symbol tiles that depend on the given image id.
1791    pub fn invalidate_symbol_image_dependency(&mut self, image_id: &str) -> usize {
1792        self.invalidate_symbol_dependency_tiles(|deps| deps.images.contains(image_id))
1793    }
1794
1795    /// Invalidate placed-symbol tiles that depend on the given glyph.
1796    pub fn invalidate_symbol_glyph_dependency(
1797        &mut self,
1798        font_stack: &str,
1799        codepoint: char,
1800    ) -> usize {
1801        let glyph = crate::symbols::GlyphKey {
1802            font_stack: font_stack.to_owned(),
1803            codepoint,
1804        };
1805        self.invalidate_symbol_dependency_tiles(|deps| deps.glyphs.contains(&glyph))
1806    }
1807
1808    /// Override the vector meshes for this frame (useful for testing).
1809    pub fn set_vector_meshes(&mut self, meshes: Vec<VectorMeshData>) {
1810        let meshes = Arc::new(meshes);
1811        self.pending_vector_meshes = Some(meshes.clone());
1812        self.vector_meshes = meshes;
1813    }
1814
1815    /// Override the model instances for this frame (useful for testing).
1816    pub fn set_model_instances(&mut self, instances: Vec<ModelInstance>) {
1817        let instances = Arc::new(instances);
1818        self.pending_model_instances = Some(instances.clone());
1819        self.model_instances = instances;
1820    }
1821
1822    /// Override the placed symbols for this frame (useful for testing).
1823    pub fn set_placed_symbols(&mut self, symbols: Vec<PlacedSymbol>) {
1824        self.placed_symbols = Arc::new(symbols);
1825        self.symbol_assets
1826            .rebuild_from_symbols(&self.placed_symbols);
1827    }
1828
1829    // =====================================================================
1830    // Update cycle
1831    // =====================================================================
1832
1833    /// Recompute all derived state with an explicit delta-time (seconds).
1834    pub fn update_with_dt(&mut self, dt: f64) {
1835        let was_flying = self.animator.is_flying() || self.animator.is_easing();
1836
1837        self.update_camera(dt);
1838
1839        let is_flying = self.animator.is_flying() || self.animator.is_easing();
1840
1841        // -- Async path: dispatch + poll (cheap every frame) ----------------
1842        if self.async_pipeline.is_some() {
1843            self.dispatch_data_requests();
1844            self.poll_completed_results(dt);
1845        } else {
1846            // -- Synchronous path (fallback) ---------------------------------
1847            //
1848            // Tile layer updates (poll completed HTTP responses, recompute
1849            // the visible tile set, issue new requests) are cheap and must
1850            // run every frame so that:
1851            //   1. Completed tile downloads are picked up promptly.
1852            //   2. The visible tile set tracks the current camera position.
1853            //   3. Tiles transition from "pending" to "loaded" without lag.
1854            //
1855            // Expensive work (terrain meshing, vector tessellation, symbol
1856            // placement, model collection) is throttled during animation.
1857            let animation_active = is_flying;
1858
1859            // Always update tile layers every frame.
1860            self.update_tile_layers();
1861
1862            if animation_active && self.data_update_interval > 0.0 {
1863                self.data_update_elapsed += dt;
1864                if self.data_update_elapsed >= self.data_update_interval {
1865                    self.data_update_elapsed = 0.0;
1866                    self.update_heavy_layers(dt);
1867                }
1868            } else {
1869                self.data_update_elapsed = 0.0;
1870                self.update_heavy_layers(dt);
1871            }
1872        }
1873
1874        // Post-animation catch-up: ensure a full data update fires on the
1875        // first frame after a fly-to / ease-to animation finishes.
1876        if was_flying && !is_flying && self.async_pipeline.is_none() {
1877            self.update_tile_layers();
1878            self.update_heavy_layers(dt);
1879        }
1880
1881        // -- Style transition clock (lightweight, every frame) -------------
1882        self.style_time += dt.max(0.0);
1883
1884        // -- Loading placeholders (lightweight, every frame) ----------------
1885        self.placeholder_time += dt;
1886        self.loading_placeholders = Arc::new(PlaceholderGenerator::generate(
1887            &self.visible_tiles,
1888            &self.placeholder_style,
1889            self.placeholder_time,
1890        ));
1891
1892        if let Err(e) = self.sync_attached_style_runtime() {
1893            log::warn!("style sync error: {e:?}");
1894        }
1895
1896        self.apply_pending_frame_overrides();
1897
1898        // -- Recompute fog params for this frame ----------------------------
1899        {
1900            let bg = self.background_color().unwrap_or([1.0, 1.0, 1.0, 1.0]);
1901            let pitch = self.camera.pitch();
1902            let distance = self.camera.distance();
1903            let style_fog = self.style.as_ref().and_then(|s| s.document().fog());
1904            let effective_fog = self.fog_config.as_ref().or(style_fog);
1905            self.computed_fog = crate::style::compute_fog(pitch, distance, bg, effective_fog);
1906        }
1907
1908        // -- Recompute lighting params for this frame -----------------------
1909        {
1910            let style_lights = self.style.as_ref().and_then(|s| s.document().lights());
1911            let effective_lights = self.light_config.as_ref().or(style_lights);
1912            let default_lights = crate::style::LightConfig::default();
1913            let config = effective_lights.unwrap_or(&default_lights);
1914            self.computed_lighting = crate::style::compute_lighting(config);
1915
1916            // -- Recompute shadow cascades when enabled ---------------------
1917            if self.computed_lighting.shadows_enabled {
1918                let vp = self.camera.view_projection_matrix();
1919                let dir = self.computed_lighting.directional_dir;
1920                let cam_dist = self.camera.distance();
1921                self.computed_shadow =
1922                    crate::style::compute_shadow_cascades(&vp, dir, cam_dist, &config.shadow);
1923            } else {
1924                self.computed_shadow = crate::style::ComputedShadow::default();
1925            }
1926        }
1927
1928        // -- Recompute sky params for this frame ----------------------------
1929        {
1930            let style_sky = self.style.as_ref().and_then(|s| s.document().sky());
1931            let effective_sky = self.sky_config.as_ref().or(style_sky);
1932            if let Some(sky) = effective_sky {
1933                // Fallback sun direction from the effective light config.
1934                let style_lights = self.style.as_ref().and_then(|s| s.document().lights());
1935                let effective_lights = self.light_config.as_ref().or(style_lights);
1936                let fallback_sun = effective_lights
1937                    .map(|l| l.directional.direction)
1938                    .unwrap_or([210.0, 45.0]);
1939                self.computed_sky = crate::style::compute_sky(sky, fallback_sun);
1940            } else {
1941                self.computed_sky = crate::style::ComputedSky::default();
1942            }
1943        }
1944    }
1945
1946    /// Forward an input event to the camera controller.
1947    ///
1948    /// [`Touch`](InputEvent::Touch) events are routed through the
1949    /// built-in [`GestureRecognizer`](crate::gesture::GestureRecognizer),
1950    /// which converts multi-touch sequences into pan / zoom / rotate
1951    /// events before forwarding them to the camera controller.
1952    pub fn handle_input(&mut self, event: InputEvent) {
1953        if let InputEvent::Touch(contact) = event {
1954            let derived = self.gesture_recognizer.process(contact);
1955            for e in derived {
1956                CameraController::handle_event(&mut self.camera, e, &self.constraints);
1957            }
1958            return;
1959        }
1960        CameraController::handle_event(&mut self.camera, event, &self.constraints);
1961    }
1962
1963    /// Access the gesture recognizer (e.g. to call [`reset`](crate::gesture::GestureRecognizer::reset)).
1964    pub fn gesture_recognizer(&self) -> &crate::gesture::GestureRecognizer {
1965        &self.gesture_recognizer
1966    }
1967
1968    /// Convenience: calls `update_with_dt(1.0 / 60.0)`.
1969    pub fn update(&mut self) {
1970        self.update_with_dt(1.0 / 60.0);
1971    }
1972
1973    /// Begin a fly-to animation.
1974    pub fn fly_to(&mut self, options: crate::camera_animator::FlyToOptions) {
1975        self.animator.start_fly_to(&mut self.camera, &options);
1976    }
1977
1978    /// Begin an ease-to animation.
1979    pub fn ease_to(&mut self, options: crate::camera_animator::EaseToOptions) {
1980        self.animator.start_ease_to(&mut self.camera, &options);
1981    }
1982
1983    /// Immediately jump the camera to the given state.
1984    pub fn jump_to(
1985        &mut self,
1986        target: GeoCoord,
1987        distance: f64,
1988        pitch: Option<f64>,
1989        yaw: Option<f64>,
1990    ) {
1991        self.animator.cancel();
1992        self.camera.set_target(target);
1993        self.camera.set_distance(distance);
1994        if let Some(p) = pitch {
1995            self.camera.set_pitch(p);
1996        }
1997        if let Some(y) = yaw {
1998            self.camera.set_yaw(y);
1999        }
2000    }
2001
2002    /// Tick the camera animator and recompute zoom / viewport / frustum.
2003    pub fn update_camera(&mut self, dt: f64) {
2004        self.animator.tick(&mut self.camera, dt);
2005
2006        // Normalize the camera longitude after every animation tick to
2007        // keep it within [-180, 180).  Without this, fly-to and ease-to
2008        // animations that cross the antimeridian can leave the camera in
2009        // a different world copy, causing viewport_bounds and tile
2010        // selection to diverge and tiles to stop rendering.  This matches
2011        // the Mapbox/MapLibre wrap-jump handling behavior.
2012        {
2013            let target = *self.camera.target();
2014            let wrapped_lon = wrap_lon_180(target.lon);
2015            if (wrapped_lon - target.lon).abs() > 1e-12 {
2016                self.camera
2017                    .set_target(GeoCoord::from_lat_lon(target.lat, wrapped_lon));
2018            }
2019        }
2020
2021        let mpp = self.camera.meters_per_pixel();
2022        self.zoom_level = Self::mpp_to_zoom(mpp);
2023        self.viewport_bounds = self.compute_viewport_bounds();
2024        self.scene_viewport_bounds = self.compute_scene_viewport_bounds();
2025        self.frustum = Some(Frustum::from_view_projection(
2026            &self.camera.view_projection_matrix(),
2027        ));
2028        self.update_camera_motion_state(dt);
2029
2030        let fractional_zoom = self.fractional_zoom();
2031        self.camera_zoom_delta = self
2032            .previous_fractional_zoom
2033            .map_or(0.0, |previous| fractional_zoom - previous);
2034        self.previous_fractional_zoom = Some(fractional_zoom);
2035    }
2036
2037    /// Build a detached per-frame snapshot for renderers.
2038    pub fn frame_output(&self) -> FrameOutput {
2039        FrameOutput {
2040            view_projection: self.camera.view_projection_matrix(),
2041            frustum: self.frustum.clone(),
2042            tiles: Arc::clone(&self.visible_tiles),
2043            terrain: Arc::clone(&self.terrain_meshes),
2044            hillshade: Arc::clone(&self.hillshade_rasters),
2045            vectors: Arc::clone(&self.vector_meshes),
2046            models: Arc::clone(&self.model_instances),
2047            symbols: Arc::clone(&self.placed_symbols),
2048            visualization: Arc::clone(&self.visualization_overlays),
2049            placeholders: Arc::clone(&self.loading_placeholders),
2050            image_overlays: Arc::clone(&self.image_overlays),
2051            zoom_level: self.zoom_level,
2052        }
2053    }
2054
2055    /// Query the terrain elevation at a geographic coordinate.
2056    pub fn elevation_at(&self, coord: &GeoCoord) -> Option<f64> {
2057        self.terrain.elevation_at(coord)
2058    }
2059
2060    /// Unproject a screen pixel to a geographic coordinate (flat ground).
2061    pub fn screen_to_geo(&self, px: f64, py: f64) -> Option<GeoCoord> {
2062        self.camera.screen_to_geo(px, py)
2063    }
2064
2065    /// Project a geographic coordinate to a screen-space pixel position.
2066    ///
2067    /// Returns `(px, py)` in logical pixels with `(0, 0)` at the
2068    /// top-left corner of the viewport, or `None` if the point is
2069    /// behind the camera.
2070    pub fn geo_to_screen(&self, geo: &GeoCoord) -> Option<(f64, f64)> {
2071        self.camera.geo_to_screen(geo)
2072    }
2073
2074    /// Fit the camera to a geographic bounding box.
2075    ///
2076    /// Computes the center and zoom level that make the entire bounding
2077    /// box visible, accounting for optional padding.  Depending on
2078    /// `options.animate`, the transition may be animated (`fly_to`) or
2079    /// instant (`jump_to`).
2080    pub fn fit_bounds(&mut self, bounds: &GeoBounds, options: &FitBoundsOptions) {
2081        let center = bounds.center();
2082
2083        // Project SW and NE to world meters.
2084        let sw_world = WebMercator::project_clamped(&bounds.sw());
2085        let ne_world = WebMercator::project_clamped(&bounds.ne());
2086
2087        let dx = (ne_world.position.x - sw_world.position.x).abs();
2088        let dy = (ne_world.position.y - sw_world.position.y).abs();
2089
2090        // Subtract padding from the effective viewport.
2091        let vw =
2092            (self.camera.viewport_width() as f64 - options.padding.left - options.padding.right)
2093                .max(1.0);
2094        let vh =
2095            (self.camera.viewport_height() as f64 - options.padding.top - options.padding.bottom)
2096                .max(1.0);
2097
2098        // Meters-per-pixel required to fit the bounds.
2099        let mpp_x = dx / vw;
2100        let mpp_y = dy / vh;
2101        let mpp = mpp_x.max(mpp_y);
2102
2103        // Convert mpp to zoom.
2104        let zoom = if mpp <= 0.0 || !mpp.is_finite() {
2105            MAX_ZOOM as f64
2106        } else {
2107            (WGS84_CIRCUMFERENCE / (mpp * TILE_PX))
2108                .log2()
2109                .clamp(0.0, MAX_ZOOM as f64)
2110        };
2111
2112        // Clamp to max_zoom.
2113        let zoom = match options.max_zoom {
2114            Some(mz) => zoom.min(mz),
2115            None => zoom,
2116        };
2117
2118        if options.animate {
2119            let fly = crate::camera_animator::FlyToOptions {
2120                center: Some(center),
2121                zoom: Some(zoom),
2122                bearing: options.bearing,
2123                pitch: options.pitch,
2124                duration: options.duration,
2125                ..Default::default()
2126            };
2127            self.fly_to(fly);
2128        } else {
2129            // Convert zoom to distance.
2130            let is_perspective = matches!(self.camera.mode(), CameraMode::Perspective);
2131            let distance = {
2132                let mpp = WGS84_CIRCUMFERENCE / (2.0_f64.powf(zoom) * TILE_PX);
2133                let vis_h = mpp * self.camera.viewport_height().max(1) as f64;
2134                if is_perspective {
2135                    vis_h / (2.0 * (self.camera.fov_y() / 2.0).tan())
2136                } else {
2137                    vis_h / 2.0
2138                }
2139            };
2140            self.jump_to(center, distance, options.pitch, options.bearing);
2141        }
2142    }
2143
2144    /// Cast a ray and intersect with the flat ground plane (z = 0).
2145    pub fn ray_to_geo(&self, origin: glam::DVec3, direction: glam::DVec3) -> Option<GeoCoord> {
2146        if direction.z.abs() < 1e-12 {
2147            return None;
2148        }
2149        let t = -origin.z / direction.z;
2150        if t < 0.0 {
2151            return None;
2152        }
2153        let hit = origin + direction * t;
2154        let world = self.camera.target_world();
2155        let world_hit = WorldCoord::new(hit.x + world.x, hit.y + world.y, 0.0);
2156        Some(self.camera.projection().unproject(&world_hit))
2157    }
2158
2159    /// Cast a ray and intersect with the terrain surface.
2160    ///
2161    /// Falls back to flat ground intersection if terrain is disabled or
2162    /// no intersection is found.
2163    pub fn ray_to_geo_on_terrain(
2164        &self,
2165        origin: glam::DVec3,
2166        direction: glam::DVec3,
2167    ) -> Option<GeoCoord> {
2168        if !self.terrain.enabled() {
2169            return self.ray_to_geo(origin, direction);
2170        }
2171
2172        let world = self.camera.target_world();
2173        let steps = 64;
2174        let max_t = self.camera.distance() * 4.0;
2175        let step = max_t / steps as f64;
2176
2177        let mut prev_above = true;
2178        for i in 0..=steps {
2179            let t = step * i as f64;
2180            let p = origin + direction * t;
2181            let world_hit = WorldCoord::new(p.x + world.x, p.y + world.y, p.z);
2182            let geo = self.camera.projection().unproject(&world_hit);
2183            let elev = self.terrain.elevation_at(&geo).unwrap_or(0.0);
2184            let above = p.z >= elev;
2185            if !above && prev_above && i > 0 {
2186                // Linear interpolation between previous and current step.
2187                let prev_t = step * (i - 1) as f64;
2188                let prev_p = origin + direction * prev_t;
2189                let prev_world = WorldCoord::new(prev_p.x + world.x, prev_p.y + world.y, prev_p.z);
2190                let prev_geo = self.camera.projection().unproject(&prev_world);
2191                let prev_elev = self.terrain.elevation_at(&prev_geo).unwrap_or(0.0);
2192                let prev_height = prev_p.z - prev_elev;
2193                let curr_height = p.z - elev;
2194                let frac = prev_height / (prev_height - curr_height);
2195                let hit_t = prev_t + (t - prev_t) * frac;
2196                let hit = origin + direction * hit_t;
2197                let hit_world = WorldCoord::new(hit.x + world.x, hit.y + world.y, hit.z);
2198                return Some(self.camera.projection().unproject(&hit_world));
2199            }
2200            prev_above = above;
2201        }
2202        // Fallback to flat ground.
2203        self.ray_to_geo(origin, direction)
2204    }
2205
2206    /// Cast a ray and intersect with the flat ground plane, ignoring terrain.
2207    pub fn ray_to_flat_geo(&self, origin: glam::DVec3, direction: glam::DVec3) -> Option<GeoCoord> {
2208        self.ray_to_geo(origin, direction)
2209    }
2210
2211    /// Screen-to-geo through the terrain surface.
2212    pub fn screen_to_geo_on_terrain(&self, px: f64, py: f64) -> Option<GeoCoord> {
2213        let (origin, direction) = self.camera.screen_to_ray(px, py);
2214        self.ray_to_geo_on_terrain(origin, direction)
2215    }
2216
2217    // =====================================================================
2218    // Synchronous layer update (fallback when no async pipeline)
2219    // =====================================================================
2220
2221    // Full synchronous layer update (tile + heavy layers).
2222    //
2223    // Used by the non-animation path and the async dispatch path.
2224    // =====================================================================
2225    // Async dispatch + poll
2226    // =====================================================================
2227
2228    // =====================================================================
2229    // Private helpers
2230    // =====================================================================
2231
2232    /// Camera target in Web Mercator world coordinates.
2233    fn mercator_camera_world(&self) -> (f64, f64) {
2234        let w = WebMercator::project(self.camera.target());
2235        (w.position.x, w.position.y)
2236    }
2237
2238    fn update_camera_motion_state(&mut self, dt: f64) {
2239        self.camera_motion_time_seconds += dt.max(0.0);
2240
2241        let current_wrapped = self.mercator_camera_world();
2242        let current_target = if let Some(last) = self.camera_motion_samples.back() {
2243            glam::DVec2::new(
2244                last.target_world.x
2245                    + heavy_layers::wrapped_world_delta(current_wrapped.0 - last.target_world.x),
2246                current_wrapped.1,
2247            )
2248        } else {
2249            glam::DVec2::new(current_wrapped.0, current_wrapped.1)
2250        };
2251
2252        self.camera_motion_samples.push_back(CameraMotionSample {
2253            time_seconds: self.camera_motion_time_seconds,
2254            target_world: current_target,
2255        });
2256        self.trim_camera_motion_samples();
2257        self.recompute_camera_motion_state();
2258    }
2259
2260    fn trim_camera_motion_samples(&mut self) {
2261        let max_samples = self.camera_velocity_config.sample_window.max(1) + 1;
2262        while self.camera_motion_samples.len() > max_samples {
2263            self.camera_motion_samples.pop_front();
2264        }
2265    }
2266
2267    fn recompute_camera_motion_state(&mut self) {
2268        let pan_velocity_world = if let (Some(first), Some(last)) = (
2269            self.camera_motion_samples.front(),
2270            self.camera_motion_samples.back(),
2271        ) {
2272            let dt = last.time_seconds - first.time_seconds;
2273            if dt > 1e-9 {
2274                (last.target_world - first.target_world) / dt
2275            } else {
2276                glam::DVec2::ZERO
2277            }
2278        } else {
2279            glam::DVec2::ZERO
2280        };
2281
2282        let current_wrapped = self.mercator_camera_world();
2283        let current_target_world = glam::DVec2::new(current_wrapped.0, current_wrapped.1);
2284        let look_ahead = self.camera_velocity_config.look_ahead_seconds.max(0.0);
2285        let predicted_delta = pan_velocity_world * look_ahead;
2286
2287        self.camera_motion_state = CameraMotionState {
2288            pan_velocity_world,
2289            predicted_target_world: current_target_world + predicted_delta,
2290            predicted_viewport_bounds: heavy_layers::translated_world_bounds(
2291                &self.viewport_bounds,
2292                predicted_delta,
2293            ),
2294        };
2295    }
2296
2297    /// Camera target in the active scene projection.
2298    /// Convert meters-per-pixel to an integer zoom level.
2299    fn mpp_to_zoom(mpp: f64) -> u8 {
2300        if mpp <= 0.0 || !mpp.is_finite() {
2301            return MAX_ZOOM;
2302        }
2303        let z = (WGS84_CIRCUMFERENCE / (mpp * TILE_PX)).log2();
2304        (z.round() as u8).min(MAX_ZOOM)
2305    }
2306
2307    /// Compute the viewport bounding box in Web Mercator world coordinates.
2308    fn compute_viewport_bounds(&self) -> WorldBounds {
2309        use rustial_math::WebMercator;
2310
2311        let w = self.camera.viewport_width() as f64;
2312        let h = self.camera.viewport_height() as f64;
2313
2314        if w <= 0.0 || h <= 0.0 {
2315            let zero = WorldCoord::new(0.0, 0.0, 0.0);
2316            return WorldBounds::new(zero, zero);
2317        }
2318
2319        let cam = &self.camera;
2320
2321        // Check if camera supports orthographic bounds directly.
2322        if cam.mode() == CameraMode::Orthographic {
2323            let half_w = cam.distance() * (w / h).max(1.0);
2324            let half_h = cam.distance() / (w / h).min(1.0);
2325            let target = WebMercator::project(cam.target());
2326            let overscan = 1.3;
2327            return WorldBounds::new(
2328                WorldCoord::new(
2329                    target.position.x - half_w * overscan,
2330                    target.position.y - half_h * overscan,
2331                    0.0,
2332                ),
2333                WorldCoord::new(
2334                    target.position.x + half_w * overscan,
2335                    target.position.y + half_h * overscan,
2336                    0.0,
2337                ),
2338            );
2339        }
2340
2341        let mut min_x = f64::MAX;
2342        let mut min_y = f64::MAX;
2343        let mut max_x = f64::MIN;
2344        let mut max_y = f64::MIN;
2345        let mut any_hit = false;
2346
2347        for (sx, sy) in viewport_sample_points(w, h) {
2348            if let Some(geo) = cam.screen_to_geo(sx, sy) {
2349                let world = WebMercator::project_clamped(&geo);
2350                min_x = min_x.min(world.position.x);
2351                min_y = min_y.min(world.position.y);
2352                max_x = max_x.max(world.position.x);
2353                max_y = max_y.max(world.position.y);
2354                any_hit = true;
2355            }
2356        }
2357
2358        if !any_hit {
2359            let target = WebMercator::project(cam.target());
2360            let mpp = cam.meters_per_pixel();
2361            let half_w = mpp * w * 0.5;
2362            let half_h = mpp * h * 0.5;
2363            return WorldBounds::new(
2364                WorldCoord::new(target.position.x - half_w, target.position.y - half_h, 0.0),
2365                WorldCoord::new(target.position.x + half_w, target.position.y + half_h, 0.0),
2366            );
2367        }
2368
2369        let overscan = perspective_viewport_overscan(cam.pitch());
2370        let cx = (min_x + max_x) * 0.5;
2371        let cy = (min_y + max_y) * 0.5;
2372        let hw = (max_x - min_x) * 0.5 * overscan;
2373        let hh = (max_y - min_y) * 0.5 * overscan;
2374        WorldBounds::new(
2375            WorldCoord::new(cx - hw, cy - hh, 0.0),
2376            WorldCoord::new(cx + hw, cy + hh, 0.0),
2377        )
2378    }
2379
2380    /// Compute the viewport bounding box in the active scene projection.
2381    fn compute_scene_viewport_bounds(&self) -> WorldBounds {
2382        let w = self.camera.viewport_width() as f64;
2383        let h = self.camera.viewport_height() as f64;
2384
2385        if w <= 0.0 || h <= 0.0 {
2386            let zero = WorldCoord::new(0.0, 0.0, 0.0);
2387            return WorldBounds::new(zero, zero);
2388        }
2389
2390        let cam = &self.camera;
2391        let proj = cam.projection();
2392
2393        if cam.mode() == CameraMode::Orthographic {
2394            let half_w = cam.distance() * (w / h).max(1.0);
2395            let half_h = cam.distance() / (w / h).min(1.0);
2396            let target = proj.project(cam.target());
2397            let overscan = 1.3;
2398            return WorldBounds::new(
2399                WorldCoord::new(
2400                    target.position.x - half_w * overscan,
2401                    target.position.y - half_h * overscan,
2402                    0.0,
2403                ),
2404                WorldCoord::new(
2405                    target.position.x + half_w * overscan,
2406                    target.position.y + half_h * overscan,
2407                    0.0,
2408                ),
2409            );
2410        }
2411
2412        let mut min_x = f64::MAX;
2413        let mut min_y = f64::MAX;
2414        let mut max_x = f64::MIN;
2415        let mut max_y = f64::MIN;
2416        let mut any_hit = false;
2417
2418        for (sx, sy) in viewport_sample_points(w, h) {
2419            if let Some(geo) = cam.screen_to_geo(sx, sy) {
2420                let world = proj.project(&geo);
2421                min_x = min_x.min(world.position.x);
2422                min_y = min_y.min(world.position.y);
2423                max_x = max_x.max(world.position.x);
2424                max_y = max_y.max(world.position.y);
2425                any_hit = true;
2426            }
2427        }
2428
2429        if !any_hit {
2430            let target = proj.project(cam.target());
2431            let mpp = cam.meters_per_pixel();
2432            let half_w = mpp * w * 0.5;
2433            let half_h = mpp * h * 0.5;
2434            return WorldBounds::new(
2435                WorldCoord::new(target.position.x - half_w, target.position.y - half_h, 0.0),
2436                WorldCoord::new(target.position.x + half_w, target.position.y + half_h, 0.0),
2437            );
2438        }
2439
2440        let overscan = perspective_viewport_overscan(cam.pitch());
2441        let cx = (min_x + max_x) * 0.5;
2442        let cy = (min_y + max_y) * 0.5;
2443        let hw = (max_x - min_x) * 0.5 * overscan;
2444        let hh = (max_y - min_y) * 0.5 * overscan;
2445        WorldBounds::new(
2446            WorldCoord::new(cx - hw, cy - hh, 0.0),
2447            WorldCoord::new(cx + hw, cy + hh, 0.0),
2448        )
2449    }
2450
2451    /// Re-evaluate the attached style runtime at the current zoom.
2452    fn sync_attached_style_runtime(&mut self) -> Result<(), StyleError> {
2453        let Some(style) = self.style.as_ref() else {
2454            return Ok(());
2455        };
2456        let doc = style.document();
2457        let ctx = crate::style::StyleEvalContext {
2458            zoom: self.fractional_zoom() as f32,
2459        };
2460        let now = self.style_time;
2461
2462        // Walk each style layer and update transition state.
2463        for style_layer in doc.layers() {
2464            let (layer_id, transition_spec, opacity, color, secondary_color, width, height, base) =
2465                extract_transition_props(style_layer, ctx, &doc.transition());
2466            let state = self
2467                .layer_transitions
2468                .entry(layer_id.to_owned())
2469                .or_insert_with(|| {
2470                    crate::style::LayerTransitionState::from_initial(
2471                        transition_spec,
2472                        opacity,
2473                        color,
2474                        secondary_color,
2475                        width,
2476                        height,
2477                        base,
2478                    )
2479                });
2480            state.update(now, opacity, color, secondary_color, width, height, base);
2481        }
2482        Ok(())
2483    }
2484
2485    /// Replace an existing layer with the same name or push a new one.
2486    fn replace_or_push_named_layer(&mut self, name: &str, layer: Box<dyn crate::layer::Layer>) {
2487        if let Some(index) = self.layers.index_of(name) {
2488            let _ = self.layers.remove(index);
2489            self.layers.insert(index, layer);
2490        } else {
2491            self.layers.push(layer);
2492        }
2493        self.style = None;
2494    }
2495}
2496
2497// =========================================================================
2498// Free helper functions
2499// =========================================================================
2500
2501/// Extract the transitionable paint-property values from a [`StyleLayer`]
2502/// at the given zoom.
2503///
2504/// Returns `(id, spec, opacity, color, secondary_color, width, height, base)`.
2505fn extract_transition_props<'a>(
2506    layer: &'a crate::style::StyleLayer,
2507    ctx: crate::style::StyleEvalContext,
2508    global_transition: &crate::style::TransitionSpec,
2509) -> (
2510    &'a str,
2511    crate::style::TransitionSpec,
2512    f32,
2513    [f32; 4],
2514    [f32; 4],
2515    f32,
2516    f32,
2517    f32,
2518) {
2519    use crate::style::StyleLayer;
2520
2521    let meta = layer.meta();
2522    let spec = if meta.transition.is_active() {
2523        meta.transition
2524    } else {
2525        *global_transition
2526    };
2527    let opacity = meta.opacity.evaluate_with_context(ctx);
2528
2529    let transparent: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
2530    let (color, secondary_color, width, height, base) = match layer {
2531        StyleLayer::Fill(f) => (
2532            f.fill_color.evaluate_with_context(ctx),
2533            f.outline_color.evaluate_with_context(ctx),
2534            f.outline_width.evaluate_with_context(ctx),
2535            0.0,
2536            0.0,
2537        ),
2538        StyleLayer::Line(l) => (
2539            l.color.evaluate_with_context(ctx),
2540            transparent,
2541            l.width.evaluate_with_context(ctx),
2542            0.0,
2543            0.0,
2544        ),
2545        StyleLayer::Circle(c) => (
2546            c.color.evaluate_with_context(ctx),
2547            c.stroke_color.evaluate_with_context(ctx),
2548            c.radius.evaluate_with_context(ctx),
2549            0.0,
2550            0.0,
2551        ),
2552        StyleLayer::FillExtrusion(e) => (
2553            e.color.evaluate_with_context(ctx),
2554            transparent,
2555            0.0,
2556            e.height.evaluate_with_context(ctx),
2557            e.base.evaluate_with_context(ctx),
2558        ),
2559        StyleLayer::Symbol(s) => (
2560            s.color.evaluate_with_context(ctx),
2561            s.halo_color.evaluate_with_context(ctx),
2562            s.size.evaluate_with_context(ctx),
2563            0.0,
2564            0.0,
2565        ),
2566        StyleLayer::Heatmap(h) => (
2567            h.color.evaluate_with_context(ctx),
2568            transparent,
2569            h.radius.evaluate_with_context(ctx),
2570            0.0,
2571            0.0,
2572        ),
2573        StyleLayer::Background(b) => (
2574            b.color.evaluate_with_context(ctx),
2575            transparent,
2576            0.0,
2577            0.0,
2578            0.0,
2579        ),
2580        _ => (transparent, transparent, 0.0, 0.0, 0.0),
2581    };
2582
2583    (
2584        meta.id.as_str(),
2585        spec,
2586        opacity,
2587        color,
2588        secondary_color,
2589        width,
2590        height,
2591        base,
2592    )
2593}