use crate::camera_projection::CameraProjection;
use crate::cluster::{ClusterOptions, PointCluster};
use crate::geometry::{FeatureCollection, PropertyValue};
use crate::layer::Layer;
use crate::layers::{
BackgroundLayer, DynamicImageOverlayLayer, FrameProviderFactory, HillshadeLayer, LineCap,
LineJoin, ModelLayer, TileLayer, VectorLayer, VectorRenderMode, VectorStyle,
};
use crate::models::ModelInstance;
use crate::query::FeatureState;
use crate::symbols::{
SymbolAnchor, SymbolIconTextFit, SymbolPlacement, SymbolTextJustify, SymbolTextTransform,
SymbolWritingMode,
};
use crate::terrain::{ElevationSource, TerrainConfig};
use crate::tile_manager::TileSelectionConfig;
use crate::tile_source::TileSource;
use rustial_math::GeoCoord;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StyleError {
DuplicateSourceId(String),
DuplicateLayerId(String),
MissingSource(String),
SourceKindMismatch {
layer_id: String,
source_id: String,
expected: &'static str,
actual: &'static str,
},
MissingSourceLayer {
layer_id: String,
source_id: String,
source_layer: String,
},
}
impl fmt::Display for StyleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StyleError::DuplicateSourceId(id) => write!(f, "duplicate style source id `{id}`"),
StyleError::DuplicateLayerId(id) => write!(f, "duplicate style layer id `{id}`"),
StyleError::MissingSource(id) => write!(f, "missing style source `{id}`"),
StyleError::SourceKindMismatch {
layer_id,
source_id,
expected,
actual,
} => write!(
f,
"style layer `{layer_id}` expected source `{source_id}` of kind `{expected}`, got `{actual}`"
),
StyleError::MissingSourceLayer {
layer_id,
source_id,
source_layer,
} => write!(
f,
"style layer `{layer_id}` referenced missing source-layer `{source_layer}` on source `{source_id}`"
),
}
}
}
impl std::error::Error for StyleError {}
pub type StyleSourceId = String;
pub type StyleLayerId = String;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StyleEvalContext {
pub zoom: f32,
}
impl StyleEvalContext {
pub fn new(zoom: f32) -> Self {
Self { zoom }
}
pub fn with_feature_state(self, feature_state: &FeatureState) -> StyleEvalContextFull<'_> {
StyleEvalContextFull {
zoom: self.zoom,
feature_state,
}
}
}
impl Default for StyleEvalContext {
fn default() -> Self {
Self { zoom: 0.0 }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StyleEvalContextFull<'a> {
pub zoom: f32,
pub feature_state: &'a FeatureState,
}
impl<'a> StyleEvalContextFull<'a> {
pub fn new(zoom: f32, feature_state: &'a FeatureState) -> Self {
Self {
zoom,
feature_state,
}
}
pub fn to_base(&self) -> StyleEvalContext {
StyleEvalContext { zoom: self.zoom }
}
pub fn get_feature_state(&self, key: &str) -> Option<&PropertyValue> {
self.feature_state.get(key)
}
pub fn feature_state_bool(&self, key: &str) -> bool {
self.feature_state
.get(key)
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
pub fn feature_state_f64(&self, key: &str, default: f64) -> f64 {
self.feature_state
.get(key)
.and_then(|v| v.as_f64())
.unwrap_or(default)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StyleProjection {
#[default]
Mercator,
Equirectangular,
Globe,
VerticalPerspective,
}
impl StyleProjection {
pub fn to_camera_projection(self) -> CameraProjection {
match self {
StyleProjection::Mercator => CameraProjection::WebMercator,
StyleProjection::Equirectangular => CameraProjection::Equirectangular,
StyleProjection::Globe => CameraProjection::Globe,
StyleProjection::VerticalPerspective => {
CameraProjection::vertical_perspective(GeoCoord::default(), 10_000_000.0)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum LightingMode {
#[default]
Default,
Flat,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AmbientLight {
pub color: [f32; 3],
pub intensity: f32,
}
impl Default for AmbientLight {
fn default() -> Self {
Self {
color: [1.0, 1.0, 1.0],
intensity: 0.5,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DirectionalLight {
pub direction: [f32; 2],
pub color: [f32; 3],
pub intensity: f32,
pub cast_shadows: bool,
}
impl Default for DirectionalLight {
fn default() -> Self {
Self {
direction: [210.0, 45.0],
color: [1.0, 1.0, 1.0],
intensity: 0.5,
cast_shadows: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct LightConfig {
pub mode: LightingMode,
pub ambient: AmbientLight,
pub directional: DirectionalLight,
pub shadow: ShadowConfig,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComputedLighting {
pub ambient_color: [f32; 3],
pub directional_dir: [f32; 3],
pub directional_color: [f32; 3],
pub lighting_enabled: f32,
pub shadows_enabled: bool,
}
impl Default for ComputedLighting {
fn default() -> Self {
compute_lighting(&LightConfig::default())
}
}
pub fn compute_lighting(config: &LightConfig) -> ComputedLighting {
let enabled = match config.mode {
LightingMode::Default => 1.0,
LightingMode::Flat => 0.0,
};
let ambient_color = [
config.ambient.color[0] * config.ambient.intensity,
config.ambient.color[1] * config.ambient.intensity,
config.ambient.color[2] * config.ambient.intensity,
];
let [azimuth_deg, altitude_deg] = config.directional.direction;
let azimuth = azimuth_deg.to_radians();
let altitude = altitude_deg.to_radians();
let cos_alt = altitude.cos();
let dir = [
azimuth.sin() * cos_alt,
azimuth.cos() * cos_alt,
altitude.sin(),
];
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
let directional_dir = if len > 1e-6 {
[dir[0] / len, dir[1] / len, dir[2] / len]
} else {
[0.0, 0.0, 1.0]
};
let directional_color = [
config.directional.color[0] * config.directional.intensity,
config.directional.color[1] * config.directional.intensity,
config.directional.color[2] * config.directional.intensity,
];
ComputedLighting {
ambient_color,
directional_dir,
directional_color,
lighting_enabled: enabled,
shadows_enabled: enabled > 0.5 && config.directional.cast_shadows,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ShadowConfig {
pub cascade_count: u32,
pub map_resolution: u32,
pub intensity: f32,
pub normal_offset: f32,
}
impl Default for ShadowConfig {
fn default() -> Self {
Self {
cascade_count: 2,
map_resolution: 2048,
intensity: 0.8,
normal_offset: 3.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComputedShadow {
pub enabled: bool,
pub light_matrices: [[[f32; 4]; 4]; 4],
pub cascade_count: u32,
pub map_resolution: u32,
pub intensity: f32,
pub texel_size: f32,
pub normal_offset: f32,
pub cascade_split: f32,
}
impl Default for ComputedShadow {
fn default() -> Self {
Self {
enabled: false,
light_matrices: [[[0.0; 4]; 4]; 4],
cascade_count: 2,
map_resolution: 2048,
intensity: 0.0,
texel_size: 1.0 / 2048.0,
normal_offset: 3.0,
cascade_split: 0.0,
}
}
}
pub fn compute_shadow_cascades(
view_proj: &glam::DMat4,
light_dir: [f32; 3],
camera_distance: f64,
config: &ShadowConfig,
) -> ComputedShadow {
use glam::{DMat4, DVec3};
let cascade_count = config.cascade_count.clamp(1, 4) as usize;
let resolution = config.map_resolution.max(256);
let split0 = camera_distance * 1.5;
let splits: [f64; 4] = [split0, split0 * 2.0, split0 * 4.0, split0 * 8.0];
let inv_vp = view_proj.inverse();
let light = DVec3::new(
light_dir[0] as f64,
light_dir[1] as f64,
light_dir[2] as f64,
)
.normalize_or_zero();
if light.length_squared() < 0.5 {
return ComputedShadow::default();
}
let up_hint = if light.z.abs() > 0.99 {
DVec3::new(0.0, 1.0, 0.0)
} else {
DVec3::new(0.0, 0.0, 1.0)
};
let light_right = light.cross(up_hint).normalize();
let light_up = light_right.cross(light).normalize();
let light_view_rot = DMat4::from_cols(
glam::DVec4::new(light_right.x, light_up.x, light.x, 0.0),
glam::DVec4::new(light_right.y, light_up.y, light.y, 0.0),
glam::DVec4::new(light_right.z, light_up.z, light.z, 0.0),
glam::DVec4::new(0.0, 0.0, 0.0, 1.0),
);
let mut result = ComputedShadow {
enabled: true,
light_matrices: [[[0.0; 4]; 4]; 4],
cascade_count: cascade_count as u32,
map_resolution: resolution,
intensity: config.intensity.clamp(0.0, 1.0),
texel_size: 1.0 / resolution as f32,
normal_offset: config.normal_offset,
cascade_split: splits[0] as f32,
};
let ndc_corners: [[f64; 3]; 8] = [
[-1.0, -1.0, 0.0],
[1.0, -1.0, 0.0],
[-1.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
[-1.0, -1.0, 1.0],
[1.0, -1.0, 1.0],
[-1.0, 1.0, 1.0],
[1.0, 1.0, 1.0],
];
for cascade in 0..cascade_count {
let near_frac = if cascade == 0 {
0.0
} else {
splits[cascade - 1] / splits[cascade_count - 1]
};
let far_frac = splits[cascade] / splits[cascade_count - 1];
let mut world_corners = [DVec3::ZERO; 8];
for (i, ndc) in ndc_corners.iter().enumerate() {
let z = if i < 4 { near_frac } else { far_frac };
let ndc_pos = glam::DVec4::new(ndc[0], ndc[1], z, 1.0);
let world_h = inv_vp * ndc_pos;
world_corners[i] = DVec3::new(
world_h.x / world_h.w,
world_h.y / world_h.w,
world_h.z / world_h.w,
);
}
let center: DVec3 = world_corners.iter().copied().sum::<DVec3>() / 8.0;
let radius = world_corners
.iter()
.map(|c| (*c - center).length())
.fold(0.0_f64, f64::max);
let texel_size = (radius * 2.0) / resolution as f64;
let light_center = light_view_rot * glam::DVec4::new(center.x, center.y, center.z, 1.0);
let snapped_x = (light_center.x / texel_size).floor() * texel_size;
let snapped_y = (light_center.y / texel_size).floor() * texel_size;
let snap_offset_x = snapped_x - light_center.x;
let snap_offset_y = snapped_y - light_center.y;
let snapped_center = center + DVec3::new(snap_offset_x, snap_offset_y, 0.0);
let light_pos = snapped_center - light * radius * 2.0;
let light_view = DMat4::look_at_rh(light_pos, snapped_center, light_up);
let view_center = light_view * glam::DVec4::new(center.x, center.y, center.z, 1.0);
let snap_offset_x = (view_center.x / texel_size).fract() * texel_size;
let snap_offset_y = (view_center.y / texel_size).fract() * texel_size;
let snap = DMat4::from_translation(DVec3::new(-snap_offset_x, -snap_offset_y, 0.0));
let snapped_view = snap * light_view;
let ortho = DMat4::orthographic_rh(-radius, radius, -radius, radius, 0.0, radius * 4.0);
let light_vp = ortho * snapped_view;
let m32 = light_vp.as_mat4();
result.light_matrices[cascade] = m32.to_cols_array_2d();
}
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SkyType {
#[default]
Atmosphere,
Gradient,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SkyConfig {
pub sky_type: SkyType,
pub sun_position: Option<[f32; 2]>,
pub sun_intensity: f32,
pub atmosphere_color: [f32; 3],
pub halo_color: [f32; 3],
pub opacity: f32,
}
impl Default for SkyConfig {
fn default() -> Self {
Self {
sky_type: SkyType::default(),
sun_position: None,
sun_intensity: 10.0,
atmosphere_color: [1.0, 1.0, 1.0],
halo_color: [1.0, 1.0, 1.0],
opacity: 1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComputedSky {
pub sun_direction: [f32; 3],
pub sun_intensity: f32,
pub rayleigh_color: [f32; 3],
pub mie_color: [f32; 3],
pub sky_enabled: f32,
}
impl Default for ComputedSky {
fn default() -> Self {
Self {
sun_direction: [0.0, 0.0, 1.0],
sun_intensity: 0.0,
rayleigh_color: [1.0, 1.0, 1.0],
mie_color: [1.0, 1.0, 1.0],
sky_enabled: 0.0, }
}
}
pub fn compute_sky(config: &SkyConfig, fallback_sun: [f32; 2]) -> ComputedSky {
let enabled = if config.opacity > 0.0 { 1.0 } else { 0.0 };
let [azimuth_deg, altitude_deg] = config.sun_position.unwrap_or(fallback_sun);
let azimuth = azimuth_deg.to_radians();
let altitude = altitude_deg.to_radians();
let cos_alt = altitude.cos();
let dir = [
azimuth.sin() * cos_alt,
azimuth.cos() * cos_alt,
altitude.sin(),
];
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
let sun_direction = if len > 1e-6 {
[dir[0] / len, dir[1] / len, dir[2] / len]
} else {
[0.0, 0.0, 1.0]
};
ComputedSky {
sun_direction,
sun_intensity: config.sun_intensity,
rayleigh_color: config.atmosphere_color,
mie_color: config.halo_color,
sky_enabled: enabled,
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct FogConfig {
pub color: Option<[f32; 4]>,
pub range: Option<[f32; 2]>,
pub density: Option<f32>,
pub horizon_color: Option<[f32; 4]>,
pub horizon_blend: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComputedFog {
pub fog_color: [f32; 4],
pub fog_start: f32,
pub fog_end: f32,
pub fog_density: f32,
pub clear_color: [f32; 4],
}
impl Default for ComputedFog {
fn default() -> Self {
Self {
fog_color: [1.0; 4],
fog_start: 10_000.0,
fog_end: 20_000.0,
fog_density: 0.0,
clear_color: [1.0; 4],
}
}
}
pub fn atmospheric_clear_color(base: [f32; 4], pitch: f64) -> [f32; 4] {
let t = (((pitch - 0.25) / 1.0).clamp(0.0, 1.0)) as f32;
let horizon = [
(base[0] * 0.92 + 0.05).clamp(0.0, 1.0),
(base[1] * 0.95 + 0.06).clamp(0.0, 1.0),
(base[2] * 0.98 + 0.08).clamp(0.0, 1.0),
base[3],
];
[
base[0] * (1.0 - t) + horizon[0] * t,
base[1] * (1.0 - t) + horizon[1] * t,
base[2] * (1.0 - t) + horizon[2] * t,
base[3],
]
}
pub fn compute_fog(
pitch: f64,
camera_distance: f64,
background_color: [f32; 4],
config: Option<&FogConfig>,
) -> ComputedFog {
let auto_clear = atmospheric_clear_color(background_color, pitch);
let auto_density = (((pitch - 0.70) / 0.55).clamp(0.0, 1.0) as f32) * 0.9;
let visible_range = camera_distance / pitch.cos().max(0.05);
let auto_start = (visible_range * 0.55) as f32;
let auto_end = (visible_range * 1.05) as f32;
let (fog_start, fog_end) = match config.and_then(|c| c.range) {
Some([s, e]) => ((visible_range as f32) * s, (visible_range as f32) * e),
None => (auto_start, auto_end),
};
let fog_density = config.and_then(|c| c.density).unwrap_or(auto_density);
let fog_color = config.and_then(|c| c.color).unwrap_or(auto_clear);
let clear_color = match config.and_then(|c| c.horizon_color) {
Some(horizon) => {
let blend = config
.and_then(|c| c.horizon_blend)
.unwrap_or_else(|| ((pitch - 0.25) / 1.0).clamp(0.0, 1.0) as f32);
[
background_color[0] * (1.0 - blend) + horizon[0] * blend,
background_color[1] * (1.0 - blend) + horizon[1] * blend,
background_color[2] * (1.0 - blend) + horizon[2] * blend,
background_color[3],
]
}
None => auto_clear,
};
ComputedFog {
fog_color,
fog_start,
fog_end,
fog_density,
clear_color,
}
}
pub trait StyleInterpolatable: Clone + FromFeatureStateProperty {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self;
}
impl StyleInterpolatable for f32 {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
*a + (*b - *a) * t.clamp(0.0, 1.0)
}
}
impl StyleInterpolatable for [f32; 4] {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
let t = t.clamp(0.0, 1.0);
[
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
a[3] + (b[3] - a[3]) * t,
]
}
}
impl StyleInterpolatable for bool {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
if t < 1.0 {
*a
} else {
*b
}
}
}
impl StyleInterpolatable for String {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
if t < 1.0 {
a.clone()
} else {
b.clone()
}
}
}
impl StyleInterpolatable for SymbolTextJustify {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
if t < 1.0 {
*a
} else {
*b
}
}
}
impl StyleInterpolatable for SymbolTextTransform {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
if t < 1.0 {
*a
} else {
*b
}
}
}
impl StyleInterpolatable for SymbolIconTextFit {
fn interpolate(a: &Self, b: &Self, t: f32) -> Self {
if t < 1.0 {
*a
} else {
*b
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TransitionSpec {
pub duration: f32,
pub delay: f32,
}
impl Default for TransitionSpec {
fn default() -> Self {
Self {
duration: 0.3,
delay: 0.0,
}
}
}
impl TransitionSpec {
pub const INSTANT: Self = Self {
duration: 0.0,
delay: 0.0,
};
pub fn is_active(&self) -> bool {
self.duration > 0.0 || self.delay > 0.0
}
}
fn ease_cubic_in_out(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t < 0.5 {
4.0 * t * t * t
} else {
let f = 2.0 * t - 2.0;
0.5 * f * f * f + 1.0
}
}
#[derive(Debug, Clone)]
pub struct Transitioning<T: StyleInterpolatable> {
prior: T,
target: T,
begin: f64,
end: f64,
}
impl<T: StyleInterpolatable> Transitioning<T> {
pub fn new(prior: T, target: T, now: f64, spec: &TransitionSpec) -> Self {
let begin = now + spec.delay as f64;
let end = begin + spec.duration as f64;
Self {
prior,
target,
begin,
end,
}
}
pub fn settled(value: T) -> Self {
Self {
prior: value.clone(),
target: value,
begin: 0.0,
end: 0.0,
}
}
pub fn resolve(&self, now: f64) -> T {
if now >= self.end {
return self.target.clone();
}
if now < self.begin {
return self.prior.clone();
}
let duration = self.end - self.begin;
if duration <= 0.0 {
return self.target.clone();
}
let t = ((now - self.begin) / duration) as f32;
T::interpolate(&self.prior, &self.target, ease_cubic_in_out(t))
}
pub fn is_active(&self, now: f64) -> bool {
now < self.end
}
pub fn target(&self) -> &T {
&self.target
}
pub fn retarget(&mut self, new_target: T, now: f64, spec: &TransitionSpec) {
let current = self.resolve(now);
self.prior = current;
self.target = new_target;
self.begin = now + spec.delay as f64;
self.end = self.begin + spec.duration as f64;
}
}
#[derive(Debug, Clone)]
pub struct LayerTransitionState {
pub spec: TransitionSpec,
pub opacity: Transitioning<f32>,
pub color: Transitioning<[f32; 4]>,
pub secondary_color: Transitioning<[f32; 4]>,
pub width: Transitioning<f32>,
pub height: Transitioning<f32>,
pub base: Transitioning<f32>,
}
impl LayerTransitionState {
pub fn from_initial(
spec: TransitionSpec,
opacity: f32,
color: [f32; 4],
secondary_color: [f32; 4],
width: f32,
height: f32,
base: f32,
) -> Self {
Self {
spec,
opacity: Transitioning::settled(opacity),
color: Transitioning::settled(color),
secondary_color: Transitioning::settled(secondary_color),
width: Transitioning::settled(width),
height: Transitioning::settled(height),
base: Transitioning::settled(base),
}
}
fn update_f32(trans: &mut Transitioning<f32>, new_val: f32, now: f64, spec: &TransitionSpec) {
if (trans.target() - new_val).abs() > 1e-6 {
trans.retarget(new_val, now, spec);
}
}
fn update_color(
trans: &mut Transitioning<[f32; 4]>,
new_val: [f32; 4],
now: f64,
spec: &TransitionSpec,
) {
let old = trans.target();
let diff = (old[0] - new_val[0]).abs()
+ (old[1] - new_val[1]).abs()
+ (old[2] - new_val[2]).abs()
+ (old[3] - new_val[3]).abs();
if diff > 1e-5 {
trans.retarget(new_val, now, spec);
}
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
now: f64,
opacity: f32,
color: [f32; 4],
secondary_color: [f32; 4],
width: f32,
height: f32,
base: f32,
) {
let spec = self.spec;
Self::update_f32(&mut self.opacity, opacity, now, &spec);
Self::update_color(&mut self.color, color, now, &spec);
Self::update_color(&mut self.secondary_color, secondary_color, now, &spec);
Self::update_f32(&mut self.width, width, now, &spec);
Self::update_f32(&mut self.height, height, now, &spec);
Self::update_f32(&mut self.base, base, now, &spec);
}
pub fn resolve(&self, now: f64) -> ResolvedTransitions {
ResolvedTransitions {
opacity: self.opacity.resolve(now),
color: self.color.resolve(now),
secondary_color: self.secondary_color.resolve(now),
width: self.width.resolve(now),
height: self.height.resolve(now),
base: self.base.resolve(now),
}
}
pub fn has_active_transitions(&self, now: f64) -> bool {
self.opacity.is_active(now)
|| self.color.is_active(now)
|| self.secondary_color.is_active(now)
|| self.width.is_active(now)
|| self.height.is_active(now)
|| self.base.is_active(now)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResolvedTransitions {
pub opacity: f32,
pub color: [f32; 4],
pub secondary_color: [f32; 4],
pub width: f32,
pub height: f32,
pub base: f32,
}
pub type RasterSourceFactory = Arc<dyn Fn() -> Box<dyn TileSource> + Send + Sync>;
pub type VectorTileSourceFactory = Arc<dyn Fn() -> Box<dyn TileSource> + Send + Sync>;
pub type TerrainSourceFactory = Arc<dyn Fn() -> Box<dyn ElevationSource> + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StyleSourceKind {
Raster,
Terrain,
GeoJson,
VectorTile,
Image,
Video,
Canvas,
Model,
}
impl StyleSourceKind {
pub fn as_str(self) -> &'static str {
match self {
StyleSourceKind::Raster => "raster",
StyleSourceKind::Terrain => "terrain",
StyleSourceKind::GeoJson => "geojson",
StyleSourceKind::VectorTile => "vector",
StyleSourceKind::Image => "image",
StyleSourceKind::Video => "video",
StyleSourceKind::Canvas => "canvas",
StyleSourceKind::Model => "model",
}
}
}
#[derive(Clone)]
pub enum StyleSource {
Raster(RasterSource),
Terrain(TerrainSource),
GeoJson(GeoJsonSource),
VectorTile(VectorTileSource),
Image(ImageSource),
Video(VideoSource),
Canvas(CanvasSource),
Model(ModelSource),
}
impl StyleSource {
pub fn kind_name(&self) -> &'static str {
self.kind().as_str()
}
pub fn kind(&self) -> StyleSourceKind {
match self {
StyleSource::Raster(_) => StyleSourceKind::Raster,
StyleSource::Terrain(_) => StyleSourceKind::Terrain,
StyleSource::GeoJson(_) => StyleSourceKind::GeoJson,
StyleSource::VectorTile(_) => StyleSourceKind::VectorTile,
StyleSource::Image(_) => StyleSourceKind::Image,
StyleSource::Video(_) => StyleSourceKind::Video,
StyleSource::Canvas(_) => StyleSourceKind::Canvas,
StyleSource::Model(_) => StyleSourceKind::Model,
}
}
}
impl fmt::Debug for StyleSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StyleSource::Raster(src) => f.debug_tuple("Raster").field(src).finish(),
StyleSource::Terrain(src) => f.debug_tuple("Terrain").field(src).finish(),
StyleSource::GeoJson(src) => f.debug_tuple("GeoJson").field(src).finish(),
StyleSource::VectorTile(src) => f.debug_tuple("VectorTile").field(src).finish(),
StyleSource::Image(src) => f.debug_tuple("Image").field(src).finish(),
StyleSource::Video(src) => f.debug_tuple("Video").field(src).finish(),
StyleSource::Canvas(src) => f.debug_tuple("Canvas").field(src).finish(),
StyleSource::Model(src) => f.debug_tuple("Model").field(src).finish(),
}
}
}
#[derive(Clone)]
pub struct RasterSource {
pub cache_capacity: usize,
pub selection: TileSelectionConfig,
pub factory: RasterSourceFactory,
}
impl RasterSource {
pub fn new(factory: impl Fn() -> Box<dyn TileSource> + Send + Sync + 'static) -> Self {
Self {
cache_capacity: 256,
selection: TileSelectionConfig::default(),
factory: Arc::new(factory),
}
}
pub fn with_cache_capacity(mut self, cache_capacity: usize) -> Self {
self.cache_capacity = cache_capacity;
self
}
pub fn with_selection(mut self, selection: TileSelectionConfig) -> Self {
self.selection = selection;
self
}
}
impl fmt::Debug for RasterSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RasterSource")
.field("cache_capacity", &self.cache_capacity)
.field("selection", &self.selection)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct TerrainSource {
pub enabled: bool,
pub vertical_exaggeration: f64,
pub mesh_resolution: u16,
pub skirt_depth: f64,
pub cache_capacity: usize,
pub factory: TerrainSourceFactory,
}
impl TerrainSource {
pub fn new(factory: impl Fn() -> Box<dyn ElevationSource> + Send + Sync + 'static) -> Self {
Self {
enabled: true,
vertical_exaggeration: 1.0,
mesh_resolution: 64,
skirt_depth: 100.0,
cache_capacity: 256,
factory: Arc::new(factory),
}
}
pub fn with_cache_capacity(mut self, cache_capacity: usize) -> Self {
self.cache_capacity = cache_capacity;
self
}
pub fn to_terrain_config(&self) -> TerrainConfig {
TerrainConfig {
enabled: self.enabled,
vertical_exaggeration: self.vertical_exaggeration,
mesh_resolution: self.mesh_resolution,
skirt_depth: self.skirt_depth,
source_max_zoom: TerrainConfig::default().source_max_zoom,
source: (self.factory)(),
}
}
}
impl fmt::Debug for TerrainSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TerrainSource")
.field("enabled", &self.enabled)
.field("vertical_exaggeration", &self.vertical_exaggeration)
.field("mesh_resolution", &self.mesh_resolution)
.field("skirt_depth", &self.skirt_depth)
.field("cache_capacity", &self.cache_capacity)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct GeoJsonSource {
pub data: FeatureCollection,
cluster_index: Option<Arc<PointCluster>>,
}
impl fmt::Debug for GeoJsonSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GeoJsonSource")
.field("features", &self.data.len())
.field("clustered", &self.cluster_index.is_some())
.finish()
}
}
impl GeoJsonSource {
pub fn new(data: FeatureCollection) -> Self {
Self {
data,
cluster_index: None,
}
}
pub fn with_clustering(mut self, options: ClusterOptions) -> Self {
let mut cluster = PointCluster::new(options);
cluster.load(&self.data);
self.cluster_index = Some(Arc::new(cluster));
self
}
pub fn is_clustered(&self) -> bool {
self.cluster_index.is_some()
}
pub fn features_at_zoom(&self, zoom: u8) -> Cow<'_, FeatureCollection> {
if let Some(ref cluster) = self.cluster_index {
Cow::Owned(cluster.get_clusters_for_zoom(zoom))
} else {
Cow::Borrowed(&self.data)
}
}
}
#[derive(Clone)]
pub struct VectorTileSource {
pub data: FeatureCollection,
pub source_layers: HashMap<String, FeatureCollection>,
pub factory: Option<VectorTileSourceFactory>,
pub cache_capacity: usize,
pub selection: TileSelectionConfig,
}
impl fmt::Debug for VectorTileSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VectorTileSource")
.field("feature_count", &self.data.len())
.field("source_layer_count", &self.source_layers.len())
.field("streamed", &self.factory.is_some())
.field("cache_capacity", &self.cache_capacity)
.field("selection", &self.selection)
.finish()
}
}
impl VectorTileSource {
pub fn new(data: FeatureCollection) -> Self {
Self {
data,
source_layers: HashMap::new(),
factory: None,
cache_capacity: 256,
selection: TileSelectionConfig::default(),
}
}
pub fn streamed(factory: impl Fn() -> Box<dyn TileSource> + Send + Sync + 'static) -> Self {
Self {
data: FeatureCollection::default(),
source_layers: HashMap::new(),
factory: Some(Arc::new(factory)),
cache_capacity: 256,
selection: TileSelectionConfig::default(),
}
}
pub fn from_source_layers(source_layers: HashMap<String, FeatureCollection>) -> Self {
let mut data = FeatureCollection::default();
for features in source_layers.values() {
data.features.extend(features.features.iter().cloned());
}
Self {
data,
source_layers,
factory: None,
cache_capacity: 256,
selection: TileSelectionConfig::default(),
}
}
pub fn with_source_layer(mut self, name: impl Into<String>, data: FeatureCollection) -> Self {
self.source_layers.insert(name.into(), data);
self.rebuild_flattened_data();
self
}
pub fn source_layer(&self, name: &str) -> Option<&FeatureCollection> {
self.source_layers.get(name)
}
pub fn has_source_layers(&self) -> bool {
!self.source_layers.is_empty()
}
pub fn is_streamed(&self) -> bool {
self.factory.is_some()
}
pub fn with_cache_capacity(mut self, cache_capacity: usize) -> Self {
self.cache_capacity = cache_capacity;
self
}
pub fn with_selection(mut self, selection: TileSelectionConfig) -> Self {
self.selection = selection;
self
}
pub fn make_tile_source(&self) -> Option<Box<dyn TileSource>> {
self.factory.as_ref().map(|factory| (factory)())
}
fn rebuild_flattened_data(&mut self) {
let mut data = FeatureCollection::default();
for features in self.source_layers.values() {
data.features.extend(features.features.iter().cloned());
}
self.data = data;
}
}
#[derive(Clone)]
pub struct ImageSource {
pub cache_capacity: usize,
pub selection: TileSelectionConfig,
pub factory: RasterSourceFactory,
}
impl ImageSource {
pub fn new(factory: impl Fn() -> Box<dyn TileSource> + Send + Sync + 'static) -> Self {
Self {
cache_capacity: 16,
selection: TileSelectionConfig::default(),
factory: Arc::new(factory),
}
}
pub fn with_cache_capacity(mut self, cache_capacity: usize) -> Self {
self.cache_capacity = cache_capacity;
self
}
pub fn with_selection(mut self, selection: TileSelectionConfig) -> Self {
self.selection = selection;
self
}
}
impl fmt::Debug for ImageSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageSource")
.field("cache_capacity", &self.cache_capacity)
.field("selection", &self.selection)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct VideoSource {
pub coordinates: [GeoCoord; 4],
pub factory: FrameProviderFactory,
}
impl VideoSource {
pub fn new(
coordinates: [GeoCoord; 4],
factory: impl Fn() -> Box<dyn crate::layers::FrameProvider> + Send + Sync + 'static,
) -> Self {
Self {
coordinates,
factory: Arc::new(factory),
}
}
}
impl fmt::Debug for VideoSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VideoSource")
.field("coordinates", &self.coordinates)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct CanvasSource {
pub coordinates: [GeoCoord; 4],
pub factory: FrameProviderFactory,
pub animate: bool,
}
impl CanvasSource {
pub fn new(
coordinates: [GeoCoord; 4],
factory: impl Fn() -> Box<dyn crate::layers::FrameProvider> + Send + Sync + 'static,
) -> Self {
Self {
coordinates,
factory: Arc::new(factory),
animate: true,
}
}
pub fn with_animate(mut self, animate: bool) -> Self {
self.animate = animate;
self
}
}
impl fmt::Debug for CanvasSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CanvasSource")
.field("coordinates", &self.coordinates)
.field("animate", &self.animate)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default)]
pub struct ModelSource {
pub instances: Vec<ModelInstance>,
}
impl ModelSource {
pub fn new(instances: Vec<ModelInstance>) -> Self {
Self { instances }
}
}
#[derive(Debug, Default)]
pub struct StyleDocument {
sources: HashMap<StyleSourceId, StyleSource>,
layers: Vec<StyleLayer>,
terrain_source: Option<StyleSourceId>,
projection: StyleProjection,
fog: Option<FogConfig>,
lights: Option<LightConfig>,
sky: Option<SkyConfig>,
transition: TransitionSpec,
}
pub type StyleValue<T> = crate::expression::Expression<T>;
pub trait FromFeatureStateProperty: Sized {
fn from_feature_state_property(prop: &PropertyValue) -> Option<Self>;
}
impl FromFeatureStateProperty for f32 {
fn from_feature_state_property(prop: &PropertyValue) -> Option<Self> {
prop.as_f64().map(|v| v as f32)
}
}
impl FromFeatureStateProperty for [f32; 4] {
fn from_feature_state_property(_prop: &PropertyValue) -> Option<Self> {
None
}
}
impl FromFeatureStateProperty for bool {
fn from_feature_state_property(prop: &PropertyValue) -> Option<Self> {
prop.as_bool()
}
}
impl FromFeatureStateProperty for String {
fn from_feature_state_property(prop: &PropertyValue) -> Option<Self> {
prop.as_str().map(|s| s.to_owned())
}
}
impl FromFeatureStateProperty for SymbolTextJustify {
fn from_feature_state_property(_prop: &PropertyValue) -> Option<Self> {
None
}
}
impl FromFeatureStateProperty for SymbolTextTransform {
fn from_feature_state_property(_prop: &PropertyValue) -> Option<Self> {
None
}
}
impl FromFeatureStateProperty for SymbolIconTextFit {
fn from_feature_state_property(_prop: &PropertyValue) -> Option<Self> {
None
}
}
#[derive(Debug, Clone)]
pub struct StyleLayerMeta {
pub id: StyleLayerId,
pub name: String,
pub visible: StyleValue<bool>,
pub opacity: StyleValue<f32>,
pub min_zoom: Option<f32>,
pub max_zoom: Option<f32>,
pub transition: TransitionSpec,
}
impl StyleLayerMeta {
pub fn new(id: impl Into<String>) -> Self {
let id = id.into();
Self {
name: id.clone(),
id,
visible: StyleValue::Constant(true),
opacity: StyleValue::Constant(1.0),
min_zoom: None,
max_zoom: None,
transition: TransitionSpec::default(),
}
}
fn visible_in_context(&self, ctx: StyleEvalContext) -> bool {
if let Some(min_zoom) = self.min_zoom {
if ctx.zoom < min_zoom {
return false;
}
}
if let Some(max_zoom) = self.max_zoom {
if ctx.zoom > max_zoom {
return false;
}
}
self.visible.evaluate_with_context(ctx)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct BackgroundStyleLayer {
pub meta: StyleLayerMeta,
pub color: StyleValue<[f32; 4]>,
}
impl BackgroundStyleLayer {
pub fn new(id: impl Into<String>, color: impl Into<StyleValue<[f32; 4]>>) -> Self {
Self {
meta: StyleLayerMeta::new(id),
color: color.into(),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct HillshadeStyleLayer {
pub meta: StyleLayerMeta,
pub highlight_color: StyleValue<[f32; 4]>,
pub shadow_color: StyleValue<[f32; 4]>,
pub accent_color: StyleValue<[f32; 4]>,
pub illumination_direction_deg: StyleValue<f32>,
pub illumination_altitude_deg: StyleValue<f32>,
pub exaggeration: StyleValue<f32>,
}
impl HillshadeStyleLayer {
pub fn new(id: impl Into<String>) -> Self {
Self {
meta: StyleLayerMeta::new(id),
highlight_color: [1.0, 1.0, 1.0, 1.0].into(),
shadow_color: [0.0, 0.0, 0.0, 1.0].into(),
accent_color: [0.42, 0.48, 0.42, 1.0].into(),
illumination_direction_deg: 335.0.into(),
illumination_altitude_deg: 45.0.into(),
exaggeration: 1.0.into(),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct RasterStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
}
impl RasterStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct VectorStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
pub source_layer: Option<String>,
pub fill_color: StyleValue<[f32; 4]>,
pub stroke_color: StyleValue<[f32; 4]>,
pub stroke_width: StyleValue<f32>,
}
impl VectorStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
source_layer: None,
fill_color: VectorStyle::default().fill_color.into(),
stroke_color: VectorStyle::default().stroke_color.into(),
stroke_width: VectorStyle::default().stroke_width.into(),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct FillStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
pub source_layer: Option<String>,
pub fill_color: StyleValue<[f32; 4]>,
pub outline_color: StyleValue<[f32; 4]>,
pub outline_width: StyleValue<f32>,
pub fill_pattern: Option<std::sync::Arc<crate::PatternImage>>,
}
impl FillStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
let style = VectorStyle::default();
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
source_layer: None,
fill_color: style.fill_color.into(),
outline_color: style.stroke_color.into(),
outline_width: style.stroke_width.into(),
fill_pattern: None,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct LineStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
pub source_layer: Option<String>,
pub color: StyleValue<[f32; 4]>,
pub width: StyleValue<f32>,
pub line_cap: LineCap,
pub line_join: LineJoin,
pub miter_limit: f32,
pub dash_array: Option<Vec<f32>>,
pub line_gradient: Option<crate::visualization::ColorRamp>,
pub line_pattern: Option<std::sync::Arc<crate::PatternImage>>,
}
impl LineStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
let style = VectorStyle::default();
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
source_layer: None,
color: style.stroke_color.into(),
width: style.stroke_width.into(),
line_cap: LineCap::default(),
line_join: LineJoin::default(),
miter_limit: 2.0,
dash_array: None,
line_gradient: None,
line_pattern: None,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct CircleStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
pub source_layer: Option<String>,
pub color: StyleValue<[f32; 4]>,
pub radius: StyleValue<f32>,
pub stroke_color: StyleValue<[f32; 4]>,
pub stroke_width: StyleValue<f32>,
}
impl CircleStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
let style = VectorStyle::default();
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
source_layer: None,
color: style.fill_color.into(),
radius: style.point_radius.into(),
stroke_color: style.stroke_color.into(),
stroke_width: style.stroke_width.into(),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct HeatmapStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
pub source_layer: Option<String>,
pub color: StyleValue<[f32; 4]>,
pub radius: StyleValue<f32>,
pub intensity: StyleValue<f32>,
}
impl HeatmapStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
let style = VectorStyle::default();
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
source_layer: None,
color: style.fill_color.into(),
radius: style.heatmap_radius.into(),
intensity: style.heatmap_intensity.into(),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct FillExtrusionStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
pub source_layer: Option<String>,
pub color: StyleValue<[f32; 4]>,
pub base: StyleValue<f32>,
pub height: StyleValue<f32>,
}
impl FillExtrusionStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
let style = VectorStyle::default();
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
source_layer: None,
color: style.fill_color.into(),
base: style.extrusion_base.into(),
height: style.extrusion_height.into(),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct SymbolStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
pub source_layer: Option<String>,
pub color: StyleValue<[f32; 4]>,
pub halo_color: StyleValue<[f32; 4]>,
pub size: StyleValue<f32>,
pub text_field: Option<StyleValue<String>>,
pub icon_image: Option<StyleValue<String>>,
pub font_stack: StyleValue<String>,
pub padding: StyleValue<f32>,
pub allow_overlap: StyleValue<bool>,
pub text_allow_overlap: Option<StyleValue<bool>>,
pub icon_allow_overlap: Option<StyleValue<bool>>,
pub text_optional: Option<StyleValue<bool>>,
pub icon_optional: Option<StyleValue<bool>>,
pub text_ignore_placement: Option<StyleValue<bool>>,
pub icon_ignore_placement: Option<StyleValue<bool>>,
pub radial_offset: Option<StyleValue<f32>>,
pub variable_anchor_offsets: Option<Vec<(SymbolAnchor, [f32; 2])>>,
pub anchor: SymbolAnchor,
pub justify: StyleValue<SymbolTextJustify>,
pub transform: StyleValue<SymbolTextTransform>,
pub max_width: Option<StyleValue<f32>>,
pub line_height: Option<StyleValue<f32>>,
pub letter_spacing: Option<StyleValue<f32>>,
pub icon_text_fit: StyleValue<SymbolIconTextFit>,
pub icon_text_fit_padding: [f32; 4],
pub sort_key: Option<StyleValue<f32>>,
pub placement: SymbolPlacement,
pub spacing: StyleValue<f32>,
pub max_angle: StyleValue<f32>,
pub keep_upright: StyleValue<bool>,
pub variable_anchors: Vec<SymbolAnchor>,
pub writing_mode: SymbolWritingMode,
pub offset: [f32; 2],
}
impl SymbolStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
let style = VectorStyle::default();
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
source_layer: None,
color: style.fill_color.into(),
halo_color: style.symbol_halo_color.into(),
size: style.symbol_size.into(),
text_field: None,
icon_image: None,
font_stack: style.symbol_font_stack.into(),
padding: style.symbol_padding.into(),
allow_overlap: style.symbol_allow_overlap.into(),
text_allow_overlap: None,
icon_allow_overlap: None,
text_optional: None,
icon_optional: None,
text_ignore_placement: None,
icon_ignore_placement: None,
radial_offset: None,
variable_anchor_offsets: None,
anchor: style.symbol_text_anchor,
justify: style.symbol_text_justify.into(),
transform: style.symbol_text_transform.into(),
max_width: None,
line_height: None,
letter_spacing: None,
icon_text_fit: style.symbol_icon_text_fit.into(),
icon_text_fit_padding: style.symbol_icon_text_fit_padding,
sort_key: None,
placement: style.symbol_placement,
spacing: style.symbol_spacing.into(),
max_angle: style.symbol_max_angle.into(),
keep_upright: style.symbol_keep_upright.into(),
variable_anchors: style.symbol_anchors.clone(),
writing_mode: style.symbol_writing_mode,
offset: style.symbol_offset,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct ModelStyleLayer {
pub meta: StyleLayerMeta,
pub source: StyleSourceId,
}
impl ModelStyleLayer {
pub fn new(id: impl Into<String>, source: impl Into<String>) -> Self {
Self {
meta: StyleLayerMeta::new(id),
source: source.into(),
}
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum StyleLayer {
Background(BackgroundStyleLayer),
Hillshade(HillshadeStyleLayer),
Raster(RasterStyleLayer),
Vector(VectorStyleLayer),
Fill(FillStyleLayer),
Line(LineStyleLayer),
Circle(CircleStyleLayer),
Heatmap(HeatmapStyleLayer),
FillExtrusion(FillExtrusionStyleLayer),
Symbol(SymbolStyleLayer),
Model(ModelStyleLayer),
}
impl StyleLayer {
pub fn id(&self) -> &str {
self.meta().id.as_str()
}
pub fn meta(&self) -> &StyleLayerMeta {
match self {
StyleLayer::Background(layer) => &layer.meta,
StyleLayer::Hillshade(layer) => &layer.meta,
StyleLayer::Raster(layer) => &layer.meta,
StyleLayer::Vector(layer) => &layer.meta,
StyleLayer::Fill(layer) => &layer.meta,
StyleLayer::Line(layer) => &layer.meta,
StyleLayer::Circle(layer) => &layer.meta,
StyleLayer::Heatmap(layer) => &layer.meta,
StyleLayer::FillExtrusion(layer) => &layer.meta,
StyleLayer::Symbol(layer) => &layer.meta,
StyleLayer::Model(layer) => &layer.meta,
}
}
pub fn meta_mut(&mut self) -> &mut StyleLayerMeta {
match self {
StyleLayer::Background(layer) => &mut layer.meta,
StyleLayer::Hillshade(layer) => &mut layer.meta,
StyleLayer::Raster(layer) => &mut layer.meta,
StyleLayer::Vector(layer) => &mut layer.meta,
StyleLayer::Fill(layer) => &mut layer.meta,
StyleLayer::Line(layer) => &mut layer.meta,
StyleLayer::Circle(layer) => &mut layer.meta,
StyleLayer::Heatmap(layer) => &mut layer.meta,
StyleLayer::FillExtrusion(layer) => &mut layer.meta,
StyleLayer::Symbol(layer) => &mut layer.meta,
StyleLayer::Model(layer) => &mut layer.meta,
}
}
pub fn to_runtime_layer_with_context(
&self,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
match self {
StyleLayer::Background(layer) => Ok(Box::new(evaluate_background_layer(layer, ctx))),
StyleLayer::Hillshade(layer) => Ok(Box::new(evaluate_hillshade_layer(layer, ctx))),
StyleLayer::Raster(layer) => evaluate_raster_layer(layer, sources, ctx),
StyleLayer::Vector(layer) => evaluate_vector_layer(layer, sources, ctx),
StyleLayer::Fill(layer) => evaluate_fill_layer(layer, sources, ctx),
StyleLayer::Line(layer) => evaluate_line_layer(layer, sources, ctx),
StyleLayer::Circle(layer) => evaluate_circle_layer(layer, sources, ctx),
StyleLayer::Heatmap(layer) => evaluate_heatmap_layer(layer, sources, ctx),
StyleLayer::FillExtrusion(layer) => evaluate_fill_extrusion_layer(layer, sources, ctx),
StyleLayer::Symbol(layer) => evaluate_symbol_layer(layer, sources, ctx),
StyleLayer::Model(layer) => evaluate_model_layer(layer, sources, ctx),
}
}
pub fn apply_to_runtime_layer_with_context(
&self,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
match self {
StyleLayer::Background(layer) => apply_background_to_runtime(layer, runtime, ctx),
StyleLayer::Hillshade(layer) => apply_hillshade_to_runtime(layer, runtime, ctx),
StyleLayer::Raster(layer) => apply_raster_to_runtime(layer, runtime, sources, ctx),
StyleLayer::Vector(layer) => apply_vector_to_runtime(layer, runtime, sources, ctx),
StyleLayer::Fill(layer) => apply_fill_to_runtime(layer, runtime, sources, ctx),
StyleLayer::Line(layer) => apply_line_to_runtime(layer, runtime, sources, ctx),
StyleLayer::Circle(layer) => apply_circle_to_runtime(layer, runtime, sources, ctx),
StyleLayer::Heatmap(layer) => apply_heatmap_to_runtime(layer, runtime, sources, ctx),
StyleLayer::FillExtrusion(layer) => {
apply_fill_extrusion_to_runtime(layer, runtime, sources, ctx)
}
StyleLayer::Symbol(layer) => apply_symbol_to_runtime(layer, runtime, sources, ctx),
StyleLayer::Model(layer) => apply_model_to_runtime(layer, runtime, sources, ctx),
}
}
pub fn source_id(&self) -> Option<&str> {
match self {
StyleLayer::Background(_) | StyleLayer::Hillshade(_) => None,
StyleLayer::Raster(layer) => Some(layer.source.as_str()),
StyleLayer::Vector(layer) => Some(layer.source.as_str()),
StyleLayer::Fill(layer) => Some(layer.source.as_str()),
StyleLayer::Line(layer) => Some(layer.source.as_str()),
StyleLayer::Circle(layer) => Some(layer.source.as_str()),
StyleLayer::Heatmap(layer) => Some(layer.source.as_str()),
StyleLayer::FillExtrusion(layer) => Some(layer.source.as_str()),
StyleLayer::Symbol(layer) => Some(layer.source.as_str()),
StyleLayer::Model(layer) => Some(layer.source.as_str()),
}
}
pub fn source_layer(&self) -> Option<&str> {
match self {
StyleLayer::Vector(layer) => layer.source_layer.as_deref(),
StyleLayer::Fill(layer) => layer.source_layer.as_deref(),
StyleLayer::Line(layer) => layer.source_layer.as_deref(),
StyleLayer::Circle(layer) => layer.source_layer.as_deref(),
StyleLayer::Heatmap(layer) => layer.source_layer.as_deref(),
StyleLayer::FillExtrusion(layer) => layer.source_layer.as_deref(),
StyleLayer::Symbol(layer) => layer.source_layer.as_deref(),
_ => None,
}
}
pub fn uses_source(&self, source_id: &str) -> bool {
self.source_id() == Some(source_id)
}
pub fn has_feature_state_driven_paint(&self) -> bool {
match self {
StyleLayer::Fill(l) => {
l.fill_color.is_feature_state_driven()
|| l.outline_color.is_feature_state_driven()
|| l.outline_width.is_feature_state_driven()
}
StyleLayer::Line(l) => {
l.color.is_feature_state_driven() || l.width.is_feature_state_driven()
}
StyleLayer::Circle(l) => {
l.color.is_feature_state_driven()
|| l.radius.is_feature_state_driven()
|| l.stroke_color.is_feature_state_driven()
|| l.stroke_width.is_feature_state_driven()
}
StyleLayer::Heatmap(l) => {
l.color.is_feature_state_driven()
|| l.radius.is_feature_state_driven()
|| l.intensity.is_feature_state_driven()
}
StyleLayer::FillExtrusion(l) => {
l.color.is_feature_state_driven()
|| l.base.is_feature_state_driven()
|| l.height.is_feature_state_driven()
}
StyleLayer::Symbol(l) => {
l.color.is_feature_state_driven()
|| l.halo_color.is_feature_state_driven()
|| l.size.is_feature_state_driven()
}
StyleLayer::Vector(l) => {
l.fill_color.is_feature_state_driven()
|| l.stroke_color.is_feature_state_driven()
|| l.stroke_width.is_feature_state_driven()
}
StyleLayer::Background(_)
| StyleLayer::Hillshade(_)
| StyleLayer::Raster(_)
| StyleLayer::Model(_) => false,
}
}
pub fn resolve_style_with_feature_state(
&self,
ctx: &StyleEvalContextFull<'_>,
) -> Option<VectorStyle> {
match self {
StyleLayer::Fill(l) => Some(fill_style_with_state(l, ctx)),
StyleLayer::Line(l) => Some(line_style_with_state(l, ctx)),
StyleLayer::Circle(l) => Some(circle_style_with_state(l, ctx)),
StyleLayer::Heatmap(l) => Some(heatmap_style_with_state(l, ctx)),
StyleLayer::FillExtrusion(l) => Some(fill_extrusion_style_with_state(l, ctx)),
StyleLayer::Symbol(l) => Some(symbol_style_with_state(l, ctx)),
StyleLayer::Vector(l) => Some(vector_style_with_state(l, ctx)),
StyleLayer::Background(_)
| StyleLayer::Hillshade(_)
| StyleLayer::Raster(_)
| StyleLayer::Model(_) => None,
}
}
}
impl StyleDocument {
pub fn new() -> Self {
Self::default()
}
pub fn add_source(
&mut self,
id: impl Into<String>,
source: StyleSource,
) -> Result<(), StyleError> {
let id = id.into();
if self.sources.contains_key(&id) {
return Err(StyleError::DuplicateSourceId(id));
}
self.sources.insert(id, source);
Ok(())
}
pub fn set_source(&mut self, id: impl Into<String>, source: StyleSource) {
self.sources.insert(id.into(), source);
}
pub fn remove_source(&mut self, id: &str) -> Option<StyleSource> {
if self.terrain_source.as_deref() == Some(id) {
self.terrain_source = None;
}
self.sources.remove(id)
}
pub fn source(&self, id: &str) -> Option<&StyleSource> {
self.sources.get(id)
}
pub fn sources(&self) -> impl Iterator<Item = (&str, &StyleSource)> {
self.sources
.iter()
.map(|(id, source)| (id.as_str(), source))
}
pub fn set_terrain_source(&mut self, source_id: Option<impl Into<String>>) {
self.terrain_source = source_id.map(Into::into);
}
pub fn terrain_source(&self) -> Option<&str> {
self.terrain_source.as_deref()
}
pub fn set_projection(&mut self, projection: StyleProjection) {
self.projection = projection;
}
pub fn projection(&self) -> StyleProjection {
self.projection
}
pub fn set_fog(&mut self, fog: Option<FogConfig>) {
self.fog = fog;
}
pub fn fog(&self) -> Option<&FogConfig> {
self.fog.as_ref()
}
pub fn set_lights(&mut self, lights: Option<LightConfig>) {
self.lights = lights;
}
pub fn lights(&self) -> Option<&LightConfig> {
self.lights.as_ref()
}
pub fn set_sky(&mut self, sky: Option<SkyConfig>) {
self.sky = sky;
}
pub fn sky(&self) -> Option<&SkyConfig> {
self.sky.as_ref()
}
pub fn set_transition(&mut self, spec: TransitionSpec) {
self.transition = spec;
}
pub fn transition(&self) -> TransitionSpec {
self.transition
}
pub fn add_layer(&mut self, layer: StyleLayer) -> Result<(), StyleError> {
if self
.layers
.iter()
.any(|existing| existing.id() == layer.id())
{
return Err(StyleError::DuplicateLayerId(layer.id().to_owned()));
}
self.layers.push(layer);
Ok(())
}
pub fn insert_layer_before(
&mut self,
before_id: &str,
layer: StyleLayer,
) -> Result<(), StyleError> {
if self
.layers
.iter()
.any(|existing| existing.id() == layer.id())
{
return Err(StyleError::DuplicateLayerId(layer.id().to_owned()));
}
if let Some(index) = self.layer_index(before_id) {
self.layers.insert(index, layer);
} else {
self.layers.push(layer);
}
Ok(())
}
pub fn move_layer_before(&mut self, layer_id: &str, before_id: &str) -> bool {
let Some(from) = self.layer_index(layer_id) else {
return false;
};
let layer = self.layers.remove(from);
let to = self.layer_index(before_id).unwrap_or(self.layers.len());
self.layers.insert(to, layer);
true
}
pub fn remove_layer(&mut self, layer_id: &str) -> Option<StyleLayer> {
self.layer_index(layer_id)
.map(|index| self.layers.remove(index))
}
pub fn layer(&self, layer_id: &str) -> Option<&StyleLayer> {
self.layers.iter().find(|layer| layer.id() == layer_id)
}
pub fn layer_mut(&mut self, layer_id: &str) -> Option<&mut StyleLayer> {
self.layers.iter_mut().find(|layer| layer.id() == layer_id)
}
pub fn layers(&self) -> &[StyleLayer] {
&self.layers
}
pub fn to_runtime_layers(&self) -> Result<Vec<Box<dyn Layer>>, StyleError> {
self.to_runtime_layers_with_context(StyleEvalContext::default())
}
pub fn to_runtime_layers_with_context(
&self,
ctx: StyleEvalContext,
) -> Result<Vec<Box<dyn Layer>>, StyleError> {
self.layers
.iter()
.map(|layer| layer.to_runtime_layer_with_context(&self.sources, ctx))
.collect()
}
pub fn to_terrain_config(&self) -> Result<Option<(TerrainConfig, usize)>, StyleError> {
let Some(source_id) = self.terrain_source.as_deref() else {
return Ok(None);
};
let Some(source) = self.sources.get(source_id) else {
return Err(StyleError::MissingSource(source_id.to_owned()));
};
match source {
StyleSource::Terrain(terrain) => {
Ok(Some((terrain.to_terrain_config(), terrain.cache_capacity)))
}
other => Err(StyleError::SourceKindMismatch {
layer_id: "<terrain>".to_owned(),
source_id: source_id.to_owned(),
expected: "terrain",
actual: other.kind_name(),
}),
}
}
#[allow(dead_code)]
pub(crate) fn apply_runtime_layers_with_context(
&self,
layers: &mut crate::layers::LayerStack,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
if layers.len() != self.layers.len() {
return Ok(());
}
for (style_layer, runtime_layer) in self.layers.iter().zip(layers.iter_mut()) {
style_layer.apply_to_runtime_layer_with_context(
runtime_layer.as_mut(),
&self.sources,
ctx,
)?;
}
Ok(())
}
fn layer_index(&self, layer_id: &str) -> Option<usize> {
self.layers.iter().position(|layer| layer.id() == layer_id)
}
pub fn source_is_used(&self, source_id: &str) -> bool {
self.terrain_source.as_deref() == Some(source_id)
|| self.layers.iter().any(|layer| layer.uses_source(source_id))
}
pub fn layer_ids_using_source(&self, source_id: &str) -> Vec<&str> {
self.layers
.iter()
.filter(|layer| layer.uses_source(source_id))
.map(|layer| layer.id())
.collect()
}
}
fn evaluate_background_layer(
layer: &BackgroundStyleLayer,
ctx: StyleEvalContext,
) -> BackgroundLayer {
let mut runtime = BackgroundLayer::new(
layer.meta.name.clone(),
layer.color.evaluate_with_context(ctx),
);
apply_shared_meta(&mut runtime, &layer.meta, ctx);
runtime
}
fn evaluate_hillshade_layer(layer: &HillshadeStyleLayer, ctx: StyleEvalContext) -> HillshadeLayer {
let mut runtime = HillshadeLayer::new(layer.meta.name.clone());
apply_shared_meta(&mut runtime, &layer.meta, ctx);
runtime.set_highlight_color(layer.highlight_color.evaluate_with_context(ctx));
runtime.set_shadow_color(layer.shadow_color.evaluate_with_context(ctx));
runtime.set_accent_color(layer.accent_color.evaluate_with_context(ctx));
runtime.set_illumination_direction_deg(
layer.illumination_direction_deg.evaluate_with_context(ctx),
);
runtime
.set_illumination_altitude_deg(layer.illumination_altitude_deg.evaluate_with_context(ctx));
runtime.set_exaggeration(layer.exaggeration.evaluate_with_context(ctx));
runtime
}
fn evaluate_raster_layer(
layer: &RasterStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
if let Some(result) =
try_dynamic_overlay_from_source(&layer.meta.name, &layer.source, sources, ctx)
{
return result.map(|mut runtime| {
apply_shared_meta(runtime.as_mut(), &layer.meta, ctx);
runtime
});
}
let (factory, cache_capacity, selection) =
require_raster_source(&layer.meta.id, &layer.source, sources)?;
let mut runtime = TileLayer::new_with_selection_config(
layer.meta.name.clone(),
(factory)(),
cache_capacity,
selection.clone(),
);
apply_shared_meta(&mut runtime, &layer.meta, ctx);
Ok(Box::new(runtime))
}
fn evaluate_vector_runtime_layer(
meta: &StyleLayerMeta,
source_id: &str,
source_layer: Option<&str>,
layer_id: &str,
style: VectorStyle,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
let features = match sources.get(source_id) {
Some(StyleSource::VectorTile(source)) if source.is_streamed() => {
FeatureCollection::default()
}
_ => require_vector_source(layer_id, source_id, source_layer, sources, ctx.zoom as u8)?
.into_owned(),
};
let mut runtime = VectorLayer::new(meta.name.clone(), features, style)
.with_query_metadata(Some(layer_id.to_owned()), Some(source_id.to_owned()))
.with_source_layer(source_layer.map(str::to_owned));
apply_shared_meta(&mut runtime, meta, ctx);
Ok(Box::new(runtime))
}
fn evaluate_vector_layer(
layer: &VectorStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
evaluate_vector_runtime_layer(
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_vector_layer(layer, ctx),
sources,
ctx,
)
}
fn evaluate_fill_layer(
layer: &FillStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
evaluate_vector_runtime_layer(
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_fill_layer(layer, ctx),
sources,
ctx,
)
}
fn evaluate_line_layer(
layer: &LineStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
evaluate_vector_runtime_layer(
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_line_layer(layer, ctx),
sources,
ctx,
)
}
fn evaluate_circle_layer(
layer: &CircleStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
evaluate_vector_runtime_layer(
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_circle_layer(layer, ctx),
sources,
ctx,
)
}
fn evaluate_heatmap_layer(
layer: &HeatmapStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
evaluate_vector_runtime_layer(
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_heatmap_layer(layer, ctx),
sources,
ctx,
)
}
fn evaluate_fill_extrusion_layer(
layer: &FillExtrusionStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
evaluate_vector_runtime_layer(
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_fill_extrusion_layer(layer, ctx),
sources,
ctx,
)
}
fn evaluate_symbol_layer(
layer: &SymbolStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
evaluate_vector_runtime_layer(
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_symbol_layer(layer, ctx),
sources,
ctx,
)
}
fn evaluate_model_layer(
layer: &ModelStyleLayer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<Box<dyn Layer>, StyleError> {
let model = require_model_source(&layer.meta.id, &layer.source, sources)?;
let mut runtime = ModelLayer::new(layer.meta.name.clone())
.with_query_metadata(Some(layer.meta.id.clone()), Some(layer.source.clone()));
apply_shared_meta(&mut runtime, &layer.meta, ctx);
runtime.instances.extend(model.instances.iter().cloned());
Ok(Box::new(runtime))
}
fn apply_background_to_runtime(
layer: &BackgroundStyleLayer,
runtime: &mut dyn Layer,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
let background = runtime
.as_any_mut()
.downcast_mut::<BackgroundLayer>()
.expect("style/runtime layer mismatch: expected BackgroundLayer");
apply_shared_meta(background, &layer.meta, ctx);
background.set_color(layer.color.evaluate_with_context(ctx));
Ok(())
}
fn apply_hillshade_to_runtime(
layer: &HillshadeStyleLayer,
runtime: &mut dyn Layer,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
let hillshade = runtime
.as_any_mut()
.downcast_mut::<HillshadeLayer>()
.expect("style/runtime layer mismatch: expected HillshadeLayer");
apply_shared_meta(hillshade, &layer.meta, ctx);
hillshade.set_highlight_color(layer.highlight_color.evaluate_with_context(ctx));
hillshade.set_shadow_color(layer.shadow_color.evaluate_with_context(ctx));
hillshade.set_accent_color(layer.accent_color.evaluate_with_context(ctx));
hillshade.set_illumination_direction_deg(
layer.illumination_direction_deg.evaluate_with_context(ctx),
);
hillshade
.set_illumination_altitude_deg(layer.illumination_altitude_deg.evaluate_with_context(ctx));
hillshade.set_exaggeration(layer.exaggeration.evaluate_with_context(ctx));
Ok(())
}
fn apply_raster_to_runtime(
layer: &RasterStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
let _ = require_raster_source(&layer.meta.id, &layer.source, sources)?;
let tile = runtime
.as_any_mut()
.downcast_mut::<TileLayer>()
.expect("style/runtime layer mismatch: expected TileLayer");
apply_shared_meta(tile, &layer.meta, ctx);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn apply_vector_style_to_runtime(
runtime: &mut dyn Layer,
meta: &StyleLayerMeta,
source_id: &str,
source_layer: Option<&str>,
layer_id: &str,
style: VectorStyle,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
let vector = require_vector_source(layer_id, source_id, source_layer, sources, ctx.zoom as u8)?;
let layer = runtime
.as_any_mut()
.downcast_mut::<VectorLayer>()
.expect("style/runtime layer mismatch: expected VectorLayer");
apply_shared_meta(layer, meta, ctx);
layer.style = style;
layer.set_query_metadata(Some(layer_id.to_owned()), Some(source_id.to_owned()));
if layer.features.len() != vector.len() {
layer.features = vector.into_owned();
}
Ok(())
}
fn apply_vector_to_runtime(
layer: &VectorStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
apply_vector_style_to_runtime(
runtime,
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_vector_layer(layer, ctx),
sources,
ctx,
)
}
fn apply_fill_to_runtime(
layer: &FillStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
apply_vector_style_to_runtime(
runtime,
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_fill_layer(layer, ctx),
sources,
ctx,
)
}
fn apply_line_to_runtime(
layer: &LineStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
apply_vector_style_to_runtime(
runtime,
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_line_layer(layer, ctx),
sources,
ctx,
)
}
fn apply_circle_to_runtime(
layer: &CircleStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
apply_vector_style_to_runtime(
runtime,
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_circle_layer(layer, ctx),
sources,
ctx,
)
}
fn apply_heatmap_to_runtime(
layer: &HeatmapStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
apply_vector_style_to_runtime(
runtime,
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_heatmap_layer(layer, ctx),
sources,
ctx,
)
}
fn apply_fill_extrusion_to_runtime(
layer: &FillExtrusionStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
apply_vector_style_to_runtime(
runtime,
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_fill_extrusion_layer(layer, ctx),
sources,
ctx,
)
}
fn apply_symbol_to_runtime(
layer: &SymbolStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
apply_vector_style_to_runtime(
runtime,
&layer.meta,
&layer.source,
layer.source_layer.as_deref(),
&layer.meta.id,
vector_style_from_symbol_layer(layer, ctx),
sources,
ctx,
)
}
fn apply_model_to_runtime(
layer: &ModelStyleLayer,
runtime: &mut dyn Layer,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Result<(), StyleError> {
let model = require_model_source(&layer.meta.id, &layer.source, sources)?;
let runtime = runtime
.as_any_mut()
.downcast_mut::<ModelLayer>()
.expect("style/runtime layer mismatch: expected ModelLayer");
apply_shared_meta(runtime, &layer.meta, ctx);
runtime.set_query_metadata(Some(layer.meta.id.clone()), Some(layer.source.clone()));
runtime.instances.clear();
runtime.instances.extend(model.instances.iter().cloned());
Ok(())
}
fn vector_style_from_vector_layer(layer: &VectorStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
VectorStyle {
render_mode: VectorRenderMode::Generic,
fill_color: layer.fill_color.evaluate_with_context(ctx),
stroke_color: layer.stroke_color.evaluate_with_context(ctx),
stroke_width: layer.stroke_width.evaluate_with_context(ctx),
..VectorStyle::default()
}
}
fn vector_style_from_fill_layer(layer: &FillStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
let mut style = VectorStyle::fill(
layer.fill_color.evaluate_with_context(ctx),
layer.outline_color.evaluate_with_context(ctx),
layer.outline_width.evaluate_with_context(ctx),
);
style.fill_pattern = layer.fill_pattern.clone();
style
}
fn vector_style_from_line_layer(layer: &LineStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
let mut style = VectorStyle::line_styled(
layer.color.evaluate_with_context(ctx),
layer.width.evaluate_with_context(ctx),
layer.line_cap,
layer.line_join,
layer.miter_limit,
layer.dash_array.clone(),
);
if layer.width.is_data_driven() {
style.width_expr = Some(layer.width.clone());
}
if layer.color.is_data_driven() {
style.stroke_color_expr = Some(layer.color.clone());
}
style.eval_zoom = ctx.zoom;
style.line_gradient = layer.line_gradient.clone();
style.line_pattern = layer.line_pattern.clone();
style
}
fn vector_style_from_circle_layer(layer: &CircleStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
VectorStyle::circle(
layer.color.evaluate_with_context(ctx),
layer.radius.evaluate_with_context(ctx),
layer.stroke_color.evaluate_with_context(ctx),
layer.stroke_width.evaluate_with_context(ctx),
)
}
fn vector_style_from_heatmap_layer(
layer: &HeatmapStyleLayer,
ctx: StyleEvalContext,
) -> VectorStyle {
VectorStyle::heatmap(
layer.color.evaluate_with_context(ctx),
layer.radius.evaluate_with_context(ctx),
layer.intensity.evaluate_with_context(ctx),
)
}
fn vector_style_from_fill_extrusion_layer(
layer: &FillExtrusionStyleLayer,
ctx: StyleEvalContext,
) -> VectorStyle {
VectorStyle::fill_extrusion(
layer.color.evaluate_with_context(ctx),
layer.base.evaluate_with_context(ctx),
layer.height.evaluate_with_context(ctx),
)
}
fn vector_style_from_symbol_layer(layer: &SymbolStyleLayer, ctx: StyleEvalContext) -> VectorStyle {
let mut style = VectorStyle::symbol(
layer.color.evaluate_with_context(ctx),
layer.halo_color.evaluate_with_context(ctx),
layer.size.evaluate_with_context(ctx),
);
style.symbol_text_field = layer
.text_field
.as_ref()
.map(|value| value.evaluate_with_context(ctx));
style.symbol_icon_image = layer
.icon_image
.as_ref()
.map(|value| value.evaluate_with_context(ctx));
style.symbol_font_stack = layer.font_stack.evaluate_with_context(ctx);
style.symbol_padding = layer.padding.evaluate_with_context(ctx);
let shared_overlap = layer.allow_overlap.evaluate_with_context(ctx);
style.symbol_allow_overlap = shared_overlap;
style.symbol_text_allow_overlap = layer
.text_allow_overlap
.as_ref()
.map(|value| value.evaluate_with_context(ctx))
.unwrap_or(shared_overlap);
style.symbol_icon_allow_overlap = layer
.icon_allow_overlap
.as_ref()
.map(|value| value.evaluate_with_context(ctx))
.unwrap_or(shared_overlap);
style.symbol_text_optional = layer
.text_optional
.as_ref()
.map(|value| value.evaluate_with_context(ctx))
.unwrap_or(false);
style.symbol_icon_optional = layer
.icon_optional
.as_ref()
.map(|value| value.evaluate_with_context(ctx))
.unwrap_or(false);
style.symbol_text_ignore_placement = layer
.text_ignore_placement
.as_ref()
.map(|value| value.evaluate_with_context(ctx))
.unwrap_or(false);
style.symbol_icon_ignore_placement = layer
.icon_ignore_placement
.as_ref()
.map(|value| value.evaluate_with_context(ctx))
.unwrap_or(false);
style.symbol_text_radial_offset = layer
.radial_offset
.as_ref()
.map(|value| value.evaluate_with_context(ctx));
style.symbol_variable_anchor_offsets = layer.variable_anchor_offsets.clone();
style.symbol_text_anchor = layer.anchor;
style.symbol_text_justify =
effective_symbol_text_justify(layer.justify.evaluate_with_context(ctx), layer.anchor);
style.symbol_text_transform = layer.transform.evaluate_with_context(ctx);
style.symbol_text_max_width = layer
.max_width
.as_ref()
.map(|value| value.evaluate_with_context(ctx));
style.symbol_text_line_height = layer
.line_height
.as_ref()
.map(|value| value.evaluate_with_context(ctx));
style.symbol_text_letter_spacing = layer
.letter_spacing
.as_ref()
.map(|value| value.evaluate_with_context(ctx));
style.symbol_icon_text_fit = layer.icon_text_fit.evaluate_with_context(ctx);
style.symbol_icon_text_fit_padding = layer.icon_text_fit_padding;
style.symbol_sort_key = layer
.sort_key
.as_ref()
.map(|value| value.evaluate_with_context(ctx));
style.symbol_placement = layer.placement;
style.symbol_spacing = layer.spacing.evaluate_with_context(ctx);
style.symbol_max_angle = layer.max_angle.evaluate_with_context(ctx);
style.symbol_keep_upright = layer.keep_upright.evaluate_with_context(ctx);
style.symbol_anchors = effective_symbol_anchor_order(
layer.anchor,
&layer.variable_anchors,
layer.variable_anchor_offsets.as_deref(),
);
style.symbol_writing_mode = layer.writing_mode;
style.symbol_offset = layer.offset;
style
}
fn effective_symbol_anchor_order(
anchor: SymbolAnchor,
variable_anchors: &[SymbolAnchor],
variable_anchor_offsets: Option<&[(SymbolAnchor, [f32; 2])]>,
) -> Vec<SymbolAnchor> {
if let Some(anchor_offsets) = variable_anchor_offsets {
return anchor_offsets.iter().map(|(anchor, _)| *anchor).collect();
}
if variable_anchors.is_empty() {
vec![anchor]
} else {
variable_anchors.to_vec()
}
}
pub fn fill_style_with_state(
layer: &FillStyleLayer,
ctx: &StyleEvalContextFull<'_>,
) -> VectorStyle {
let mut style = VectorStyle::fill(
layer.fill_color.evaluate_with_full_context(ctx),
layer.outline_color.evaluate_with_full_context(ctx),
layer.outline_width.evaluate_with_full_context(ctx),
);
style.fill_pattern = layer.fill_pattern.clone();
style
}
pub fn line_style_with_state(
layer: &LineStyleLayer,
ctx: &StyleEvalContextFull<'_>,
) -> VectorStyle {
let mut style = VectorStyle::line_styled(
layer.color.evaluate_with_full_context(ctx),
layer.width.evaluate_with_full_context(ctx),
layer.line_cap,
layer.line_join,
layer.miter_limit,
layer.dash_array.clone(),
);
style.line_gradient = layer.line_gradient.clone();
style.line_pattern = layer.line_pattern.clone();
style
}
pub fn circle_style_with_state(
layer: &CircleStyleLayer,
ctx: &StyleEvalContextFull<'_>,
) -> VectorStyle {
VectorStyle::circle(
layer.color.evaluate_with_full_context(ctx),
layer.radius.evaluate_with_full_context(ctx),
layer.stroke_color.evaluate_with_full_context(ctx),
layer.stroke_width.evaluate_with_full_context(ctx),
)
}
pub fn heatmap_style_with_state(
layer: &HeatmapStyleLayer,
ctx: &StyleEvalContextFull<'_>,
) -> VectorStyle {
VectorStyle::heatmap(
layer.color.evaluate_with_full_context(ctx),
layer.radius.evaluate_with_full_context(ctx),
layer.intensity.evaluate_with_full_context(ctx),
)
}
pub fn fill_extrusion_style_with_state(
layer: &FillExtrusionStyleLayer,
ctx: &StyleEvalContextFull<'_>,
) -> VectorStyle {
VectorStyle::fill_extrusion(
layer.color.evaluate_with_full_context(ctx),
layer.base.evaluate_with_full_context(ctx),
layer.height.evaluate_with_full_context(ctx),
)
}
pub fn vector_style_with_state(
layer: &VectorStyleLayer,
ctx: &StyleEvalContextFull<'_>,
) -> VectorStyle {
VectorStyle {
render_mode: VectorRenderMode::Generic,
fill_color: layer.fill_color.evaluate_with_full_context(ctx),
stroke_color: layer.stroke_color.evaluate_with_full_context(ctx),
stroke_width: layer.stroke_width.evaluate_with_full_context(ctx),
..VectorStyle::default()
}
}
pub fn symbol_style_with_state(
layer: &SymbolStyleLayer,
ctx: &StyleEvalContextFull<'_>,
) -> VectorStyle {
let base = ctx.to_base();
let mut style = VectorStyle::symbol(
layer.color.evaluate_with_full_context(ctx),
layer.halo_color.evaluate_with_full_context(ctx),
layer.size.evaluate_with_full_context(ctx),
);
style.symbol_text_field = layer
.text_field
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx));
style.symbol_icon_image = layer
.icon_image
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx));
style.symbol_font_stack = layer.font_stack.evaluate_with_full_context(ctx);
style.symbol_padding = layer.padding.evaluate_with_full_context(ctx);
let shared_overlap = layer.allow_overlap.evaluate_with_full_context(ctx);
style.symbol_allow_overlap = shared_overlap;
style.symbol_text_allow_overlap = layer
.text_allow_overlap
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx))
.unwrap_or(shared_overlap);
style.symbol_icon_allow_overlap = layer
.icon_allow_overlap
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx))
.unwrap_or(shared_overlap);
style.symbol_text_optional = layer
.text_optional
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx))
.unwrap_or(false);
style.symbol_icon_optional = layer
.icon_optional
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx))
.unwrap_or(false);
style.symbol_text_ignore_placement = layer
.text_ignore_placement
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx))
.unwrap_or(false);
style.symbol_icon_ignore_placement = layer
.icon_ignore_placement
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx))
.unwrap_or(false);
style.symbol_text_radial_offset = layer
.radial_offset
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx));
style.symbol_variable_anchor_offsets = layer.variable_anchor_offsets.clone();
style.symbol_text_anchor = layer.anchor;
style.symbol_text_justify =
effective_symbol_text_justify(layer.justify.evaluate_with_context(base), layer.anchor);
style.symbol_text_transform = layer.transform.evaluate_with_full_context(ctx);
style.symbol_text_max_width = layer
.max_width
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx));
style.symbol_text_line_height = layer
.line_height
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx));
style.symbol_text_letter_spacing = layer
.letter_spacing
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx));
style.symbol_icon_text_fit = layer.icon_text_fit.evaluate_with_full_context(ctx);
style.symbol_icon_text_fit_padding = layer.icon_text_fit_padding;
style.symbol_sort_key = layer
.sort_key
.as_ref()
.map(|v| v.evaluate_with_full_context(ctx));
style.symbol_placement = layer.placement;
style.symbol_spacing = layer.spacing.evaluate_with_full_context(ctx);
style.symbol_max_angle = layer.max_angle.evaluate_with_full_context(ctx);
style.symbol_keep_upright = layer.keep_upright.evaluate_with_full_context(ctx);
style.symbol_anchors = effective_symbol_anchor_order(
layer.anchor,
&layer.variable_anchors,
layer.variable_anchor_offsets.as_deref(),
);
style.symbol_writing_mode = layer.writing_mode;
style.symbol_offset = layer.offset;
style
}
fn effective_symbol_text_justify(
justify: SymbolTextJustify,
anchor: SymbolAnchor,
) -> SymbolTextJustify {
match justify {
SymbolTextJustify::Auto => anchor_justification(anchor),
explicit => explicit,
}
}
fn anchor_justification(anchor: SymbolAnchor) -> SymbolTextJustify {
match anchor {
SymbolAnchor::Left | SymbolAnchor::TopLeft | SymbolAnchor::BottomLeft => {
SymbolTextJustify::Left
}
SymbolAnchor::Right | SymbolAnchor::TopRight | SymbolAnchor::BottomRight => {
SymbolTextJustify::Right
}
_ => SymbolTextJustify::Center,
}
}
#[derive(Debug, Default)]
pub struct MapStyle {
document: StyleDocument,
}
impl MapStyle {
pub fn new() -> Self {
Self::default()
}
pub fn from_document(document: StyleDocument) -> Self {
Self { document }
}
pub fn document(&self) -> &StyleDocument {
&self.document
}
pub fn document_mut(&mut self) -> &mut StyleDocument {
&mut self.document
}
pub fn into_document(self) -> StyleDocument {
self.document
}
}
fn apply_shared_meta(layer: &mut dyn Layer, meta: &StyleLayerMeta, ctx: StyleEvalContext) {
layer.set_visible(meta.visible_in_context(ctx));
layer.set_opacity(meta.opacity.evaluate_with_context(ctx));
}
fn require_raster_source<'a>(
layer_id: &str,
source_id: &str,
sources: &'a HashMap<StyleSourceId, StyleSource>,
) -> Result<(&'a RasterSourceFactory, usize, &'a TileSelectionConfig), StyleError> {
let Some(source) = sources.get(source_id) else {
return Err(StyleError::MissingSource(source_id.to_owned()));
};
match source {
StyleSource::Raster(raster) => {
Ok((&raster.factory, raster.cache_capacity, &raster.selection))
}
StyleSource::Image(image) => Ok((&image.factory, image.cache_capacity, &image.selection)),
other => Err(StyleError::SourceKindMismatch {
layer_id: layer_id.to_owned(),
source_id: source_id.to_owned(),
expected: "raster|image",
actual: other.kind_name(),
}),
}
}
fn try_dynamic_overlay_from_source(
layer_name: &str,
source_id: &str,
sources: &HashMap<StyleSourceId, StyleSource>,
ctx: StyleEvalContext,
) -> Option<Result<Box<dyn Layer>, StyleError>> {
let source = sources.get(source_id)?;
match source {
StyleSource::Video(video) => {
let provider = (video.factory)();
let mut layer =
DynamicImageOverlayLayer::new(layer_name.to_owned(), video.coordinates, provider);
layer.set_opacity(ctx.zoom.fract()); Some(Ok(Box::new(layer)))
}
StyleSource::Canvas(canvas) => {
let provider = (canvas.factory)();
let mut layer =
DynamicImageOverlayLayer::new(layer_name.to_owned(), canvas.coordinates, provider);
layer.set_opacity(ctx.zoom.fract());
Some(Ok(Box::new(layer)))
}
_ => None,
}
}
fn require_vector_source<'a>(
layer_id: &str,
source_id: &str,
source_layer: Option<&str>,
sources: &'a HashMap<StyleSourceId, StyleSource>,
zoom: u8,
) -> Result<Cow<'a, FeatureCollection>, StyleError> {
let Some(source) = sources.get(source_id) else {
return Err(StyleError::MissingSource(source_id.to_owned()));
};
match source {
StyleSource::GeoJson(source) => Ok(source.features_at_zoom(zoom)),
StyleSource::VectorTile(source) => {
if let Some(source_layer) = source_layer {
source
.source_layer(source_layer)
.map(Cow::Borrowed)
.ok_or_else(|| StyleError::MissingSourceLayer {
layer_id: layer_id.to_owned(),
source_id: source_id.to_owned(),
source_layer: source_layer.to_owned(),
})
} else {
Ok(Cow::Borrowed(&source.data))
}
}
other => Err(StyleError::SourceKindMismatch {
layer_id: layer_id.to_owned(),
source_id: source_id.to_owned(),
expected: "geojson|vector",
actual: other.kind_name(),
}),
}
}
fn require_model_source<'a>(
layer_id: &str,
source_id: &str,
sources: &'a HashMap<StyleSourceId, StyleSource>,
) -> Result<&'a ModelSource, StyleError> {
let Some(source) = sources.get(source_id) else {
return Err(StyleError::MissingSource(source_id.to_owned()));
};
match source {
StyleSource::Model(model) => Ok(model),
other => Err(StyleError::SourceKindMismatch {
layer_id: layer_id.to_owned(),
source_id: source_id.to_owned(),
expected: "model",
actual: other.kind_name(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::{Feature, FeatureCollection, Geometry, Point};
use crate::tile_source::{TileError, TileResponse, TileSource};
use std::collections::HashMap;
struct EmptyTileSource;
impl TileSource for EmptyTileSource {
fn request(&self, _id: rustial_math::TileId) {}
fn poll(&self) -> Vec<(rustial_math::TileId, Result<TileResponse, TileError>)> {
Vec::new()
}
}
fn feature_at(lat: f64, lon: f64) -> Feature {
Feature {
geometry: Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(lat, lon),
}),
properties: HashMap::new(),
}
}
fn collection_with_point(lat: f64, lon: f64) -> FeatureCollection {
FeatureCollection {
features: vec![feature_at(lat, lon)],
}
}
#[test]
fn vector_tile_source_can_be_partitioned_by_source_layer() {
let mut source_layers = HashMap::new();
source_layers.insert("roads".to_string(), collection_with_point(1.0, 2.0));
source_layers.insert("water".to_string(), collection_with_point(3.0, 4.0));
let source = VectorTileSource::from_source_layers(source_layers);
assert!(source.has_source_layers());
assert_eq!(source.source_layer("roads").map(|fc| fc.len()), Some(1));
assert_eq!(source.source_layer("water").map(|fc| fc.len()), Some(1));
assert_eq!(source.data.len(), 2);
}
#[test]
fn vector_style_layer_resolves_requested_source_layer() {
let mut document = StyleDocument::new();
let mut source_layers = HashMap::new();
source_layers.insert("roads".to_string(), collection_with_point(10.0, 20.0));
source_layers.insert("water".to_string(), collection_with_point(30.0, 40.0));
document
.add_source(
"vector",
StyleSource::VectorTile(VectorTileSource::from_source_layers(source_layers)),
)
.expect("source added");
let mut layer = LineStyleLayer::new("roads-line", "vector");
layer.source_layer = Some("roads".to_string());
document
.add_layer(StyleLayer::Line(layer))
.expect("layer added");
let runtime = document.to_runtime_layers().expect("runtime layers");
let vector = runtime[0]
.as_any()
.downcast_ref::<VectorLayer>()
.expect("vector runtime layer");
assert_eq!(vector.features.len(), 1);
match &vector.features.features[0].geometry {
Geometry::Point(point) => {
assert!((point.coord.lat - 10.0).abs() < 1e-9);
assert!((point.coord.lon - 20.0).abs() < 1e-9);
}
other => panic!("expected point geometry, got {other:?}"),
}
}
#[test]
fn missing_source_layer_returns_style_error() {
let mut document = StyleDocument::new();
document
.add_source(
"vector",
StyleSource::VectorTile(VectorTileSource::new(collection_with_point(0.0, 0.0))),
)
.expect("source added");
let mut layer = FillStyleLayer::new("water-fill", "vector");
layer.source_layer = Some("water".to_string());
document
.add_layer(StyleLayer::Fill(layer))
.expect("layer added");
let err = document
.to_runtime_layers()
.expect_err("missing source-layer should fail");
assert!(matches!(err, StyleError::MissingSourceLayer { .. }));
}
#[test]
fn streamed_vector_source_allows_runtime_layer_creation_without_resolved_features() {
let mut document = StyleDocument::new();
document
.add_source(
"vector",
StyleSource::VectorTile(
VectorTileSource::streamed(|| Box::new(EmptyTileSource)).with_cache_capacity(8),
),
)
.expect("source added");
let mut layer = CircleStyleLayer::new("labels", "vector");
layer.source_layer = Some("poi".to_string());
document
.add_layer(StyleLayer::Circle(layer))
.expect("layer added");
let runtime = document.to_runtime_layers().expect("runtime layers");
let vector = runtime[0]
.as_any()
.downcast_ref::<VectorLayer>()
.expect("vector runtime layer");
assert!(vector.features.is_empty());
assert_eq!(vector.query_source_layer.as_deref(), Some("poi"));
}
#[test]
fn style_document_reports_source_usage() {
let mut document = StyleDocument::new();
document
.add_source(
"places",
StyleSource::GeoJson(GeoJsonSource::new(collection_with_point(0.0, 0.0))),
)
.expect("source added");
document
.add_source(
"labels",
StyleSource::VectorTile(VectorTileSource::new(collection_with_point(1.0, 1.0))),
)
.expect("source added");
document.set_terrain_source(Some("labels"));
document
.add_layer(StyleLayer::Fill(FillStyleLayer::new("fill", "places")))
.expect("fill layer added");
document
.add_layer(StyleLayer::Line(LineStyleLayer::new("line", "labels")))
.expect("line layer added");
assert!(document.source_is_used("places"));
assert!(document.source_is_used("labels"));
assert!(!document.source_is_used("missing"));
let layer_ids = document.layer_ids_using_source("labels");
assert_eq!(layer_ids, vec!["line"]);
}
#[test]
fn feature_state_value_returns_fallback_with_zoom_only_context() {
let value = StyleValue::<f32>::feature_state_key("opacity", 0.5);
let result = value.evaluate_with_context(StyleEvalContext::new(10.0));
assert!((result - 0.5).abs() < f32::EPSILON);
}
#[test]
fn feature_state_value_resolves_with_full_context() {
let mut state = HashMap::new();
state.insert("opacity".to_string(), PropertyValue::Number(0.8));
let value = StyleValue::<f32>::feature_state_key("opacity", 0.5);
let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
let result = value.evaluate_with_full_context(&ctx);
assert!((result - 0.8).abs() < f32::EPSILON);
}
#[test]
fn feature_state_value_falls_back_when_key_absent() {
let state: FeatureState = HashMap::new();
let value = StyleValue::<f32>::feature_state_key("opacity", 0.5);
let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
let result = value.evaluate_with_full_context(&ctx);
assert!((result - 0.5).abs() < f32::EPSILON);
}
#[test]
fn feature_state_bool_resolves_hover_flag() {
let mut state = HashMap::new();
state.insert("hover".to_string(), PropertyValue::Bool(true));
let value = StyleValue::<bool>::feature_state_key("hover", false);
let ctx = StyleEvalContext::new(14.0).with_feature_state(&state);
assert!(value.evaluate_with_full_context(&ctx));
}
#[test]
fn feature_state_color_array_always_returns_fallback() {
let mut state = HashMap::new();
state.insert("color".to_string(), PropertyValue::Number(1.0));
let fallback = [0.1, 0.2, 0.3, 1.0];
let value = StyleValue::<[f32; 4]>::feature_state_key("color", fallback);
let ctx = StyleEvalContext::new(14.0).with_feature_state(&state);
assert_eq!(value.evaluate_with_full_context(&ctx), fallback);
}
#[test]
fn is_feature_state_driven_flag() {
let constant: StyleValue<f32> = StyleValue::Constant(1.0);
assert!(!constant.is_feature_state_driven());
let driven: StyleValue<f32> = StyleValue::feature_state_key("opacity", 1.0);
assert!(driven.is_feature_state_driven());
}
#[test]
fn constant_and_zoom_stops_unchanged_with_full_context() {
let state: FeatureState = HashMap::new();
let ctx = StyleEvalContext::new(5.0).with_feature_state(&state);
let constant = StyleValue::Constant(42.0_f32);
assert!((constant.evaluate_with_full_context(&ctx) - 42.0).abs() < f32::EPSILON);
let stops = StyleValue::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
let result = stops.evaluate_with_full_context(&ctx);
assert!((result - 50.0).abs() < f32::EPSILON);
}
#[test]
fn full_context_helpers_return_expected_values() {
let mut state = HashMap::new();
state.insert("hover".to_string(), PropertyValue::Bool(true));
state.insert("width".to_string(), PropertyValue::Number(3.5));
let ctx = StyleEvalContextFull::new(14.0, &state);
assert!(ctx.feature_state_bool("hover"));
assert!(!ctx.feature_state_bool("missing"));
assert!((ctx.feature_state_f64("width", 1.0) - 3.5).abs() < f64::EPSILON);
assert!((ctx.feature_state_f64("missing", 1.0) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn fill_layer_resolves_with_feature_state() {
let mut layer = FillStyleLayer::new("buildings", "source");
layer.outline_width = StyleValue::feature_state_key("width", 1.0);
let empty_state: FeatureState = HashMap::new();
let ctx = StyleEvalContext::new(14.0).with_feature_state(&empty_state);
let style = fill_style_with_state(&layer, &ctx);
assert!((style.stroke_width - 1.0).abs() < f32::EPSILON);
let mut hover_state = HashMap::new();
hover_state.insert("width".to_string(), PropertyValue::Number(4.0));
let ctx = StyleEvalContext::new(14.0).with_feature_state(&hover_state);
let style = fill_style_with_state(&layer, &ctx);
assert!((style.stroke_width - 4.0).abs() < f32::EPSILON);
}
#[test]
fn line_layer_resolves_with_feature_state() {
let mut layer = LineStyleLayer::new("roads", "source");
layer.width = StyleValue::feature_state_key("highlight_width", 2.0);
let mut state = HashMap::new();
state.insert("highlight_width".to_string(), PropertyValue::Number(6.0));
let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
let style = line_style_with_state(&layer, &ctx);
assert!((style.stroke_width - 6.0).abs() < f32::EPSILON);
}
#[test]
fn circle_layer_resolves_with_feature_state() {
let mut layer = CircleStyleLayer::new("points", "source");
layer.radius = StyleValue::feature_state_key("size", 5.0);
let mut state = HashMap::new();
state.insert("size".to_string(), PropertyValue::Number(12.0));
let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
let style = circle_style_with_state(&layer, &ctx);
assert!((style.point_radius - 12.0).abs() < f32::EPSILON);
}
#[test]
fn has_feature_state_driven_paint_detects_driven_fields() {
let mut fill = FillStyleLayer::new("buildings", "source");
let fill_layer = StyleLayer::Fill(fill.clone());
assert!(!fill_layer.has_feature_state_driven_paint());
fill.outline_width = StyleValue::feature_state_key("width", 1.0);
let fill_layer = StyleLayer::Fill(fill);
assert!(fill_layer.has_feature_state_driven_paint());
}
#[test]
fn has_feature_state_driven_paint_false_for_non_vector_layers() {
let bg = BackgroundStyleLayer::new("bg", [0.0, 0.0, 0.0, 1.0]);
assert!(!StyleLayer::Background(bg).has_feature_state_driven_paint());
}
#[test]
fn resolve_style_with_feature_state_returns_none_for_background() {
let bg = BackgroundStyleLayer::new("bg", [0.0, 0.0, 0.0, 1.0]);
let state: FeatureState = HashMap::new();
let ctx = StyleEvalContext::new(10.0).with_feature_state(&state);
assert!(StyleLayer::Background(bg)
.resolve_style_with_feature_state(&ctx)
.is_none());
}
#[test]
fn resolve_style_with_feature_state_dispatches_fill() {
let mut fill = FillStyleLayer::new("buildings", "source");
fill.outline_width = StyleValue::feature_state_key("width", 1.0);
let mut state = HashMap::new();
state.insert("width".to_string(), PropertyValue::Number(5.0));
let ctx = StyleEvalContext::new(14.0).with_feature_state(&state);
let style = StyleLayer::Fill(fill)
.resolve_style_with_feature_state(&ctx)
.expect("fill layer should produce VectorStyle");
assert!((style.stroke_width - 5.0).abs() < f32::EPSILON);
}
#[test]
fn non_driven_fields_unchanged_through_full_context() {
let layer = FillStyleLayer::new("buildings", "source");
let state: FeatureState = HashMap::new();
let full_ctx = StyleEvalContext::new(14.0).with_feature_state(&state);
let zoom_ctx = StyleEvalContext::new(14.0);
let via_full = fill_style_with_state(&layer, &full_ctx);
let via_zoom = vector_style_from_fill_layer(&layer, zoom_ctx);
assert_eq!(via_full.fill_color, via_zoom.fill_color);
assert_eq!(via_full.stroke_color, via_zoom.stroke_color);
assert!((via_full.stroke_width - via_zoom.stroke_width).abs() < f32::EPSILON);
}
#[test]
fn geojson_source_with_clustering_returns_clustered_features_at_low_zoom() {
let features = FeatureCollection {
features: (0..10)
.map(|i| feature_at(48.858 + i as f64 * 0.0001, 2.294))
.collect(),
};
let source = GeoJsonSource::new(features).with_clustering(ClusterOptions {
radius: 80.0,
max_zoom: 16,
min_points: 2,
..Default::default()
});
assert!(source.is_clustered());
let clustered = source.features_at_zoom(2);
assert!(
clustered.len() < 10,
"Expected fewer features at zoom 2 (got {})",
clustered.len(),
);
let unclustered = source.features_at_zoom(20);
assert_eq!(unclustered.len(), 10);
}
#[test]
fn geojson_source_without_clustering_returns_raw_data() {
let source = GeoJsonSource::new(FeatureCollection {
features: vec![feature_at(51.5, -0.12), feature_at(51.51, -0.13)],
});
assert!(!source.is_clustered());
let result = source.features_at_zoom(5);
assert_eq!(result.len(), 2);
}
#[test]
fn clustered_geojson_circle_layer_resolves_to_runtime_layer() {
let features = FeatureCollection {
features: (0..20)
.map(|i| feature_at(48.858 + i as f64 * 0.0001, 2.294))
.collect(),
};
let source = GeoJsonSource::new(features).with_clustering(Default::default());
let mut doc = StyleDocument::new();
doc.add_source("points", StyleSource::GeoJson(source))
.expect("source added");
doc.add_layer(StyleLayer::Circle(CircleStyleLayer::new("dots", "points")))
.expect("layer added");
let ctx = StyleEvalContext::new(3.0);
let layers = doc.to_runtime_layers_with_context(ctx).expect("layers ok");
assert_eq!(layers.len(), 1, "expected 1 circle layer");
}
#[test]
fn video_source_produces_dynamic_image_overlay_layer() {
use crate::layers::{FrameData, FrameProvider};
struct TestProvider;
impl FrameProvider for TestProvider {
fn next_frame(&mut self) -> Option<FrameData> {
Some(FrameData {
width: 4,
height: 4,
data: vec![255; 64],
})
}
}
let corners = [
GeoCoord::from_lat_lon(40.0, -74.0),
GeoCoord::from_lat_lon(40.0, -73.0),
GeoCoord::from_lat_lon(39.0, -73.0),
GeoCoord::from_lat_lon(39.0, -74.0),
];
let source = VideoSource::new(corners, || Box::new(TestProvider));
let mut doc = StyleDocument::new();
doc.add_source("video", StyleSource::Video(source))
.expect("source added");
doc.add_layer(StyleLayer::Raster(RasterStyleLayer::new(
"video-layer",
"video",
)))
.expect("layer added");
let layers = doc.to_runtime_layers().expect("runtime layers");
assert_eq!(layers.len(), 1);
assert!(
layers[0]
.as_any()
.downcast_ref::<DynamicImageOverlayLayer>()
.is_some(),
"video source should produce a DynamicImageOverlayLayer"
);
}
#[test]
fn canvas_source_produces_dynamic_image_overlay_layer() {
use crate::layers::{FrameData, FrameProvider};
struct StaticCanvas;
impl FrameProvider for StaticCanvas {
fn next_frame(&mut self) -> Option<FrameData> {
Some(FrameData {
width: 8,
height: 8,
data: vec![128; 256],
})
}
fn is_animating(&self) -> bool {
false
}
}
let corners = [
GeoCoord::from_lat_lon(51.0, -1.0),
GeoCoord::from_lat_lon(51.0, 0.0),
GeoCoord::from_lat_lon(50.0, 0.0),
GeoCoord::from_lat_lon(50.0, -1.0),
];
let source = CanvasSource::new(corners, || Box::new(StaticCanvas)).with_animate(false);
let mut doc = StyleDocument::new();
doc.add_source("canvas", StyleSource::Canvas(source))
.expect("source added");
doc.add_layer(StyleLayer::Raster(RasterStyleLayer::new(
"canvas-layer",
"canvas",
)))
.expect("layer added");
let layers = doc.to_runtime_layers().expect("runtime layers");
assert_eq!(layers.len(), 1);
let dyn_layer = layers[0]
.as_any()
.downcast_ref::<DynamicImageOverlayLayer>()
.expect("canvas source should produce a DynamicImageOverlayLayer");
assert!(!dyn_layer.provider().is_animating());
}
#[test]
fn compute_lighting_defaults() {
let config = LightConfig::default();
let lit = compute_lighting(&config);
assert!((lit.lighting_enabled - 1.0).abs() < f32::EPSILON);
assert!((lit.ambient_color[0] - 0.5).abs() < 0.01);
assert!((lit.ambient_color[1] - 0.5).abs() < 0.01);
assert!((lit.ambient_color[2] - 0.5).abs() < 0.01);
assert!((lit.directional_color[0] - 0.5).abs() < 0.01);
assert!((lit.directional_color[1] - 0.5).abs() < 0.01);
assert!((lit.directional_color[2] - 0.5).abs() < 0.01);
let len = (lit.directional_dir[0].powi(2)
+ lit.directional_dir[1].powi(2)
+ lit.directional_dir[2].powi(2))
.sqrt();
assert!((len - 1.0).abs() < 0.01);
}
#[test]
fn compute_lighting_flat_mode() {
let config = LightConfig {
mode: LightingMode::Flat,
..Default::default()
};
let lit = compute_lighting(&config);
assert!((lit.lighting_enabled - 0.0).abs() < f32::EPSILON);
}
#[test]
fn compute_lighting_custom_ambient() {
let config = LightConfig {
ambient: AmbientLight {
color: [1.0, 0.0, 0.0],
intensity: 0.8,
},
..Default::default()
};
let lit = compute_lighting(&config);
assert!((lit.ambient_color[0] - 0.8).abs() < 0.01);
assert!((lit.ambient_color[1] - 0.0).abs() < 0.01);
assert!((lit.ambient_color[2] - 0.0).abs() < 0.01);
}
#[test]
fn compute_lighting_direction_north_overhead() {
let config = LightConfig {
directional: DirectionalLight {
direction: [0.0, 90.0], ..Default::default()
},
..Default::default()
};
let lit = compute_lighting(&config);
assert!((lit.directional_dir[2] - 1.0).abs() < 0.01);
assert!(lit.directional_dir[0].abs() < 0.01);
assert!(lit.directional_dir[1].abs() < 0.01);
}
#[test]
fn compute_sky_defaults_disabled() {
let sky = ComputedSky::default();
assert!((sky.sky_enabled - 0.0).abs() < f32::EPSILON);
}
#[test]
fn compute_sky_enabled_with_config() {
let config = SkyConfig::default();
let fallback = [210.0, 45.0];
let sky = compute_sky(&config, fallback);
assert!((sky.sky_enabled - 1.0).abs() < f32::EPSILON);
assert!((sky.sun_intensity - 10.0).abs() < 0.01);
}
#[test]
fn compute_sky_inherits_sun_from_fallback() {
let config = SkyConfig {
sun_position: None,
..Default::default()
};
let fallback = [0.0, 90.0]; let sky = compute_sky(&config, fallback);
assert!((sky.sun_direction[2] - 1.0).abs() < 0.01);
assert!(sky.sun_direction[0].abs() < 0.01);
assert!(sky.sun_direction[1].abs() < 0.01);
}
#[test]
fn compute_sky_explicit_sun_position() {
let config = SkyConfig {
sun_position: Some([90.0, 45.0]), ..Default::default()
};
let sky = compute_sky(&config, [0.0, 0.0]);
assert!(sky.sun_direction[0] > 0.5);
assert!((sky.sun_direction[2] - 0.707).abs() < 0.02);
}
#[test]
fn compute_sky_zero_opacity_disables() {
let config = SkyConfig {
opacity: 0.0,
..Default::default()
};
let sky = compute_sky(&config, [210.0, 45.0]);
assert!((sky.sky_enabled - 0.0).abs() < f32::EPSILON);
}
#[test]
fn transition_spec_default_is_300ms() {
let spec = TransitionSpec::default();
assert!((spec.duration - 0.3).abs() < 1e-6);
assert!((spec.delay - 0.0).abs() < 1e-6);
assert!(spec.is_active());
}
#[test]
fn transition_spec_instant_is_zero() {
let spec = TransitionSpec::INSTANT;
assert!((spec.duration - 0.0).abs() < 1e-6);
assert!(!spec.is_active());
}
#[test]
fn transitioning_settled_resolves_immediately() {
let t = Transitioning::settled(0.5_f32);
assert!((t.resolve(0.0) - 0.5).abs() < 1e-6);
assert!((t.resolve(100.0) - 0.5).abs() < 1e-6);
assert!(!t.is_active(0.0));
}
#[test]
fn transitioning_interpolates_over_duration() {
let spec = TransitionSpec {
duration: 1.0,
delay: 0.0,
};
let t = Transitioning::new(0.0_f32, 1.0, 0.0, &spec);
assert!((t.resolve(0.0) - 0.0).abs() < 1e-6);
let mid = t.resolve(0.5);
assert!((mid - 0.5).abs() < 0.05);
assert!((t.resolve(1.0) - 1.0).abs() < 1e-6);
assert!((t.resolve(2.0) - 1.0).abs() < 1e-6);
}
#[test]
fn transitioning_respects_delay() {
let spec = TransitionSpec {
duration: 1.0,
delay: 0.5,
};
let t = Transitioning::new(0.0_f32, 1.0, 0.0, &spec);
assert!((t.resolve(0.0) - 0.0).abs() < 1e-6);
assert!((t.resolve(0.25) - 0.0).abs() < 1e-6);
assert!((t.resolve(0.5) - 0.0).abs() < 1e-6);
assert!(t.resolve(1.0) > 0.1);
assert!((t.resolve(1.5) - 1.0).abs() < 1e-6);
}
#[test]
fn transitioning_retarget_starts_from_current() {
let spec = TransitionSpec {
duration: 1.0,
delay: 0.0,
};
let mut t = Transitioning::new(0.0_f32, 1.0, 0.0, &spec);
let mid = t.resolve(0.5);
assert!(mid > 0.3 && mid < 0.7);
t.retarget(2.0, 0.5, &spec);
let after_retarget = t.resolve(0.5);
assert!((after_retarget - mid).abs() < 0.05);
assert!((t.resolve(1.5) - 2.0).abs() < 1e-6);
}
#[test]
fn transitioning_color_interpolation() {
let spec = TransitionSpec {
duration: 1.0,
delay: 0.0,
};
let red: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
let blue: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
let t = Transitioning::new(red, blue, 0.0, &spec);
let mid = t.resolve(0.5);
assert!(mid[0] > 0.3 && mid[0] < 0.7);
assert!(mid[2] > 0.3 && mid[2] < 0.7);
let end = t.resolve(1.0);
assert!((end[0] - 0.0).abs() < 1e-6);
assert!((end[2] - 1.0).abs() < 1e-6);
}
#[test]
fn layer_transition_state_detects_changes() {
let spec = TransitionSpec {
duration: 0.5,
delay: 0.0,
};
let mut state = LayerTransitionState::from_initial(
spec,
1.0,
[1.0, 0.0, 0.0, 1.0],
[0.0; 4],
2.0,
0.0,
0.0,
);
assert!(!state.has_active_transitions(0.0));
state.update(1.0, 0.5, [1.0, 0.0, 0.0, 1.0], [0.0; 4], 2.0, 0.0, 0.0);
assert!(state.has_active_transitions(1.0));
let resolved = state.resolve(1.25);
assert!(resolved.opacity > 0.5 && resolved.opacity < 1.0);
let resolved = state.resolve(1.5);
assert!((resolved.opacity - 0.5).abs() < 1e-6);
}
#[test]
fn ease_cubic_in_out_boundary_values() {
assert!((super::ease_cubic_in_out(0.0) - 0.0).abs() < 1e-6);
assert!((super::ease_cubic_in_out(0.5) - 0.5).abs() < 1e-6);
assert!((super::ease_cubic_in_out(1.0) - 1.0).abs() < 1e-6);
let v1 = super::ease_cubic_in_out(0.25);
let v2 = super::ease_cubic_in_out(0.5);
let v3 = super::ease_cubic_in_out(0.75);
assert!(v1 < v2);
assert!(v2 < v3);
}
#[test]
fn style_document_global_transition() {
let mut doc = StyleDocument::new();
assert!(doc.transition().is_active());
assert!((doc.transition().duration - 0.3).abs() < 1e-6);
doc.set_transition(TransitionSpec {
duration: 0.5,
delay: 0.1,
});
assert!((doc.transition().duration - 0.5).abs() < 1e-6);
assert!((doc.transition().delay - 0.1).abs() < 1e-6);
}
#[test]
fn style_layer_meta_has_transition() {
let meta = StyleLayerMeta::new("test");
assert!((meta.transition.duration - 0.3).abs() < 1e-6);
}
#[test]
fn shadow_config_defaults() {
let cfg = ShadowConfig::default();
assert_eq!(cfg.cascade_count, 2);
assert_eq!(cfg.map_resolution, 2048);
assert!((cfg.intensity - 0.8).abs() < 1e-6);
assert!((cfg.normal_offset - 3.0).abs() < 1e-6);
}
#[test]
fn computed_shadow_default_is_disabled() {
let s = ComputedShadow::default();
assert!(!s.enabled);
assert_eq!(s.cascade_count, 2);
}
#[test]
fn shadow_cascade_identity_vp_produces_valid_output() {
let vp = glam::DMat4::IDENTITY;
let light_dir = [0.0_f32, -0.707, 0.707];
let config = ShadowConfig::default();
let shadow = compute_shadow_cascades(&vp, light_dir, 500.0, &config);
assert!(shadow.enabled);
assert_eq!(shadow.cascade_count, 2);
assert_eq!(shadow.map_resolution, 2048);
let has_nonzero = shadow.light_matrices[0]
.iter()
.flat_map(|r| r.iter())
.any(|v| v.abs() > 1e-10);
assert!(has_nonzero, "cascade 0 matrix should not be all-zero");
}
#[test]
fn shadow_cascade_count_clamped_to_four() {
let vp = glam::DMat4::IDENTITY;
let light_dir = [0.0_f32, -0.707, 0.707];
let config = ShadowConfig {
cascade_count: 10,
..Default::default()
};
let shadow = compute_shadow_cascades(&vp, light_dir, 500.0, &config);
assert!(shadow.cascade_count <= 4);
}
#[test]
fn shadow_disabled_when_light_dir_zero() {
let vp = glam::DMat4::IDENTITY;
let light_dir = [0.0_f32, 0.0, 0.0];
let config = ShadowConfig::default();
let shadow = compute_shadow_cascades(&vp, light_dir, 500.0, &config);
assert!(!shadow.enabled);
}
#[test]
fn shadows_enabled_flag_from_lighting() {
let config = LightConfig::default();
let lit = compute_lighting(&config);
assert!(!lit.shadows_enabled);
let with_shadows = LightConfig {
directional: DirectionalLight {
cast_shadows: true,
..Default::default()
},
..Default::default()
};
let lit_on = compute_lighting(&with_shadows);
assert!(lit_on.shadows_enabled);
let flat = LightConfig {
mode: LightingMode::Flat,
directional: DirectionalLight {
cast_shadows: true,
..Default::default()
},
..Default::default()
};
let lit_flat = compute_lighting(&flat);
assert!(!lit_flat.shadows_enabled);
}
#[test]
fn shadow_texel_size_matches_resolution() {
let vp = glam::DMat4::IDENTITY;
let light_dir = [0.0_f32, -0.707, 0.707];
let config = ShadowConfig {
map_resolution: 1024,
..Default::default()
};
let shadow = compute_shadow_cascades(&vp, light_dir, 500.0, &config);
assert!((shadow.texel_size - 1.0 / 1024.0).abs() < 1e-6);
}
}