use crate::async_data::{
AsyncDataPipeline, DataTaskPool, TerrainTaskInput, VectorCacheKey, VectorTaskInput,
};
use crate::camera::{Camera, CameraConstraints, CameraController, CameraMode};
use crate::camera_animator::CameraAnimator;
use crate::camera_projection::CameraProjection;
use crate::geo_wrap::wrap_lon_180;
use crate::geometry::{FeatureCollection, PropertyValue};
use crate::input::InputEvent;
use crate::layer::Layer;
use crate::layer::LayerId;
use crate::layers::{FeatureProvenance, LayerStack, VectorMeshData, VectorStyle};
use crate::loading_placeholder::{LoadingPlaceholder, PlaceholderGenerator, PlaceholderStyle};
use crate::models::ModelInstance;
use crate::picking::{HitCategory, HitProvenance, PickHit, PickOptions, PickQuery, PickResult};
use crate::query::{
feature_id_for_feature, geometry_hit_distance, FeatureState, FeatureStateId, QueriedFeature,
QueryOptions,
};
use crate::streamed_payload::{
collect_affected_symbol_payloads, prune_affected_symbol_payloads,
resolve_streamed_vector_layer_refresh, symbol_query_payloads_from_optional,
StreamedPayloadView, StreamedSymbolPayloadKey, StreamedVectorLayerRefreshSpec,
SymbolDependencyPayload, SymbolQueryPayload, TileQueryPayload, VisiblePlacedSymbolView,
};
use crate::style::{MapStyle, StyleDocument, StyleError};
use crate::symbols::{PlacedSymbol, SymbolAssetRegistry, SymbolPlacementEngine};
use crate::terrain::{PreparedHillshadeRaster, TerrainConfig, TerrainManager, TerrainMeshData};
use crate::tile_cache::TileCacheStats;
use crate::tile_lifecycle::TileLifecycleDiagnostics;
use crate::tile_manager::{TileManagerCounters, TileSelectionStats};
use crate::tile_manager::{VisibleTile, ZoomPrefetchDirection};
use crate::tile_request_coordinator::{
CoordinatorConfig, CoordinatorStats, SourcePriority, TileRequestCoordinator,
};
use crate::tile_source::TileSourceDiagnostics;
use rustial_math::{
visible_tiles, visible_tiles_flat_view_with_config, FlatTileSelectionConfig, Frustum,
GeoBounds, GeoCoord, TileId, WebMercator, WorldBounds, WorldCoord, MAX_ZOOM,
};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
mod async_pipeline;
mod heavy_layers;
mod picking;
#[cfg(test)]
mod tests;
mod tile_selection;
const WGS84_CIRCUMFERENCE: f64 = 2.0 * std::f64::consts::PI * 6_378_137.0;
const TILE_PX: f64 = 256.0;
const SPECULATIVE_PREFETCH_BUDGET_FRACTION: f64 = 0.25;
const DEFAULT_SPECULATIVE_PREFETCH_REQUEST_BUDGET: usize = 8;
const ZOOM_DIRECTION_PREFETCH_THRESHOLD: f64 = 0.01;
fn viewport_sample_points(width: f64, height: f64) -> Vec<(f64, f64)> {
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];
let mut samples = Vec::with_capacity(FRACTIONS.len() * FRACTIONS.len());
for fy in FRACTIONS {
for fx in FRACTIONS {
samples.push((width * fx, height * fy));
}
}
samples
}
fn perspective_viewport_overscan(pitch: f64) -> f64 {
let normalized_pitch = (pitch / std::f64::consts::FRAC_PI_2).clamp(0.0, 1.0);
1.3 + 0.5 * normalized_pitch
}
fn terrain_base_tile_budget(required_tiles: usize) -> usize {
required_tiles.clamp(80, 256)
}
fn terrain_horizon_tile_budget(base_budget: usize, pitch: f64) -> usize {
if pitch <= 0.5 {
0
} else {
(base_budget / 3).clamp(24, 96)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FitBoundsPadding {
pub top: f64,
pub bottom: f64,
pub left: f64,
pub right: f64,
}
impl Default for FitBoundsPadding {
fn default() -> Self {
Self {
top: 0.0,
bottom: 0.0,
left: 0.0,
right: 0.0,
}
}
}
impl FitBoundsPadding {
pub fn uniform(px: f64) -> Self {
Self {
top: px,
bottom: px,
left: px,
right: px,
}
}
}
#[derive(Debug, Clone)]
pub struct FitBoundsOptions {
pub padding: FitBoundsPadding,
pub max_zoom: Option<f64>,
pub animate: bool,
pub duration: Option<f64>,
pub bearing: Option<f64>,
pub pitch: Option<f64>,
}
impl Default for FitBoundsOptions {
fn default() -> Self {
Self {
padding: FitBoundsPadding::default(),
max_zoom: None,
animate: true,
duration: None,
bearing: None,
pitch: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct SyncVectorCacheKey {
layer_id: LayerId,
style_fingerprint: u64,
data_generation: u64,
projection: CameraProjection,
}
#[derive(Clone)]
struct SyncVectorCacheEntry {
mesh: VectorMeshData,
}
#[derive(Debug, Default)]
pub struct FrameOutput {
pub view_projection: glam::DMat4,
pub frustum: Option<Frustum>,
pub tiles: Arc<Vec<VisibleTile>>,
pub terrain: Arc<Vec<TerrainMeshData>>,
pub hillshade: Arc<Vec<PreparedHillshadeRaster>>,
pub vectors: Arc<Vec<VectorMeshData>>,
pub models: Arc<Vec<ModelInstance>>,
pub symbols: Arc<Vec<PlacedSymbol>>,
pub visualization: Arc<Vec<crate::visualization::VisualizationOverlay>>,
pub placeholders: Arc<Vec<LoadingPlaceholder>>,
pub image_overlays: Arc<Vec<crate::layers::ImageOverlayData>>,
pub zoom_level: u8,
}
#[derive(Debug, Clone, Default)]
pub struct TilePipelineDiagnostics {
pub layer_name: String,
pub visible_tiles: usize,
pub visible_loaded_tiles: usize,
pub visible_fallback_tiles: usize,
pub visible_missing_tiles: usize,
pub visible_overzoomed_tiles: usize,
pub selection_stats: TileSelectionStats,
pub counters: TileManagerCounters,
pub cache_stats: TileCacheStats,
pub source_diagnostics: Option<TileSourceDiagnostics>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CameraVelocityConfig {
pub sample_window: usize,
pub look_ahead_seconds: f64,
}
impl Default for CameraVelocityConfig {
fn default() -> Self {
Self {
sample_window: 6,
look_ahead_seconds: 0.5,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct CameraMotionSample {
time_seconds: f64,
target_world: glam::DVec2,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CameraMotionState {
pub pan_velocity_world: glam::DVec2,
pub predicted_target_world: glam::DVec2,
pub predicted_viewport_bounds: WorldBounds,
}
impl Default for CameraMotionState {
fn default() -> Self {
let zero = WorldCoord::new(0.0, 0.0, 0.0);
Self {
pan_velocity_world: glam::DVec2::ZERO,
predicted_target_world: glam::DVec2::ZERO,
predicted_viewport_bounds: WorldBounds::new(zero, zero),
}
}
}
pub struct MapState {
camera: Camera,
constraints: CameraConstraints,
pub(crate) layers: LayerStack,
style: Option<MapStyle>,
pub(crate) terrain: TerrainManager,
animator: CameraAnimator,
data_update_interval: f64,
data_update_elapsed: f64,
async_pipeline: Option<AsyncDataPipeline>,
zoom_level: u8,
viewport_bounds: WorldBounds,
scene_viewport_bounds: WorldBounds,
frustum: Option<Frustum>,
terrain_meshes: Arc<Vec<TerrainMeshData>>,
pending_terrain_meshes: Option<Arc<Vec<TerrainMeshData>>>,
hillshade_rasters: Arc<Vec<PreparedHillshadeRaster>>,
visible_tiles: Arc<Vec<VisibleTile>>,
pub(crate) vector_meshes: Arc<Vec<VectorMeshData>>,
pending_vector_meshes: Option<Arc<Vec<VectorMeshData>>>,
pub(crate) model_instances: Arc<Vec<ModelInstance>>,
pending_model_instances: Option<Arc<Vec<ModelInstance>>>,
placed_symbols: Arc<Vec<PlacedSymbol>>,
symbol_assets: SymbolAssetRegistry,
symbol_placement: SymbolPlacementEngine,
feature_state: HashMap<FeatureStateId, FeatureState>,
streamed_vector_sources: HashMap<String, crate::layers::TileLayer>,
streamed_vector_layer_fingerprints: HashMap<String, u64>,
streamed_vector_query_payloads: HashMap<String, Vec<TileQueryPayload>>,
streamed_symbol_query_payloads: HashMap<String, Vec<SymbolQueryPayload>>,
streamed_symbol_dependency_payloads: HashMap<String, Vec<SymbolDependencyPayload>>,
dirty_streamed_symbol_layers: HashSet<String>,
dirty_streamed_symbol_tiles: HashMap<String, HashSet<TileId>>,
visualization_overlays: Arc<Vec<crate::visualization::VisualizationOverlay>>,
image_overlays: Arc<Vec<crate::layers::ImageOverlayData>>,
placeholder_style: PlaceholderStyle,
loading_placeholders: Arc<Vec<LoadingPlaceholder>>,
placeholder_time: f64,
interaction_manager: Option<crate::interaction_manager::InteractionManager>,
sync_vector_cache: HashMap<SyncVectorCacheKey, SyncVectorCacheEntry>,
request_coordinator: TileRequestCoordinator,
camera_velocity_config: CameraVelocityConfig,
camera_motion_time_seconds: f64,
camera_motion_samples: VecDeque<CameraMotionSample>,
camera_motion_state: CameraMotionState,
camera_zoom_delta: f64,
previous_fractional_zoom: Option<f64>,
prefetch_route: Option<Vec<GeoCoord>>,
event_emitter: crate::event_emitter::EventEmitter,
gesture_recognizer: crate::gesture::GestureRecognizer,
fog_config: Option<crate::style::FogConfig>,
computed_fog: crate::style::ComputedFog,
light_config: Option<crate::style::LightConfig>,
computed_lighting: crate::style::ComputedLighting,
computed_shadow: crate::style::ComputedShadow,
sky_config: Option<crate::style::SkyConfig>,
computed_sky: crate::style::ComputedSky,
style_time: f64,
layer_transitions:
std::collections::HashMap<crate::style::StyleLayerId, crate::style::LayerTransitionState>,
}
impl Default for MapState {
fn default() -> Self {
let zero = WorldCoord::new(0.0, 0.0, 0.0);
Self {
camera: Camera::default(),
constraints: CameraConstraints::default(),
layers: LayerStack::new(),
style: None,
terrain: TerrainManager::new(TerrainConfig::default(), 256),
animator: CameraAnimator::new(),
data_update_interval: 0.15,
data_update_elapsed: 0.0,
async_pipeline: None,
zoom_level: 0,
viewport_bounds: WorldBounds::new(zero, zero),
scene_viewport_bounds: WorldBounds::new(zero, zero),
frustum: None,
terrain_meshes: Arc::new(Vec::new()),
pending_terrain_meshes: None,
hillshade_rasters: Arc::new(Vec::new()),
visible_tiles: Arc::new(Vec::new()),
vector_meshes: Arc::new(Vec::new()),
pending_vector_meshes: None,
model_instances: Arc::new(Vec::new()),
pending_model_instances: None,
placed_symbols: Arc::new(Vec::new()),
symbol_assets: SymbolAssetRegistry::new(),
symbol_placement: SymbolPlacementEngine::new(),
feature_state: HashMap::new(),
streamed_vector_sources: HashMap::new(),
streamed_vector_layer_fingerprints: HashMap::new(),
streamed_vector_query_payloads: HashMap::new(),
streamed_symbol_query_payloads: HashMap::new(),
streamed_symbol_dependency_payloads: HashMap::new(),
dirty_streamed_symbol_layers: HashSet::new(),
dirty_streamed_symbol_tiles: HashMap::new(),
visualization_overlays: Arc::new(Vec::new()),
image_overlays: Arc::new(Vec::new()),
placeholder_style: PlaceholderStyle::default(),
loading_placeholders: Arc::new(Vec::new()),
placeholder_time: 0.0,
interaction_manager: None,
sync_vector_cache: HashMap::new(),
request_coordinator: TileRequestCoordinator::default(),
camera_velocity_config: CameraVelocityConfig::default(),
camera_motion_time_seconds: 0.0,
camera_motion_samples: VecDeque::new(),
camera_motion_state: CameraMotionState::default(),
camera_zoom_delta: 0.0,
previous_fractional_zoom: None,
prefetch_route: None,
event_emitter: crate::event_emitter::EventEmitter::new(),
gesture_recognizer: crate::gesture::GestureRecognizer::new(),
fog_config: None,
computed_fog: crate::style::ComputedFog::default(),
light_config: None,
computed_lighting: crate::style::ComputedLighting::default(),
computed_shadow: crate::style::ComputedShadow::default(),
sky_config: None,
computed_sky: crate::style::ComputedSky::default(),
style_time: 0.0,
layer_transitions: std::collections::HashMap::new(),
}
}
}
impl MapState {
pub fn new() -> Self {
Self::default()
}
pub fn with_terrain(terrain_config: TerrainConfig, terrain_cache_size: usize) -> Self {
let zero = WorldCoord::new(0.0, 0.0, 0.0);
Self {
camera: Camera::default(),
constraints: CameraConstraints::default(),
layers: LayerStack::new(),
style: None,
terrain: TerrainManager::new(terrain_config, terrain_cache_size),
animator: CameraAnimator::new(),
data_update_interval: 0.15,
data_update_elapsed: 0.0,
async_pipeline: None,
zoom_level: 0,
viewport_bounds: WorldBounds::new(zero, zero),
scene_viewport_bounds: WorldBounds::new(zero, zero),
frustum: None,
terrain_meshes: Arc::new(Vec::new()),
pending_terrain_meshes: None,
hillshade_rasters: Arc::new(Vec::new()),
visible_tiles: Arc::new(Vec::new()),
vector_meshes: Arc::new(Vec::new()),
pending_vector_meshes: None,
model_instances: Arc::new(Vec::new()),
pending_model_instances: None,
placed_symbols: Arc::new(Vec::new()),
symbol_assets: SymbolAssetRegistry::new(),
symbol_placement: SymbolPlacementEngine::new(),
feature_state: HashMap::new(),
streamed_vector_sources: HashMap::new(),
streamed_vector_layer_fingerprints: HashMap::new(),
streamed_vector_query_payloads: HashMap::new(),
streamed_symbol_query_payloads: HashMap::new(),
streamed_symbol_dependency_payloads: HashMap::new(),
dirty_streamed_symbol_layers: HashSet::new(),
dirty_streamed_symbol_tiles: HashMap::new(),
visualization_overlays: Arc::new(Vec::new()),
image_overlays: Arc::new(Vec::new()),
placeholder_style: PlaceholderStyle::default(),
loading_placeholders: Arc::new(Vec::new()),
placeholder_time: 0.0,
interaction_manager: None,
sync_vector_cache: HashMap::new(),
request_coordinator: TileRequestCoordinator::default(),
camera_velocity_config: CameraVelocityConfig::default(),
camera_motion_time_seconds: 0.0,
camera_motion_samples: VecDeque::new(),
camera_motion_state: CameraMotionState::default(),
camera_zoom_delta: 0.0,
previous_fractional_zoom: None,
prefetch_route: None,
event_emitter: crate::event_emitter::EventEmitter::new(),
gesture_recognizer: crate::gesture::GestureRecognizer::new(),
fog_config: None,
computed_fog: crate::style::ComputedFog::default(),
light_config: None,
computed_lighting: crate::style::ComputedLighting::default(),
computed_shadow: crate::style::ComputedShadow::default(),
sky_config: None,
computed_sky: crate::style::ComputedSky::default(),
style_time: 0.0,
layer_transitions: std::collections::HashMap::new(),
}
}
#[inline]
pub fn zoom_level(&self) -> u8 {
self.zoom_level
}
#[inline]
pub fn fractional_zoom(&self) -> f64 {
let mpp = self.camera.near_meters_per_pixel();
if mpp <= 0.0 || !mpp.is_finite() {
return MAX_ZOOM as f64;
}
(WGS84_CIRCUMFERENCE / (mpp * TILE_PX))
.log2()
.clamp(0.0, MAX_ZOOM as f64)
}
#[inline]
pub fn viewport_bounds(&self) -> &WorldBounds {
&self.viewport_bounds
}
#[inline]
pub fn camera_velocity_config(&self) -> &CameraVelocityConfig {
&self.camera_velocity_config
}
pub fn set_camera_velocity_config(&mut self, config: CameraVelocityConfig) {
self.camera_velocity_config = config;
self.trim_camera_motion_samples();
self.recompute_camera_motion_state();
}
#[inline]
pub fn camera_motion_state(&self) -> &CameraMotionState {
&self.camera_motion_state
}
#[inline]
pub fn predicted_viewport_bounds(&self) -> &WorldBounds {
&self.camera_motion_state.predicted_viewport_bounds
}
#[inline]
pub fn scene_viewport_bounds(&self) -> &WorldBounds {
&self.scene_viewport_bounds
}
#[inline]
pub fn frustum(&self) -> Option<&Frustum> {
self.frustum.as_ref()
}
#[inline]
pub fn renderer_world_origin(&self) -> glam::DVec3 {
let (x, y) = self.mercator_camera_world();
glam::DVec3::new(x, y, 0.0)
}
#[inline]
pub fn scene_world_origin(&self) -> glam::DVec3 {
self.camera.target_world()
}
#[inline]
pub fn terrain_meshes(&self) -> &[TerrainMeshData] {
&self.terrain_meshes
}
#[inline]
pub fn hillshade_rasters(&self) -> &[PreparedHillshadeRaster] {
&self.hillshade_rasters
}
#[inline]
pub fn visible_tiles(&self) -> &[VisibleTile] {
&self.visible_tiles
}
#[inline]
pub fn loading_placeholders(&self) -> &[LoadingPlaceholder] {
&self.loading_placeholders
}
#[inline]
pub fn placeholder_style(&self) -> &PlaceholderStyle {
&self.placeholder_style
}
pub fn set_placeholder_style(&mut self, style: PlaceholderStyle) {
self.placeholder_style = style;
}
pub fn coordinator_stats(&self) -> &CoordinatorStats {
self.request_coordinator.stats()
}
pub fn coordinator_config(&self) -> &CoordinatorConfig {
self.request_coordinator.config()
}
pub fn set_coordinator_config(&mut self, config: CoordinatorConfig) {
self.request_coordinator.set_config(config);
}
pub fn set_prefetch_route(&mut self, route: Vec<GeoCoord>) {
if route.len() < 2 {
self.prefetch_route = None;
} else {
self.prefetch_route = Some(route);
}
}
pub fn clear_prefetch_route(&mut self) {
self.prefetch_route = None;
}
#[inline]
pub fn prefetch_route(&self) -> Option<&[GeoCoord]> {
self.prefetch_route.as_deref()
}
pub fn tile_pipeline_diagnostics(&self) -> Option<TilePipelineDiagnostics> {
use crate::layers::TileLayer;
for layer in self.layers.iter() {
if !layer.visible() {
continue;
}
let Some(tile_layer) = layer.as_any().downcast_ref::<TileLayer>() else {
continue;
};
let visible = tile_layer.visible_tiles();
let visible_loaded_tiles = visible
.tiles
.iter()
.filter(|tile| tile.data.is_some())
.count();
let visible_fallback_tiles = visible
.tiles
.iter()
.filter(|tile| tile.is_fallback() && tile.data.is_some())
.count();
let visible_missing_tiles = visible
.tiles
.iter()
.filter(|tile| tile.data.is_none())
.count();
let visible_overzoomed_tiles = visible
.tiles
.iter()
.filter(|tile| tile.is_overzoomed())
.count();
return Some(TilePipelineDiagnostics {
layer_name: tile_layer.name().to_owned(),
visible_tiles: visible.len(),
visible_loaded_tiles,
visible_fallback_tiles,
visible_missing_tiles,
visible_overzoomed_tiles,
selection_stats: tile_layer.last_selection_stats().clone(),
counters: tile_layer.counters().clone(),
cache_stats: tile_layer.cache_stats(),
source_diagnostics: tile_layer.source_diagnostics(),
});
}
None
}
pub fn tile_lifecycle_diagnostics(&self) -> Option<TileLifecycleDiagnostics> {
use crate::layers::TileLayer;
for layer in self.layers.iter() {
if !layer.visible() {
continue;
}
let Some(tile_layer) = layer.as_any().downcast_ref::<TileLayer>() else {
continue;
};
return Some(tile_layer.lifecycle_diagnostics());
}
None
}
pub fn background_color(&self) -> Option<[f32; 4]> {
use crate::layers::BackgroundLayer;
let mut color = None;
for layer in self.layers.iter() {
if !layer.visible() {
continue;
}
if let Some(background) = layer.as_any().downcast_ref::<BackgroundLayer>() {
color = Some(background.effective_color());
}
}
color
}
pub fn hillshade(&self) -> Option<crate::layers::HillshadeParams> {
use crate::layers::HillshadeLayer;
let mut params = None;
for layer in self.layers.iter() {
if !layer.visible() {
continue;
}
if let Some(hillshade) = layer.as_any().downcast_ref::<HillshadeLayer>() {
params = Some(hillshade.effective_params());
}
}
params
}
pub fn set_fog(&mut self, config: Option<crate::style::FogConfig>) {
self.fog_config = config;
}
pub fn fog(&self) -> Option<&crate::style::FogConfig> {
self.fog_config.as_ref()
}
pub fn computed_fog(&self) -> &crate::style::ComputedFog {
&self.computed_fog
}
pub fn set_lights(&mut self, config: Option<crate::style::LightConfig>) {
self.light_config = config;
}
pub fn lights(&self) -> Option<&crate::style::LightConfig> {
self.light_config.as_ref()
}
pub fn computed_lighting(&self) -> &crate::style::ComputedLighting {
&self.computed_lighting
}
pub fn computed_shadow(&self) -> &crate::style::ComputedShadow {
&self.computed_shadow
}
pub fn set_sky(&mut self, config: Option<crate::style::SkyConfig>) {
self.sky_config = config;
}
pub fn sky(&self) -> Option<&crate::style::SkyConfig> {
self.sky_config.as_ref()
}
pub fn computed_sky(&self) -> &crate::style::ComputedSky {
&self.computed_sky
}
pub fn style_time(&self) -> f64 {
self.style_time
}
pub fn layer_transitions(
&self,
) -> &std::collections::HashMap<crate::style::StyleLayerId, crate::style::LayerTransitionState>
{
&self.layer_transitions
}
pub fn resolved_transitions(
&self,
layer_id: &str,
) -> Option<crate::style::ResolvedTransitions> {
self.layer_transitions
.get(layer_id)
.map(|ts| ts.resolve(self.style_time))
}
pub fn set_visible_tiles(&mut self, tiles: Vec<VisibleTile>) {
self.visible_tiles = Arc::new(tiles);
}
pub fn set_terrain_meshes(&mut self, meshes: Vec<TerrainMeshData>) {
let meshes = Arc::new(meshes);
self.pending_terrain_meshes = Some(meshes.clone());
self.terrain_meshes = meshes;
}
pub fn set_hillshade_rasters(&mut self, rasters: Vec<PreparedHillshadeRaster>) {
self.hillshade_rasters = Arc::new(rasters);
}
#[inline]
pub fn camera(&self) -> &Camera {
&self.camera
}
pub fn set_viewport(&mut self, width: u32, height: u32) {
self.camera.set_viewport(width, height);
}
pub fn set_camera_target(&mut self, target: GeoCoord) {
if !target.lat.is_finite() || !target.lon.is_finite() {
return;
}
let wrapped = GeoCoord::from_lat_lon(target.lat, wrap_lon_180(target.lon));
self.camera.set_target(wrapped);
}
pub fn set_camera_distance(&mut self, distance: f64) {
if !distance.is_finite() || distance <= 0.0 {
return;
}
self.camera.set_distance(distance);
}
pub fn set_camera_pitch(&mut self, pitch: f64) {
if !pitch.is_finite() {
return;
}
self.camera.set_pitch(pitch);
}
pub fn set_camera_yaw(&mut self, yaw: f64) {
if !yaw.is_finite() {
return;
}
self.camera.set_yaw(yaw);
}
pub fn set_camera_mode(&mut self, mode: CameraMode) {
self.camera.set_mode(mode);
}
pub fn set_camera_projection(&mut self, projection: CameraProjection) {
self.camera.set_projection(projection);
}
pub fn set_camera_fov_y(&mut self, fov_y: f64) {
if !fov_y.is_finite() || fov_y <= 0.0 {
return;
}
self.camera.set_fov_y(fov_y);
}
#[inline]
pub fn constraints(&self) -> &CameraConstraints {
&self.constraints
}
pub fn set_max_pitch(&mut self, max_pitch: f64) {
if !max_pitch.is_finite() || max_pitch <= 0.0 {
return;
}
self.constraints.max_pitch = max_pitch;
}
pub fn set_min_distance(&mut self, min_distance: f64) {
if !min_distance.is_finite() || min_distance <= 0.0 {
return;
}
self.constraints.min_distance = min_distance;
}
pub fn set_max_distance(&mut self, max_distance: f64) {
if !max_distance.is_finite() || max_distance <= 0.0 {
return;
}
self.constraints.max_distance = max_distance;
}
#[inline]
pub fn layers(&self) -> &LayerStack {
&self.layers
}
pub fn push_layer(&mut self, layer: Box<dyn crate::layer::Layer>) {
self.layers.push(layer);
self.style = None;
}
pub fn set_grid_scalar(
&mut self,
name: impl Into<String>,
grid: crate::visualization::GeoGrid,
field: crate::visualization::ScalarField2D,
ramp: crate::visualization::ColorRamp,
) {
let name = name.into();
if let Some(index) = self.layers.index_of(&name) {
if let Some(layer) = self.layers.get_mut(index) {
if let Some(existing) = layer
.as_any_mut()
.downcast_mut::<crate::visualization::GridScalarLayer>()
{
existing.grid = grid;
existing.field = field;
existing.ramp = ramp;
self.style = None;
return;
}
}
}
let layer = Box::new(crate::visualization::GridScalarLayer::new(
name.clone(),
grid,
field,
ramp,
));
self.replace_or_push_named_layer(&name, layer);
}
pub fn set_grid_extrusion(
&mut self,
name: impl Into<String>,
grid: crate::visualization::GeoGrid,
field: crate::visualization::ScalarField2D,
ramp: crate::visualization::ColorRamp,
params: crate::visualization::ExtrusionParams,
) {
let name = name.into();
if let Some(index) = self.layers.index_of(&name) {
if let Some(layer) = self.layers.get_mut(index) {
if let Some(existing) = layer
.as_any_mut()
.downcast_mut::<crate::visualization::GridExtrusionLayer>()
{
existing.grid = grid;
existing.field = field;
existing.ramp = ramp;
existing.params = params;
self.style = None;
return;
}
}
}
let layer = Box::new(
crate::visualization::GridExtrusionLayer::new(name.clone(), grid, field, ramp)
.with_params(params),
);
self.replace_or_push_named_layer(&name, layer);
}
pub fn set_instanced_columns(
&mut self,
name: impl Into<String>,
columns: crate::visualization::ColumnInstanceSet,
ramp: crate::visualization::ColorRamp,
) {
let name = name.into();
if let Some(index) = self.layers.index_of(&name) {
if let Some(layer) = self.layers.get_mut(index) {
if let Some(existing) = layer
.as_any_mut()
.downcast_mut::<crate::visualization::InstancedColumnLayer>()
{
existing.columns = columns;
existing.ramp = ramp;
self.style = None;
return;
}
}
}
let layer = Box::new(crate::visualization::InstancedColumnLayer::new(
name.clone(),
columns,
ramp,
));
self.replace_or_push_named_layer(&name, layer);
}
pub fn set_point_cloud(
&mut self,
name: impl Into<String>,
points: crate::visualization::PointInstanceSet,
ramp: crate::visualization::ColorRamp,
) {
let name = name.into();
if let Some(index) = self.layers.index_of(&name) {
if let Some(layer) = self.layers.get_mut(index) {
if let Some(existing) = layer
.as_any_mut()
.downcast_mut::<crate::visualization::PointCloudLayer>()
{
existing.points = points;
existing.ramp = ramp;
self.style = None;
return;
}
}
}
let layer = Box::new(crate::visualization::PointCloudLayer::new(
name.clone(),
points,
ramp,
));
self.replace_or_push_named_layer(&name, layer);
}
#[inline]
pub fn style(&self) -> Option<&MapStyle> {
self.style.as_ref()
}
#[inline]
pub fn style_document(&self) -> Option<&StyleDocument> {
self.style.as_ref().map(MapStyle::document)
}
pub fn set_style(&mut self, style: MapStyle) -> Result<(), StyleError> {
self.apply_style_document(style.document())?;
self.style = Some(style);
Ok(())
}
pub fn set_style_document(&mut self, document: StyleDocument) -> Result<(), StyleError> {
self.set_style(MapStyle::from_document(document))
}
pub fn with_style_mut<R>(
&mut self,
mutate: impl FnOnce(&mut StyleDocument) -> R,
) -> Result<Option<R>, StyleError> {
let mut style: MapStyle = match self.style.take() {
Some(s) => s,
None => return Ok(None),
};
let result = mutate(style.document_mut());
self.apply_style_document(style.document())?;
self.style = Some(style);
Ok(Some(result))
}
pub fn reapply_style(&mut self) -> Result<bool, StyleError> {
let style: MapStyle = match self.style.take() {
Some(s) => s,
None => return Ok(false),
};
self.apply_style_document(style.document())?;
self.style = Some(style);
Ok(true)
}
#[inline]
pub fn terrain(&self) -> &TerrainManager {
&self.terrain
}
pub fn set_terrain(&mut self, terrain: TerrainManager) {
self.terrain = terrain;
if self.style.is_some() {
self.style = None;
}
}
#[inline]
pub fn animator(&self) -> &CameraAnimator {
&self.animator
}
#[inline]
pub fn is_animating(&self) -> bool {
self.animator.is_active()
}
pub fn set_data_update_interval(&mut self, interval: f64) {
self.data_update_interval = interval.max(0.0);
}
#[inline]
pub fn data_update_interval(&self) -> f64 {
self.data_update_interval
}
pub fn set_task_pool(&mut self, pool: Arc<dyn DataTaskPool>) {
self.async_pipeline = Some(AsyncDataPipeline::new(pool));
}
pub fn clear_task_pool(&mut self) {
self.async_pipeline = None;
}
#[inline]
pub fn has_async_pipeline(&self) -> bool {
self.async_pipeline.is_some()
}
#[inline]
pub fn vector_meshes(&self) -> &[VectorMeshData] {
&self.vector_meshes
}
#[inline]
pub fn model_instances(&self) -> &[ModelInstance] {
&self.model_instances
}
#[inline]
pub fn placed_symbols(&self) -> &[PlacedSymbol] {
&self.placed_symbols
}
#[inline]
pub fn symbol_assets(&self) -> &SymbolAssetRegistry {
&self.symbol_assets
}
pub fn feature_state(&self, source_id: &str, feature_id: &str) -> Option<&FeatureState> {
self.feature_state
.get(&FeatureStateId::new(source_id, feature_id))
}
pub fn set_feature_state(
&mut self,
source_id: impl Into<String>,
feature_id: impl Into<String>,
state: FeatureState,
) {
self.feature_state
.insert(FeatureStateId::new(source_id, feature_id), state);
}
pub fn set_feature_state_property(
&mut self,
source_id: impl Into<String>,
feature_id: impl Into<String>,
key: impl Into<String>,
value: crate::geometry::PropertyValue,
) {
self.feature_state
.entry(FeatureStateId::new(source_id, feature_id))
.or_default()
.insert(key.into(), value);
}
pub fn remove_feature_state(
&mut self,
source_id: &str,
feature_id: &str,
) -> Option<FeatureState> {
self.feature_state
.remove(&FeatureStateId::new(source_id, feature_id))
}
pub fn clear_feature_state(&mut self) {
self.feature_state.clear();
}
pub fn resolve_feature_style(
&self,
style_layer_id: &str,
source_id: &str,
feature_id: &str,
) -> Option<VectorStyle> {
use crate::style::StyleEvalContextFull;
let document = self.style_document()?;
let style_layer = document.layer(style_layer_id)?;
let empty_state: FeatureState = HashMap::new();
let state = self
.feature_state
.get(&FeatureStateId::new(source_id, feature_id))
.unwrap_or(&empty_state);
let ctx = StyleEvalContextFull::new(self.fractional_zoom() as f32, state);
style_layer.resolve_style_with_feature_state(&ctx)
}
pub fn set_interaction_manager(
&mut self,
manager: crate::interaction_manager::InteractionManager,
) {
self.interaction_manager = Some(manager);
}
pub fn take_interaction_manager(
&mut self,
) -> Option<crate::interaction_manager::InteractionManager> {
self.interaction_manager.take()
}
pub fn interaction_manager(&self) -> Option<&crate::interaction_manager::InteractionManager> {
self.interaction_manager.as_ref()
}
pub fn interaction_manager_mut(
&mut self,
) -> Option<&mut crate::interaction_manager::InteractionManager> {
self.interaction_manager.as_mut()
}
pub fn on<F>(
&mut self,
kind: crate::interaction::InteractionEventKind,
callback: F,
) -> crate::event_emitter::ListenerId
where
F: Fn(&crate::interaction::InteractionEvent) + Send + Sync + 'static,
{
self.event_emitter.on(kind, callback)
}
pub fn once<F>(
&mut self,
kind: crate::interaction::InteractionEventKind,
callback: F,
) -> crate::event_emitter::ListenerId
where
F: Fn(&crate::interaction::InteractionEvent) + Send + Sync + 'static,
{
self.event_emitter.once(kind, callback)
}
pub fn off(&mut self, id: crate::event_emitter::ListenerId) -> bool {
self.event_emitter.off(id)
}
pub fn dispatch_events(&mut self, events: &[crate::interaction::InteractionEvent]) {
self.event_emitter.dispatch(events);
}
pub fn event_emitter_mut(&mut self) -> &mut crate::event_emitter::EventEmitter {
&mut self.event_emitter
}
pub fn invalidate_symbol_image_dependency(&mut self, image_id: &str) -> usize {
self.invalidate_symbol_dependency_tiles(|deps| deps.images.contains(image_id))
}
pub fn invalidate_symbol_glyph_dependency(
&mut self,
font_stack: &str,
codepoint: char,
) -> usize {
let glyph = crate::symbols::GlyphKey {
font_stack: font_stack.to_owned(),
codepoint,
};
self.invalidate_symbol_dependency_tiles(|deps| deps.glyphs.contains(&glyph))
}
pub fn set_vector_meshes(&mut self, meshes: Vec<VectorMeshData>) {
let meshes = Arc::new(meshes);
self.pending_vector_meshes = Some(meshes.clone());
self.vector_meshes = meshes;
}
pub fn set_model_instances(&mut self, instances: Vec<ModelInstance>) {
let instances = Arc::new(instances);
self.pending_model_instances = Some(instances.clone());
self.model_instances = instances;
}
pub fn set_placed_symbols(&mut self, symbols: Vec<PlacedSymbol>) {
self.placed_symbols = Arc::new(symbols);
self.symbol_assets
.rebuild_from_symbols(&self.placed_symbols);
}
pub fn update_with_dt(&mut self, dt: f64) {
let was_flying = self.animator.is_flying() || self.animator.is_easing();
self.update_camera(dt);
let is_flying = self.animator.is_flying() || self.animator.is_easing();
if self.async_pipeline.is_some() {
self.dispatch_data_requests();
self.poll_completed_results(dt);
} else {
let animation_active = is_flying;
self.update_tile_layers();
if animation_active && self.data_update_interval > 0.0 {
self.data_update_elapsed += dt;
if self.data_update_elapsed >= self.data_update_interval {
self.data_update_elapsed = 0.0;
self.update_heavy_layers(dt);
}
} else {
self.data_update_elapsed = 0.0;
self.update_heavy_layers(dt);
}
}
if was_flying && !is_flying && self.async_pipeline.is_none() {
self.update_tile_layers();
self.update_heavy_layers(dt);
}
self.style_time += dt.max(0.0);
self.placeholder_time += dt;
self.loading_placeholders = Arc::new(PlaceholderGenerator::generate(
&self.visible_tiles,
&self.placeholder_style,
self.placeholder_time,
));
if let Err(e) = self.sync_attached_style_runtime() {
log::warn!("style sync error: {e:?}");
}
self.apply_pending_frame_overrides();
{
let bg = self.background_color().unwrap_or([1.0, 1.0, 1.0, 1.0]);
let pitch = self.camera.pitch();
let distance = self.camera.distance();
let style_fog = self.style.as_ref().and_then(|s| s.document().fog());
let effective_fog = self.fog_config.as_ref().or(style_fog);
self.computed_fog = crate::style::compute_fog(pitch, distance, bg, effective_fog);
}
{
let style_lights = self.style.as_ref().and_then(|s| s.document().lights());
let effective_lights = self.light_config.as_ref().or(style_lights);
let default_lights = crate::style::LightConfig::default();
let config = effective_lights.unwrap_or(&default_lights);
self.computed_lighting = crate::style::compute_lighting(config);
if self.computed_lighting.shadows_enabled {
let vp = self.camera.view_projection_matrix();
let dir = self.computed_lighting.directional_dir;
let cam_dist = self.camera.distance();
self.computed_shadow =
crate::style::compute_shadow_cascades(&vp, dir, cam_dist, &config.shadow);
} else {
self.computed_shadow = crate::style::ComputedShadow::default();
}
}
{
let style_sky = self.style.as_ref().and_then(|s| s.document().sky());
let effective_sky = self.sky_config.as_ref().or(style_sky);
if let Some(sky) = effective_sky {
let style_lights = self.style.as_ref().and_then(|s| s.document().lights());
let effective_lights = self.light_config.as_ref().or(style_lights);
let fallback_sun = effective_lights
.map(|l| l.directional.direction)
.unwrap_or([210.0, 45.0]);
self.computed_sky = crate::style::compute_sky(sky, fallback_sun);
} else {
self.computed_sky = crate::style::ComputedSky::default();
}
}
}
pub fn handle_input(&mut self, event: InputEvent) {
if let InputEvent::Touch(contact) = event {
let derived = self.gesture_recognizer.process(contact);
for e in derived {
CameraController::handle_event(&mut self.camera, e, &self.constraints);
}
return;
}
CameraController::handle_event(&mut self.camera, event, &self.constraints);
}
pub fn gesture_recognizer(&self) -> &crate::gesture::GestureRecognizer {
&self.gesture_recognizer
}
pub fn update(&mut self) {
self.update_with_dt(1.0 / 60.0);
}
pub fn fly_to(&mut self, options: crate::camera_animator::FlyToOptions) {
self.animator.start_fly_to(&mut self.camera, &options);
}
pub fn ease_to(&mut self, options: crate::camera_animator::EaseToOptions) {
self.animator.start_ease_to(&mut self.camera, &options);
}
pub fn jump_to(
&mut self,
target: GeoCoord,
distance: f64,
pitch: Option<f64>,
yaw: Option<f64>,
) {
self.animator.cancel();
self.camera.set_target(target);
self.camera.set_distance(distance);
if let Some(p) = pitch {
self.camera.set_pitch(p);
}
if let Some(y) = yaw {
self.camera.set_yaw(y);
}
}
pub fn update_camera(&mut self, dt: f64) {
self.animator.tick(&mut self.camera, dt);
{
let target = *self.camera.target();
let wrapped_lon = wrap_lon_180(target.lon);
if (wrapped_lon - target.lon).abs() > 1e-12 {
self.camera
.set_target(GeoCoord::from_lat_lon(target.lat, wrapped_lon));
}
}
let mpp = self.camera.meters_per_pixel();
self.zoom_level = Self::mpp_to_zoom(mpp);
self.viewport_bounds = self.compute_viewport_bounds();
self.scene_viewport_bounds = self.compute_scene_viewport_bounds();
self.frustum = Some(Frustum::from_view_projection(
&self.camera.view_projection_matrix(),
));
self.update_camera_motion_state(dt);
let fractional_zoom = self.fractional_zoom();
self.camera_zoom_delta = self
.previous_fractional_zoom
.map_or(0.0, |previous| fractional_zoom - previous);
self.previous_fractional_zoom = Some(fractional_zoom);
}
pub fn frame_output(&self) -> FrameOutput {
FrameOutput {
view_projection: self.camera.view_projection_matrix(),
frustum: self.frustum.clone(),
tiles: Arc::clone(&self.visible_tiles),
terrain: Arc::clone(&self.terrain_meshes),
hillshade: Arc::clone(&self.hillshade_rasters),
vectors: Arc::clone(&self.vector_meshes),
models: Arc::clone(&self.model_instances),
symbols: Arc::clone(&self.placed_symbols),
visualization: Arc::clone(&self.visualization_overlays),
placeholders: Arc::clone(&self.loading_placeholders),
image_overlays: Arc::clone(&self.image_overlays),
zoom_level: self.zoom_level,
}
}
pub fn elevation_at(&self, coord: &GeoCoord) -> Option<f64> {
self.terrain.elevation_at(coord)
}
pub fn screen_to_geo(&self, px: f64, py: f64) -> Option<GeoCoord> {
self.camera.screen_to_geo(px, py)
}
pub fn geo_to_screen(&self, geo: &GeoCoord) -> Option<(f64, f64)> {
self.camera.geo_to_screen(geo)
}
pub fn fit_bounds(&mut self, bounds: &GeoBounds, options: &FitBoundsOptions) {
let center = bounds.center();
let sw_world = WebMercator::project_clamped(&bounds.sw());
let ne_world = WebMercator::project_clamped(&bounds.ne());
let dx = (ne_world.position.x - sw_world.position.x).abs();
let dy = (ne_world.position.y - sw_world.position.y).abs();
let vw =
(self.camera.viewport_width() as f64 - options.padding.left - options.padding.right)
.max(1.0);
let vh =
(self.camera.viewport_height() as f64 - options.padding.top - options.padding.bottom)
.max(1.0);
let mpp_x = dx / vw;
let mpp_y = dy / vh;
let mpp = mpp_x.max(mpp_y);
let zoom = if mpp <= 0.0 || !mpp.is_finite() {
MAX_ZOOM as f64
} else {
(WGS84_CIRCUMFERENCE / (mpp * TILE_PX))
.log2()
.clamp(0.0, MAX_ZOOM as f64)
};
let zoom = match options.max_zoom {
Some(mz) => zoom.min(mz),
None => zoom,
};
if options.animate {
let fly = crate::camera_animator::FlyToOptions {
center: Some(center),
zoom: Some(zoom),
bearing: options.bearing,
pitch: options.pitch,
duration: options.duration,
..Default::default()
};
self.fly_to(fly);
} else {
let is_perspective = matches!(self.camera.mode(), CameraMode::Perspective);
let distance = {
let mpp = WGS84_CIRCUMFERENCE / (2.0_f64.powf(zoom) * TILE_PX);
let vis_h = mpp * self.camera.viewport_height().max(1) as f64;
if is_perspective {
vis_h / (2.0 * (self.camera.fov_y() / 2.0).tan())
} else {
vis_h / 2.0
}
};
self.jump_to(center, distance, options.pitch, options.bearing);
}
}
pub fn ray_to_geo(&self, origin: glam::DVec3, direction: glam::DVec3) -> Option<GeoCoord> {
if direction.z.abs() < 1e-12 {
return None;
}
let t = -origin.z / direction.z;
if t < 0.0 {
return None;
}
let hit = origin + direction * t;
let world = self.camera.target_world();
let world_hit = WorldCoord::new(hit.x + world.x, hit.y + world.y, 0.0);
Some(self.camera.projection().unproject(&world_hit))
}
pub fn ray_to_geo_on_terrain(
&self,
origin: glam::DVec3,
direction: glam::DVec3,
) -> Option<GeoCoord> {
if !self.terrain.enabled() {
return self.ray_to_geo(origin, direction);
}
let world = self.camera.target_world();
let steps = 64;
let max_t = self.camera.distance() * 4.0;
let step = max_t / steps as f64;
let mut prev_above = true;
for i in 0..=steps {
let t = step * i as f64;
let p = origin + direction * t;
let world_hit = WorldCoord::new(p.x + world.x, p.y + world.y, p.z);
let geo = self.camera.projection().unproject(&world_hit);
let elev = self.terrain.elevation_at(&geo).unwrap_or(0.0);
let above = p.z >= elev;
if !above && prev_above && i > 0 {
let prev_t = step * (i - 1) as f64;
let prev_p = origin + direction * prev_t;
let prev_world = WorldCoord::new(prev_p.x + world.x, prev_p.y + world.y, prev_p.z);
let prev_geo = self.camera.projection().unproject(&prev_world);
let prev_elev = self.terrain.elevation_at(&prev_geo).unwrap_or(0.0);
let prev_height = prev_p.z - prev_elev;
let curr_height = p.z - elev;
let frac = prev_height / (prev_height - curr_height);
let hit_t = prev_t + (t - prev_t) * frac;
let hit = origin + direction * hit_t;
let hit_world = WorldCoord::new(hit.x + world.x, hit.y + world.y, hit.z);
return Some(self.camera.projection().unproject(&hit_world));
}
prev_above = above;
}
self.ray_to_geo(origin, direction)
}
pub fn ray_to_flat_geo(&self, origin: glam::DVec3, direction: glam::DVec3) -> Option<GeoCoord> {
self.ray_to_geo(origin, direction)
}
pub fn screen_to_geo_on_terrain(&self, px: f64, py: f64) -> Option<GeoCoord> {
let (origin, direction) = self.camera.screen_to_ray(px, py);
self.ray_to_geo_on_terrain(origin, direction)
}
fn mercator_camera_world(&self) -> (f64, f64) {
let w = WebMercator::project(self.camera.target());
(w.position.x, w.position.y)
}
fn update_camera_motion_state(&mut self, dt: f64) {
self.camera_motion_time_seconds += dt.max(0.0);
let current_wrapped = self.mercator_camera_world();
let current_target = if let Some(last) = self.camera_motion_samples.back() {
glam::DVec2::new(
last.target_world.x
+ heavy_layers::wrapped_world_delta(current_wrapped.0 - last.target_world.x),
current_wrapped.1,
)
} else {
glam::DVec2::new(current_wrapped.0, current_wrapped.1)
};
self.camera_motion_samples.push_back(CameraMotionSample {
time_seconds: self.camera_motion_time_seconds,
target_world: current_target,
});
self.trim_camera_motion_samples();
self.recompute_camera_motion_state();
}
fn trim_camera_motion_samples(&mut self) {
let max_samples = self.camera_velocity_config.sample_window.max(1) + 1;
while self.camera_motion_samples.len() > max_samples {
self.camera_motion_samples.pop_front();
}
}
fn recompute_camera_motion_state(&mut self) {
let pan_velocity_world = if let (Some(first), Some(last)) = (
self.camera_motion_samples.front(),
self.camera_motion_samples.back(),
) {
let dt = last.time_seconds - first.time_seconds;
if dt > 1e-9 {
(last.target_world - first.target_world) / dt
} else {
glam::DVec2::ZERO
}
} else {
glam::DVec2::ZERO
};
let current_wrapped = self.mercator_camera_world();
let current_target_world = glam::DVec2::new(current_wrapped.0, current_wrapped.1);
let look_ahead = self.camera_velocity_config.look_ahead_seconds.max(0.0);
let predicted_delta = pan_velocity_world * look_ahead;
self.camera_motion_state = CameraMotionState {
pan_velocity_world,
predicted_target_world: current_target_world + predicted_delta,
predicted_viewport_bounds: heavy_layers::translated_world_bounds(
&self.viewport_bounds,
predicted_delta,
),
};
}
fn mpp_to_zoom(mpp: f64) -> u8 {
if mpp <= 0.0 || !mpp.is_finite() {
return MAX_ZOOM;
}
let z = (WGS84_CIRCUMFERENCE / (mpp * TILE_PX)).log2();
(z.round() as u8).min(MAX_ZOOM)
}
fn compute_viewport_bounds(&self) -> WorldBounds {
use rustial_math::WebMercator;
let w = self.camera.viewport_width() as f64;
let h = self.camera.viewport_height() as f64;
if w <= 0.0 || h <= 0.0 {
let zero = WorldCoord::new(0.0, 0.0, 0.0);
return WorldBounds::new(zero, zero);
}
let cam = &self.camera;
if cam.mode() == CameraMode::Orthographic {
let half_w = cam.distance() * (w / h).max(1.0);
let half_h = cam.distance() / (w / h).min(1.0);
let target = WebMercator::project(cam.target());
let overscan = 1.3;
return WorldBounds::new(
WorldCoord::new(
target.position.x - half_w * overscan,
target.position.y - half_h * overscan,
0.0,
),
WorldCoord::new(
target.position.x + half_w * overscan,
target.position.y + half_h * overscan,
0.0,
),
);
}
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
let mut any_hit = false;
for (sx, sy) in viewport_sample_points(w, h) {
if let Some(geo) = cam.screen_to_geo(sx, sy) {
let world = WebMercator::project_clamped(&geo);
min_x = min_x.min(world.position.x);
min_y = min_y.min(world.position.y);
max_x = max_x.max(world.position.x);
max_y = max_y.max(world.position.y);
any_hit = true;
}
}
if !any_hit {
let target = WebMercator::project(cam.target());
let mpp = cam.meters_per_pixel();
let half_w = mpp * w * 0.5;
let half_h = mpp * h * 0.5;
return WorldBounds::new(
WorldCoord::new(target.position.x - half_w, target.position.y - half_h, 0.0),
WorldCoord::new(target.position.x + half_w, target.position.y + half_h, 0.0),
);
}
let overscan = perspective_viewport_overscan(cam.pitch());
let cx = (min_x + max_x) * 0.5;
let cy = (min_y + max_y) * 0.5;
let hw = (max_x - min_x) * 0.5 * overscan;
let hh = (max_y - min_y) * 0.5 * overscan;
WorldBounds::new(
WorldCoord::new(cx - hw, cy - hh, 0.0),
WorldCoord::new(cx + hw, cy + hh, 0.0),
)
}
fn compute_scene_viewport_bounds(&self) -> WorldBounds {
let w = self.camera.viewport_width() as f64;
let h = self.camera.viewport_height() as f64;
if w <= 0.0 || h <= 0.0 {
let zero = WorldCoord::new(0.0, 0.0, 0.0);
return WorldBounds::new(zero, zero);
}
let cam = &self.camera;
let proj = cam.projection();
if cam.mode() == CameraMode::Orthographic {
let half_w = cam.distance() * (w / h).max(1.0);
let half_h = cam.distance() / (w / h).min(1.0);
let target = proj.project(cam.target());
let overscan = 1.3;
return WorldBounds::new(
WorldCoord::new(
target.position.x - half_w * overscan,
target.position.y - half_h * overscan,
0.0,
),
WorldCoord::new(
target.position.x + half_w * overscan,
target.position.y + half_h * overscan,
0.0,
),
);
}
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
let mut any_hit = false;
for (sx, sy) in viewport_sample_points(w, h) {
if let Some(geo) = cam.screen_to_geo(sx, sy) {
let world = proj.project(&geo);
min_x = min_x.min(world.position.x);
min_y = min_y.min(world.position.y);
max_x = max_x.max(world.position.x);
max_y = max_y.max(world.position.y);
any_hit = true;
}
}
if !any_hit {
let target = proj.project(cam.target());
let mpp = cam.meters_per_pixel();
let half_w = mpp * w * 0.5;
let half_h = mpp * h * 0.5;
return WorldBounds::new(
WorldCoord::new(target.position.x - half_w, target.position.y - half_h, 0.0),
WorldCoord::new(target.position.x + half_w, target.position.y + half_h, 0.0),
);
}
let overscan = perspective_viewport_overscan(cam.pitch());
let cx = (min_x + max_x) * 0.5;
let cy = (min_y + max_y) * 0.5;
let hw = (max_x - min_x) * 0.5 * overscan;
let hh = (max_y - min_y) * 0.5 * overscan;
WorldBounds::new(
WorldCoord::new(cx - hw, cy - hh, 0.0),
WorldCoord::new(cx + hw, cy + hh, 0.0),
)
}
fn sync_attached_style_runtime(&mut self) -> Result<(), StyleError> {
let Some(style) = self.style.as_ref() else {
return Ok(());
};
let doc = style.document();
let ctx = crate::style::StyleEvalContext {
zoom: self.fractional_zoom() as f32,
};
let now = self.style_time;
for style_layer in doc.layers() {
let (layer_id, transition_spec, opacity, color, secondary_color, width, height, base) =
extract_transition_props(style_layer, ctx, &doc.transition());
let state = self
.layer_transitions
.entry(layer_id.to_owned())
.or_insert_with(|| {
crate::style::LayerTransitionState::from_initial(
transition_spec,
opacity,
color,
secondary_color,
width,
height,
base,
)
});
state.update(now, opacity, color, secondary_color, width, height, base);
}
Ok(())
}
fn replace_or_push_named_layer(&mut self, name: &str, layer: Box<dyn crate::layer::Layer>) {
if let Some(index) = self.layers.index_of(name) {
let _ = self.layers.remove(index);
self.layers.insert(index, layer);
} else {
self.layers.push(layer);
}
self.style = None;
}
}
fn extract_transition_props<'a>(
layer: &'a crate::style::StyleLayer,
ctx: crate::style::StyleEvalContext,
global_transition: &crate::style::TransitionSpec,
) -> (
&'a str,
crate::style::TransitionSpec,
f32,
[f32; 4],
[f32; 4],
f32,
f32,
f32,
) {
use crate::style::StyleLayer;
let meta = layer.meta();
let spec = if meta.transition.is_active() {
meta.transition
} else {
*global_transition
};
let opacity = meta.opacity.evaluate_with_context(ctx);
let transparent: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
let (color, secondary_color, width, height, base) = match layer {
StyleLayer::Fill(f) => (
f.fill_color.evaluate_with_context(ctx),
f.outline_color.evaluate_with_context(ctx),
f.outline_width.evaluate_with_context(ctx),
0.0,
0.0,
),
StyleLayer::Line(l) => (
l.color.evaluate_with_context(ctx),
transparent,
l.width.evaluate_with_context(ctx),
0.0,
0.0,
),
StyleLayer::Circle(c) => (
c.color.evaluate_with_context(ctx),
c.stroke_color.evaluate_with_context(ctx),
c.radius.evaluate_with_context(ctx),
0.0,
0.0,
),
StyleLayer::FillExtrusion(e) => (
e.color.evaluate_with_context(ctx),
transparent,
0.0,
e.height.evaluate_with_context(ctx),
e.base.evaluate_with_context(ctx),
),
StyleLayer::Symbol(s) => (
s.color.evaluate_with_context(ctx),
s.halo_color.evaluate_with_context(ctx),
s.size.evaluate_with_context(ctx),
0.0,
0.0,
),
StyleLayer::Heatmap(h) => (
h.color.evaluate_with_context(ctx),
transparent,
h.radius.evaluate_with_context(ctx),
0.0,
0.0,
),
StyleLayer::Background(b) => (
b.color.evaluate_with_context(ctx),
transparent,
0.0,
0.0,
0.0,
),
_ => (transparent, transparent, 0.0, 0.0, 0.0),
};
(
meta.id.as_str(),
spec,
opacity,
color,
secondary_color,
width,
height,
base,
)
}