use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StyleError {
#[error("invalid style version {0}; must be 8")]
InvalidVersion(u8),
#[error("unknown layer type: {0}")]
UnknownLayerType(String),
#[error("color parse error: {0}")]
ColorParseError(String),
#[error("invalid filter: {0}")]
InvalidFilter(String),
#[error("serde error: {0}")]
SerdeError(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StyleSpec {
pub version: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub center: Option<[f64; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub zoom: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bearing: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pitch: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub light: Option<Light>,
pub sources: HashMap<String, Source>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sprite: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub glyphs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transition: Option<Transition>,
pub layers: Vec<Layer>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum Source {
Vector {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tiles: Option<Vec<String>>,
#[serde(rename = "minzoom", skip_serializing_if = "Option::is_none")]
min_zoom: Option<u8>,
#[serde(rename = "maxzoom", skip_serializing_if = "Option::is_none")]
max_zoom: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
attribution: Option<String>,
},
Raster {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tiles: Option<Vec<String>>,
#[serde(rename = "tileSize", skip_serializing_if = "Option::is_none")]
tile_size: Option<u32>,
#[serde(rename = "minzoom", skip_serializing_if = "Option::is_none")]
min_zoom: Option<u8>,
#[serde(rename = "maxzoom", skip_serializing_if = "Option::is_none")]
max_zoom: Option<u8>,
},
#[serde(rename = "raster-dem")]
RasterDem {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(default)]
encoding: DemEncoding,
},
#[serde(rename = "geojson")]
GeoJson {
data: serde_json::Value,
#[serde(rename = "maxzoom", skip_serializing_if = "Option::is_none")]
max_zoom: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
cluster: Option<bool>,
#[serde(rename = "clusterRadius", skip_serializing_if = "Option::is_none")]
cluster_radius: Option<u32>,
},
Image {
url: String,
coordinates: [[f64; 2]; 4],
},
Video {
urls: Vec<String>,
coordinates: [[f64; 2]; 4],
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DemEncoding {
#[default]
Mapbox,
Terrarium,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Layer {
pub id: String,
#[serde(rename = "type")]
pub layer_type: LayerType,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(rename = "source-layer", skip_serializing_if = "Option::is_none")]
pub source_layer: Option<String>,
#[serde(rename = "minzoom", skip_serializing_if = "Option::is_none")]
pub min_zoom: Option<f64>,
#[serde(rename = "maxzoom", skip_serializing_if = "Option::is_none")]
pub max_zoom: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<Filter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub layout: Option<Layout>,
#[serde(skip_serializing_if = "Option::is_none")]
pub paint: Option<Paint>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum LayerType {
Background,
Fill,
Line,
Symbol,
Raster,
Circle,
FillExtrusion,
Heatmap,
Hillshade,
Sky,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum GeomFilter {
Point,
LineString,
Polygon,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Filter {
All(#[serde(skip)] Vec<Filter>),
Any(#[serde(skip)] Vec<Filter>),
None(#[serde(skip)] Vec<Filter>),
Eq {
property: String,
value: serde_json::Value,
},
Ne {
property: String,
value: serde_json::Value,
},
Lt {
property: String,
value: f64,
},
Lte {
property: String,
value: f64,
},
Gt {
property: String,
value: f64,
},
Gte {
property: String,
value: f64,
},
In {
property: String,
values: Vec<serde_json::Value>,
},
Has(String),
NotHas(String),
GeometryType(GeomFilter),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Interpolation {
Linear,
Exponential(f64),
CubicBezier([f64; 4]),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Expression {
Get(String),
Has(String),
Literal(serde_json::Value),
Array(Vec<Expression>),
Case {
conditions: Vec<(Expression, Expression)>,
fallback: Box<Expression>,
},
Match {
input: Box<Expression>,
cases: Vec<(serde_json::Value, Expression)>,
fallback: Box<Expression>,
},
Interpolate {
interpolation: Interpolation,
input: Box<Expression>,
stops: Vec<(f64, Expression)>,
},
Step {
input: Box<Expression>,
default: Box<Expression>,
stops: Vec<(f64, Expression)>,
},
Zoom,
Add(Box<Expression>, Box<Expression>),
Subtract(Box<Expression>, Box<Expression>),
Multiply(Box<Expression>, Box<Expression>),
Divide(Box<Expression>, Box<Expression>),
Coalesce(Vec<Expression>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FieldValue<T: Clone> {
Literal(T),
Expression(Expression),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: f32,
}
impl Color {
pub fn parse(s: &str) -> Result<Self, StyleError> {
let s = s.trim();
if let Some(hex) = s.strip_prefix('#') {
Self::parse_hex(hex)
} else if let Some(inner) = s.strip_prefix("rgba(").and_then(|t| t.strip_suffix(')')) {
Self::parse_rgba(inner)
} else if let Some(inner) = s.strip_prefix("rgb(").and_then(|t| t.strip_suffix(')')) {
Self::parse_rgb(inner)
} else {
Err(StyleError::ColorParseError(format!(
"unsupported color format: {s}"
)))
}
}
fn parse_hex(hex: &str) -> Result<Self, StyleError> {
let err = || StyleError::ColorParseError(format!("invalid hex color: #{hex}"));
match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| err())?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| err())?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| err())?;
Ok(Color { r, g, b, a: 1.0 })
}
3 => {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).map_err(|_| err())?;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).map_err(|_| err())?;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).map_err(|_| err())?;
Ok(Color { r, g, b, a: 1.0 })
}
_ => Err(err()),
}
}
fn parse_rgb(inner: &str) -> Result<Self, StyleError> {
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() != 3 {
return Err(StyleError::ColorParseError(format!(
"rgb() expects 3 components, got {}",
parts.len()
)));
}
let parse_u8 = |s: &str| -> Result<u8, StyleError> {
s.trim()
.parse::<u8>()
.map_err(|_| StyleError::ColorParseError(format!("invalid channel value: {s}")))
};
Ok(Color {
r: parse_u8(parts[0])?,
g: parse_u8(parts[1])?,
b: parse_u8(parts[2])?,
a: 1.0,
})
}
fn parse_rgba(inner: &str) -> Result<Self, StyleError> {
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() != 4 {
return Err(StyleError::ColorParseError(format!(
"rgba() expects 4 components, got {}",
parts.len()
)));
}
let parse_u8 = |s: &str| -> Result<u8, StyleError> {
s.trim()
.parse::<u8>()
.map_err(|_| StyleError::ColorParseError(format!("invalid channel value: {s}")))
};
let a: f32 = parts[3]
.trim()
.parse()
.map_err(|_| StyleError::ColorParseError(format!("invalid alpha: {}", parts[3])))?;
Ok(Color {
r: parse_u8(parts[0])?,
g: parse_u8(parts[1])?,
b: parse_u8(parts[2])?,
a,
})
}
pub fn to_css(&self) -> String {
format!("rgba({},{},{},{:.6})", self.r, self.g, self.b, self.a)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Paint(pub HashMap<String, serde_json::Value>);
impl Paint {
fn get_pv<T>(&self, key: &str) -> Option<FieldValue<T>>
where
T: Clone + for<'de> Deserialize<'de>,
{
let v = self.0.get(key)?;
serde_json::from_value::<FieldValue<T>>(v.clone()).ok()
}
pub fn fill_color(&self) -> Option<FieldValue<Color>> {
self.get_pv("fill-color")
}
pub fn fill_opacity(&self) -> Option<FieldValue<f64>> {
self.get_pv("fill-opacity")
}
pub fn line_color(&self) -> Option<FieldValue<Color>> {
self.get_pv("line-color")
}
pub fn line_width(&self) -> Option<FieldValue<f64>> {
self.get_pv("line-width")
}
pub fn line_opacity(&self) -> Option<FieldValue<f64>> {
self.get_pv("line-opacity")
}
pub fn circle_color(&self) -> Option<FieldValue<Color>> {
self.get_pv("circle-color")
}
pub fn circle_radius(&self) -> Option<FieldValue<f64>> {
self.get_pv("circle-radius")
}
pub fn raster_opacity(&self) -> Option<FieldValue<f64>> {
self.get_pv("raster-opacity")
}
pub fn raster_hue_rotate(&self) -> Option<FieldValue<f64>> {
self.get_pv("raster-hue-rotate")
}
pub fn background_color(&self) -> Option<FieldValue<Color>> {
self.get_pv("background-color")
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Layout(pub HashMap<String, serde_json::Value>);
impl Layout {
fn get_str(&self, key: &str) -> Option<&str> {
self.0.get(key)?.as_str()
}
fn get_pv<T>(&self, key: &str) -> Option<FieldValue<T>>
where
T: Clone + for<'de> Deserialize<'de>,
{
let v = self.0.get(key)?;
serde_json::from_value::<FieldValue<T>>(v.clone()).ok()
}
pub fn visibility(&self) -> Visibility {
match self.get_str("visibility") {
Some("none") => Visibility::None,
_ => Visibility::Visible,
}
}
pub fn line_cap(&self) -> LineCap {
match self.get_str("line-cap") {
Some("round") => LineCap::Round,
Some("square") => LineCap::Square,
_ => LineCap::Butt,
}
}
pub fn line_join(&self) -> LineJoin {
match self.get_str("line-join") {
Some("round") => LineJoin::Round,
Some("miter") => LineJoin::Miter,
_ => LineJoin::Bevel,
}
}
pub fn symbol_placement(&self) -> SymbolPlacement {
match self.get_str("symbol-placement") {
Some("line") => SymbolPlacement::Line,
Some("line-center") => SymbolPlacement::LineCenter,
_ => SymbolPlacement::Point,
}
}
pub fn text_field(&self) -> Option<FieldValue<String>> {
self.get_pv("text-field")
}
pub fn text_size(&self) -> Option<FieldValue<f64>> {
self.get_pv("text-size")
}
pub fn icon_image(&self) -> Option<FieldValue<String>> {
self.get_pv("icon-image")
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
#[default]
Visible,
None,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LineJoin {
#[default]
Bevel,
Round,
Miter,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum SymbolPlacement {
#[default]
Point,
Line,
LineCenter,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transition {
#[serde(default)]
pub duration: u32,
#[serde(default)]
pub delay: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Light {
#[serde(default)]
pub anchor: LightAnchor,
pub color: Color,
#[serde(default = "default_intensity")]
pub intensity: f64,
#[serde(default = "default_light_position")]
pub position: [f64; 3],
}
fn default_intensity() -> f64 {
0.5
}
fn default_light_position() -> [f64; 3] {
[1.15, 210.0, 30.0]
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LightAnchor {
#[default]
Viewport,
Map,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub layer_id: Option<String>,
pub message: String,
}
pub struct StyleValidator;
impl StyleValidator {
pub fn validate(spec: &StyleSpec) -> Vec<ValidationError> {
let mut errors: Vec<ValidationError> = Vec::new();
if spec.version != 8 {
errors.push(ValidationError {
layer_id: None,
message: format!("style version must be 8, got {}", spec.version),
});
}
let mut seen_ids: HashMap<&str, usize> = HashMap::new();
for (idx, layer) in spec.layers.iter().enumerate() {
if let Some(prev) = seen_ids.insert(layer.id.as_str(), idx) {
errors.push(ValidationError {
layer_id: Some(layer.id.clone()),
message: format!(
"duplicate layer id '{}' (first at index {prev}, repeated at index {idx})",
layer.id
),
});
}
}
for layer in &spec.layers {
if let Some(src) = &layer.source {
if !spec.sources.contains_key(src.as_str()) {
errors.push(ValidationError {
layer_id: Some(layer.id.clone()),
message: format!("layer references unknown source '{src}'"),
});
}
}
if let (Some(min), Some(max)) = (layer.min_zoom, layer.max_zoom) {
if min > max {
errors.push(ValidationError {
layer_id: Some(layer.id.clone()),
message: format!("minzoom ({min}) must be <= maxzoom ({max})"),
});
}
}
if layer.layer_type == LayerType::Background && layer.source.is_some() {
errors.push(ValidationError {
layer_id: Some(layer.id.clone()),
message: "background layer must not reference a source".to_string(),
});
}
let requires_source = matches!(
layer.layer_type,
LayerType::Fill | LayerType::Line | LayerType::Circle | LayerType::Symbol
);
if requires_source && layer.source.is_none() {
errors.push(ValidationError {
layer_id: Some(layer.id.clone()),
message: format!("{:?} layer requires a source", layer.layer_type),
});
}
}
errors
}
}
pub struct StyleRenderer;
impl StyleRenderer {
pub fn eval_zoom_color(value: &FieldValue<Color>, zoom: f64) -> Color {
match value {
FieldValue::Literal(c) => c.clone(),
FieldValue::Expression(expr) => Self::eval_expr_color(expr, zoom).unwrap_or(Color {
r: 0,
g: 0,
b: 0,
a: 1.0,
}),
}
}
fn eval_expr_color(expr: &Expression, zoom: f64) -> Option<Color> {
match expr {
Expression::Literal(v) => {
let s = v.as_str()?;
Color::parse(s).ok()
}
Expression::Interpolate {
interpolation,
input,
stops,
} => {
let input_val = Self::eval_expr_f64(input, zoom)?;
if stops.is_empty() {
return None;
}
let (lo_stop, lo_expr, hi_stop, hi_expr) =
Self::find_stops_color(stops, input_val)?;
let lo_c = Self::eval_expr_color(lo_expr, zoom)?;
let hi_c = Self::eval_expr_color(hi_expr, zoom)?;
let t = Self::interp_t(interpolation, input_val, lo_stop, hi_stop);
Some(lerp_color(&lo_c, &hi_c, t))
}
_ => None,
}
}
fn find_stops_color(
stops: &[(f64, Expression)],
input: f64,
) -> Option<(f64, &Expression, f64, &Expression)> {
if stops.len() == 1 {
return Some((stops[0].0, &stops[0].1, stops[0].0, &stops[0].1));
}
let last = stops.last()?;
if input >= last.0 {
let second_last = &stops[stops.len() - 2];
return Some((second_last.0, &second_last.1, last.0, &last.1));
}
let first = stops.first()?;
if input <= first.0 {
let second = &stops[1];
return Some((first.0, &first.1, second.0, &second.1));
}
for i in 0..stops.len() - 1 {
if input >= stops[i].0 && input < stops[i + 1].0 {
return Some((stops[i].0, &stops[i].1, stops[i + 1].0, &stops[i + 1].1));
}
}
None
}
pub fn eval_zoom_f64(value: &FieldValue<f64>, zoom: f64) -> f64 {
match value {
FieldValue::Literal(v) => *v,
FieldValue::Expression(expr) => Self::eval_expr_f64(expr, zoom).unwrap_or(0.0),
}
}
fn eval_expr_f64(expr: &Expression, zoom: f64) -> Option<f64> {
match expr {
Expression::Zoom => Some(zoom),
Expression::Literal(v) => v.as_f64(),
Expression::Interpolate {
interpolation,
input,
stops,
} => {
let input_val = Self::eval_expr_f64(input, zoom)?;
if stops.is_empty() {
return None;
}
if stops.len() == 1 {
return Self::eval_expr_f64(&stops[0].1, zoom);
}
let last = stops.last()?;
if input_val >= last.0 {
return Self::eval_expr_f64(&last.1, zoom);
}
let first = stops.first()?;
if input_val <= first.0 {
return Self::eval_expr_f64(&first.1, zoom);
}
for i in 0..stops.len() - 1 {
if input_val >= stops[i].0 && input_val < stops[i + 1].0 {
let lo = Self::eval_expr_f64(&stops[i].1, zoom)?;
let hi = Self::eval_expr_f64(&stops[i + 1].1, zoom)?;
let t =
Self::interp_t(interpolation, input_val, stops[i].0, stops[i + 1].0);
return Some(lo + t * (hi - lo));
}
}
None
}
Expression::Step {
input,
default,
stops,
} => {
let input_val = Self::eval_expr_f64(input, zoom)?;
let mut result = Self::eval_expr_f64(default, zoom)?;
for (stop, val) in stops {
if input_val >= *stop {
result = Self::eval_expr_f64(val, zoom)?;
}
}
Some(result)
}
Expression::Add(a, b) => {
Some(Self::eval_expr_f64(a, zoom)? + Self::eval_expr_f64(b, zoom)?)
}
Expression::Subtract(a, b) => {
Some(Self::eval_expr_f64(a, zoom)? - Self::eval_expr_f64(b, zoom)?)
}
Expression::Multiply(a, b) => {
Some(Self::eval_expr_f64(a, zoom)? * Self::eval_expr_f64(b, zoom)?)
}
Expression::Divide(a, b) => {
let divisor = Self::eval_expr_f64(b, zoom)?;
if divisor == 0.0 {
None
} else {
Some(Self::eval_expr_f64(a, zoom)? / divisor)
}
}
_ => None,
}
}
fn interp_t(interp: &Interpolation, input: f64, lo: f64, hi: f64) -> f64 {
let range = hi - lo;
if range == 0.0 {
return 0.0;
}
match interp {
Interpolation::Linear => (input - lo) / range,
Interpolation::Exponential(base) => {
if (base - 1.0).abs() < f64::EPSILON {
(input - lo) / range
} else {
(base.powf(input - lo) - 1.0) / (base.powf(range) - 1.0)
}
}
Interpolation::CubicBezier(_) => {
(input - lo) / range
}
}
}
pub fn feature_matches_filter(
filter: &Filter,
properties: &HashMap<String, serde_json::Value>,
) -> bool {
match filter {
Filter::All(filters) => filters
.iter()
.all(|f| Self::feature_matches_filter(f, properties)),
Filter::Any(filters) => filters
.iter()
.any(|f| Self::feature_matches_filter(f, properties)),
Filter::None(filters) => !filters
.iter()
.any(|f| Self::feature_matches_filter(f, properties)),
Filter::Eq { property, value } => properties.get(property.as_str()) == Some(value),
Filter::Ne { property, value } => properties.get(property.as_str()) != Some(value),
Filter::Lt { property, value } => properties
.get(property.as_str())
.and_then(|v| v.as_f64())
.is_some_and(|v| v < *value),
Filter::Lte { property, value } => properties
.get(property.as_str())
.and_then(|v| v.as_f64())
.is_some_and(|v| v <= *value),
Filter::Gt { property, value } => properties
.get(property.as_str())
.and_then(|v| v.as_f64())
.is_some_and(|v| v > *value),
Filter::Gte { property, value } => properties
.get(property.as_str())
.and_then(|v| v.as_f64())
.is_some_and(|v| v >= *value),
Filter::In { property, values } => properties
.get(property.as_str())
.is_some_and(|v| values.contains(v)),
Filter::Has(property) => properties.contains_key(property.as_str()),
Filter::NotHas(property) => !properties.contains_key(property.as_str()),
Filter::GeometryType(_) => {
true
}
}
}
}
fn lerp_color(a: &Color, b: &Color, t: f64) -> Color {
let lerp_u8 = |lo: u8, hi: u8| -> u8 {
let v = f64::from(lo) + t * (f64::from(hi) - f64::from(lo));
v.round() as u8
};
Color {
r: lerp_u8(a.r, b.r),
g: lerp_u8(a.g, b.g),
b: lerp_u8(a.b, b.b),
a: a.a + t as f32 * (b.a - a.a),
}
}