use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use ezu_features::{Feature, FeatureLayer, Geometry, Polygon, Value as FeatureValue};
use ezu_graph::{
build_graph, Asset, AssetError, AssetLoader, BuildGraphError, Cache, CanvasInfo, Evaluator,
NodeRegistry, OpaqueValue, ParamValues, PortValue, RasterBuf, RenderError, TileId,
};
use ezu_style::{Document, LegendEntry, LegendGeometry};
use xxhash_rust::xxh3::Xxh3;
use crate::host::looks_like_asset_src;
use crate::render::SharedLayer;
const EXTENT: u32 = 4096;
#[derive(Debug, Clone, Copy)]
pub struct SwatchOptions {
pub width: u32,
pub height: u32,
pub zoom: u8,
pub pad: u32,
pub geometry: LegendGeometry,
}
impl Default for SwatchOptions {
fn default() -> Self {
Self {
width: 48,
height: 32,
zoom: 12,
pad: 0,
geometry: LegendGeometry::default(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SwatchError {
#[error("legend entry `{label}` names `@{src}`, which is not a node in this style")]
UnknownNode { label: String, src: String },
#[error("legend entry `{label}`: {source}")]
Build {
label: String,
#[source]
source: Box<BuildGraphError>,
},
#[error("legend entry `{label}`: {source}")]
Render {
label: String,
#[source]
source: Box<RenderError>,
},
#[error("legend entry `{label}` names `@{src}`, which produced {got} rather than a raster")]
NotRaster {
label: String,
src: String,
got: String,
},
}
pub fn render_swatch(
doc: &Document,
entry: &LegendEntry,
registry: &NodeRegistry,
assets: &dyn AssetLoader,
params: &ParamValues,
cache: &Cache,
opts: &SwatchOptions,
) -> Result<(Arc<RasterBuf>, CanvasInfo), SwatchError> {
let src = entry.from.as_str();
let sub = doc.subgraph(src).ok_or_else(|| SwatchError::UnknownNode {
label: entry.label.clone(),
src: src.to_string(),
})?;
let graph = build_graph(&sub, registry).map_err(|e| SwatchError::Build {
label: entry.label.clone(),
source: Box::new(e),
})?;
let geometry = entry.geometry.unwrap_or(opts.geometry);
let loader = SwatchLoader::new(assets, stand_in_layer(entry, geometry), entry);
let ev = Evaluator::new(&graph, cache, &loader);
let required = graph.required_pad().unwrap_or(0);
let canvas = CanvasInfo {
tile_w: opts.width,
tile_h: opts.height,
pad: opts.pad.max(required),
};
let n = 1u32 << opts.zoom;
let tile = TileId {
z: opts.zoom,
x: n / 2,
y: n / 2,
};
let out = ev
.render(tile, canvas, params, 0)
.map_err(|e| SwatchError::Render {
label: entry.label.clone(),
source: Box::new(e),
})?;
match out {
PortValue::Raster(r) => Ok((r, canvas)),
other => Err(SwatchError::NotRaster {
label: entry.label.clone(),
src: src.to_string(),
got: format!("{:?}", other.kind()),
}),
}
}
const LINE_VERTICES: usize = 64;
pub fn stand_in_layer(entry: &LegendEntry, geometry: LegendGeometry) -> FeatureLayer {
let e = EXTENT as i32;
let mid = e / 2;
let mut g = Geometry::default();
if matches!(geometry, LegendGeometry::All | LegendGeometry::Polygon) {
g.polygons.push(Polygon {
exterior: vec![(0, 0), (e, 0), (e, e), (0, e), (0, 0)],
holes: vec![],
});
}
if matches!(geometry, LegendGeometry::All | LegendGeometry::Line) {
let last = LINE_VERTICES - 1;
g.lines.push(
(0..LINE_VERTICES)
.map(|i| ((e as i64 * i as i64 / last as i64) as i32, mid))
.collect(),
);
}
if matches!(geometry, LegendGeometry::All | LegendGeometry::Point) {
g.points.push((mid, mid));
}
FeatureLayer {
name: "legend".to_string(),
extent: EXTENT,
features: vec![Feature {
id: None,
geometry: g,
properties: properties_of(entry),
}],
}
}
fn properties_of(entry: &LegendEntry) -> HashMap<String, FeatureValue> {
let mut out = HashMap::new();
for (k, v) in &entry.properties {
let value = match v {
serde_json::Value::String(s) => FeatureValue::String(s.clone()),
serde_json::Value::Bool(b) => FeatureValue::Bool(*b),
serde_json::Value::Null => FeatureValue::Null,
serde_json::Value::Number(n) => match n.as_i64() {
Some(i) => FeatureValue::Int(i),
None => FeatureValue::Double(n.as_f64().unwrap_or(0.0)),
},
_ => continue,
};
out.insert(k.clone(), value);
}
out
}
struct SwatchLoader<'a> {
base: &'a dyn AssetLoader,
features: OpaqueValue,
hash: u128,
}
impl<'a> SwatchLoader<'a> {
fn new(base: &'a dyn AssetLoader, layer: FeatureLayer, entry: &LegendEntry) -> Self {
let mut h = Xxh3::new();
h.update(entry.from.as_str().as_bytes());
for (k, v) in &entry.properties {
h.update(k.as_bytes());
h.update(v.to_string().as_bytes());
}
Self {
base,
features: Arc::new(SharedLayer::new(layer)) as Arc<dyn Any + Send + Sync>,
hash: h.digest128(),
}
}
}
impl AssetLoader for SwatchLoader<'_> {
fn load(&self, name: &str) -> Result<Asset, AssetError> {
if looks_like_asset_src(name) {
return self.base.load(name);
}
Ok(Asset::Features(self.features.clone()))
}
fn hash(&self, name: &str) -> u128 {
if looks_like_asset_src(name) {
return self.base.hash(name);
}
self.hash
}
}
#[cfg(test)]
mod tests {
use super::*;
use ezu_style::NodeRef;
fn entry(props: &[(&str, serde_json::Value)]) -> LegendEntry {
let mut properties = serde_json::Map::new();
for (k, v) in props {
properties.insert((*k).to_string(), v.clone());
}
LegendEntry {
label: "e".into(),
from: NodeRef("n".into()),
properties,
note: None,
min_zoom: None,
max_zoom: None,
geometry: None,
}
}
#[test]
fn the_stand_in_line_is_finely_sampled() {
let layer = stand_in_layer(&entry(&[]), LegendGeometry::Line);
let line = &layer.features[0].geometry.lines[0];
assert!(
line.len() >= 32,
"a brush needs many events along a stroke, got {}",
line.len()
);
assert_eq!(line.first().unwrap().0, 0);
assert_eq!(line.last().unwrap().0, EXTENT as i32);
assert!(line.iter().all(|&(_, y)| y == EXTENT as i32 / 2));
assert!(line.windows(2).all(|w| w[0].0 < w[1].0));
}
#[test]
fn geometry_selects_what_the_stand_in_carries() {
for (geometry, polys, lines, points) in [
(LegendGeometry::All, 1, 1, 1),
(LegendGeometry::Polygon, 1, 0, 0),
(LegendGeometry::Line, 0, 1, 0),
(LegendGeometry::Point, 0, 0, 1),
] {
let layer = stand_in_layer(&entry(&[]), geometry);
let g = &layer.features[0].geometry;
assert_eq!(
(g.polygons.len(), g.lines.len(), g.points.len()),
(polys, lines, points),
"{geometry:?}"
);
}
}
#[test]
fn properties_become_feature_values() {
let layer = stand_in_layer(
&entry(&[
("cls", "trunk".into()),
("min_zoom", 0.into()),
("density", 12.5.into()),
("on", true.into()),
("nope", serde_json::json!([1, 2])),
]),
LegendGeometry::All,
);
let p = &layer.features[0].properties;
assert!(matches!(p.get("cls"), Some(FeatureValue::String(s)) if s == "trunk"));
assert!(matches!(p.get("min_zoom"), Some(FeatureValue::Int(0))));
assert!(matches!(p.get("density"), Some(FeatureValue::Double(d)) if *d == 12.5));
assert!(matches!(p.get("on"), Some(FeatureValue::Bool(true))));
assert!(p.get("nope").is_none(), "an array is not a property value");
}
}