use std::path::Path;
use serde::Deserialize;
use crate::{
Result,
styling::{self, LabelConfig, Style, StyleBuilder, SymbolColor},
};
mod color;
mod filter;
pub use color::parse_hex_color;
#[derive(Debug, Deserialize)]
pub struct YamlStyle
{
#[serde(default)]
pub background: Option<String>,
#[serde(default)]
pub rules: Vec<YamlRule>,
}
#[derive(Debug, Deserialize)]
pub struct YamlRule
{
pub filter: YamlFilter,
pub symbol: YamlSymbol,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum TagValue
{
String(String),
Sequence(Vec<String>),
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum YamlGeometry
{
Point,
Linestring,
Polygon,
Multipolygon,
}
#[derive(Debug, Deserialize)]
pub struct YamlFilter
{
#[serde(default)]
pub geometry: Option<YamlGeometry>,
#[serde(default)]
pub tag: Option<std::collections::HashMap<String, TagValue>>,
#[serde(default)]
pub min_zoom: Option<f64>,
#[serde(default)]
pub max_zoom: Option<f64>,
}
#[derive(Debug, Deserialize)]
pub struct YamlSymbol
{
#[serde(default)]
pub fill: Option<String>,
#[serde(default)]
pub stroke: Option<String>,
#[serde(default)]
pub stroke_width: Option<f64>,
#[serde(default)]
pub radius: Option<f64>,
#[serde(default)]
pub label: Option<YamlLabel>,
}
#[derive(Debug, Deserialize)]
pub struct YamlLabel
{
pub field: String,
#[serde(default)]
pub font_size: Option<f32>,
#[serde(default)]
pub color: Option<String>,
#[serde(default)]
pub halo_color: Option<String>,
#[serde(default)]
pub min_zoom: Option<f64>,
}
pub fn load_style(path: impl AsRef<Path>) -> Result<Style<crate::osm::OsmFeature>>
{
let yaml_str = std::fs::read_to_string(path)?;
let yaml_style: YamlStyle = serde_saphyr::from_str(&yaml_str)?;
let mut builder = StyleBuilder::new();
if let Some(bg_color) = &yaml_style.background
{
let rgba = parse_hex_color(bg_color)?;
builder = builder.set_background_color(rgba);
}
for rule in yaml_style.rules
{
builder = add_rule_to_builder(builder, rule)?;
}
Ok(builder.into())
}
fn add_rule_to_builder(
builder: StyleBuilder<crate::osm::OsmFeature>,
rule: YamlRule,
) -> Result<StyleBuilder<crate::osm::OsmFeature>>
{
let symbol = build_symbol(&rule.symbol)?;
let checker = filter::compile_filter(&rule.filter);
Ok(builder.add_rule(symbol).check(checker).finish_rule())
}
fn build_symbol(yaml_symbol: &YamlSymbol) -> Result<styling::Symbol<crate::osm::OsmFeature>>
{
let fill_color = if let Some(fill_str) = &yaml_symbol.fill
{
let rgba = parse_hex_color(fill_str)?;
SymbolColor::new(move |_, _| rgba)
}
else
{
styling::Rgba::TRANSPARENT.into()
};
let stroke_color = if let Some(stroke_str) = &yaml_symbol.stroke
{
let rgba = parse_hex_color(stroke_str)?;
SymbolColor::new(move |_, _| rgba)
}
else
{
styling::Rgba::BLACK.into()
};
let stroke_width = yaml_symbol.stroke_width.unwrap_or(0.0);
let radius = yaml_symbol.radius.unwrap_or(1.0);
let label = yaml_symbol.label.as_ref().map(build_label).transpose()?;
Ok(styling::Symbol {
fill_color,
stroke_color,
stroke_width,
radius,
label,
})
}
fn build_label(yaml_label: &YamlLabel) -> Result<LabelConfig<crate::osm::OsmFeature>>
{
let field = yaml_label.field.clone();
let font_size = yaml_label.font_size.unwrap_or(12.0);
let color = if let Some(color_str) = &yaml_label.color
{
parse_hex_color(color_str)?
}
else
{
styling::Rgba::BLACK
};
let halo_color = if let Some(halo_str) = &yaml_label.halo_color
{
parse_hex_color(halo_str)?
}
else
{
styling::Rgba::WHITE
};
Ok(LabelConfig {
text: std::sync::Arc::new(Box::new(move |feature: &crate::osm::OsmFeature| feature.tag(&field).map(str::to_owned))),
font_size,
color: color.into(),
halo_color: halo_color.into(),
min_zoom: yaml_label.min_zoom.unwrap_or(0.0),
})
}
#[cfg(test)]
mod tests
{
use super::*;
#[test]
fn test_parse_hex_color_6_digits()
{
let color = parse_hex_color("#ff0000").unwrap();
assert!((color.red() - 1.0).abs() < 0.01);
assert!((color.green() - 0.0).abs() < 0.01);
assert!((color.blue() - 0.0).abs() < 0.01);
assert!((color.alpha() - 1.0).abs() < 0.01);
}
#[test]
fn test_parse_hex_color_8_digits()
{
let color = parse_hex_color("#ff000080").unwrap();
assert!((color.red() - 1.0).abs() < 0.01);
assert!((color.green() - 0.0).abs() < 0.01);
assert!((color.blue() - 0.0).abs() < 0.01);
assert!((color.alpha() - 0.5).abs() < 0.01);
}
#[test]
fn test_parse_hex_color_invalid()
{
assert!(parse_hex_color("red").is_err());
assert!(parse_hex_color("#ffff").is_err());
assert!(parse_hex_color("#gggggg").is_err());
}
#[test]
fn test_yaml_deserialize()
{
let yaml_str = "background: \"#aad3df\"\nrules:\n - filter:\n geometry: polygon\n tag:\n natural: water\n min_zoom: 8\n symbol:\n fill: \"#4a90d9\"\n stroke: \"#2c6fad\"\n stroke_width: 0.5\n label:\n field: name\n font_size: 11";
let style: YamlStyle = serde_saphyr::from_str(yaml_str).unwrap();
assert_eq!(style.background, Some("#aad3df".to_string()));
assert_eq!(style.rules.len(), 1);
assert_eq!(style.rules[0].filter.geometry, Some(YamlGeometry::Polygon));
assert_eq!(style.rules[0].filter.min_zoom, Some(8.0));
assert_eq!(
style.rules[0]
.symbol
.label
.as_ref()
.map(|label| label.field.as_str()),
Some("name")
);
}
#[test]
fn test_build_symbol_label_text()
{
let yaml_symbol: YamlSymbol =
serde_saphyr::from_str("fill: \"#4a90d9\"\nlabel:\n field: name\n min_zoom: 5").unwrap();
let symbol = build_symbol(&yaml_symbol).unwrap();
let label = symbol.label.as_ref().unwrap();
let feature = crate::osm::OsmFeature {
id: 1,
geometry: geo::Geometry::Point(geo::Point::new(0.0, 0.0)),
tags: vec![("name".to_string(), "Paris".to_string())],
};
assert_eq!((label.text)(&feature), Some("Paris".to_string()));
assert_eq!(label.min_zoom, 5.0);
}
}