use color::Rgba8;
use egui::Color32;
use log::warn;
use serde::Deserialize;
pub use serde_json::{Value, json};
use thiserror::Error;
use crate::expression::Context;
#[derive(Deserialize, Default)]
pub struct Style {
pub layers: Vec<Layer>,
}
impl Style {
pub fn protomaps_dark() -> Self {
let style_json = include_str!("../assets/protomaps-dark.json");
serde_json::from_str(style_json).expect("failed to parse style JSON")
}
pub fn protomaps_dark_vis() -> Self {
let style_json = include_str!("../assets/protomaps-dark-vis.json");
serde_json::from_str(style_json).expect("failed to parse style JSON")
}
pub fn protomaps_light() -> Self {
let style_json = include_str!("../assets/protomaps-light.json");
serde_json::from_str(style_json).expect("failed to parse style JSON")
}
pub fn openfreemap_bright() -> Self {
let style_json = include_str!("../assets/openfreemap-bright.json");
serde_json::from_str(style_json).expect("failed to parse style JSON")
}
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum Layer {
Background {
paint: Paint,
},
#[serde(rename_all = "kebab-case")]
Fill {
source_layer: String,
filter: Option<Filter>,
paint: Paint,
},
#[serde(rename_all = "kebab-case")]
Line {
source_layer: String,
filter: Option<Filter>,
paint: Paint,
},
#[serde(rename_all = "kebab-case")]
Symbol {
source_layer: String,
filter: Option<Filter>,
layout: Layout,
paint: Option<Paint>,
},
Circle {
source_layer: String,
filter: Option<Filter>,
},
Raster,
FillExtrusion,
}
#[derive(Deserialize, Default, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Paint {
pub background_color: Option<Color>,
pub fill_color: Option<Color>,
pub fill_opacity: Option<Float>,
pub line_width: Option<Float>,
pub line_color: Option<Color>,
pub line_opacity: Option<Float>,
pub text_color: Option<Color>,
pub text_halo_color: Option<Color>,
}
#[derive(Debug, Error)]
enum StyleError {
#[error(transparent)]
Expression(#[from] crate::expression::Error),
#[error("invalid type")]
InvalidType,
#[error(transparent)]
Parsing(#[from] color::ParseError),
}
#[derive(Deserialize, Debug)]
pub struct Color(pub Value);
impl Color {
pub fn evaluate(&self, context: &Context) -> Color32 {
match self.try_evaluate(context) {
Ok(color) => color,
Err(err) => {
warn!("{err}");
Color32::MAGENTA
}
}
}
fn try_evaluate(&self, context: &Context) -> Result<Color32, StyleError> {
match context.evaluate(&self.0)? {
Value::String(color) => {
let color: color::AlphaColor<color::Srgb> = color.parse()?;
let Rgba8 { r, g, b, a } = color.to_rgba8();
Ok(Color32::from_rgba_premultiplied(r, g, b, a))
}
_ => Err(StyleError::InvalidType),
}
}
}
#[derive(Deserialize, Debug)]
pub struct Float(pub Value);
impl Float {
pub fn evaluate(&self, context: &Context) -> f32 {
match self.try_evaluate(context) {
Ok(opacity) => opacity,
Err(err) => {
warn!("{err}");
0.5
}
}
}
fn try_evaluate(&self, context: &Context) -> Result<f32, StyleError> {
match context.evaluate(&self.0)? {
Value::Number(num) => Ok(num.as_f64().ok_or(StyleError::InvalidType)? as f32),
_ => Err(StyleError::InvalidType),
}
}
}
#[derive(Deserialize, Debug)]
pub struct Filter(pub Value);
impl Filter {
pub fn matches(&self, context: &Context) -> bool {
match context.evaluate(&self.0) {
Ok(Value::Bool(b)) => b,
other => {
warn!("Expected filter to evaluate to boolean, got: {other:?}");
false
}
}
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Layout {
text_field: Option<Value>,
pub text_size: Option<Float>,
}
impl Layout {
pub fn text(&self, context: &Context) -> Option<String> {
self.text_field
.as_ref()
.and_then(|value| match context.evaluate(value) {
Ok(Value::String(s)) => Some(s),
_ => None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_style_parsing() {
Style::protomaps_dark();
Style::protomaps_light();
}
}