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, PartialEq)]
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>,
}
impl Default for FogConfig {
fn default() -> Self {
Self {
color: None,
range: None,
density: None,
horizon_color: None,
horizon_blend: None,
}
}
}
#[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 }
}
}
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>,
}
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>,
}
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,
}
}
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)]
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 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(())
}
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
}
}
#[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());
}
}
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() as f32); 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() as f32);
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(),
}),
}
}