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