use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock};
use ezu_features::{Feature, FeatureLayer, Value};
use maplibre_expr::{EvaluationContext, Expr, Feature as ExprFeature, Value as ExprValue};
use crate::nodes::common::FeatureGroup;
pub struct SharedLayer {
pub layer: FeatureLayer,
prepared: OnceLock<PreparedLayer>,
}
impl SharedLayer {
pub fn new(layer: FeatureLayer) -> SharedLayer {
SharedLayer {
layer,
prepared: OnceLock::new(),
}
}
fn prepared(&self, z: u8) -> &PreparedLayer {
self.prepared
.get_or_init(|| PreparedLayer::build(&self.layer, z))
}
}
struct PreparedLayer {
features: Vec<PreparedFeature>,
}
struct PreparedFeature {
props: Arc<BTreeMap<String, ExprValue>>,
ctx: EvaluationContext,
}
impl PreparedLayer {
fn build(layer: &FeatureLayer, z: u8) -> PreparedLayer {
let features = layer
.features
.iter()
.map(|f| {
let props: BTreeMap<String, ExprValue> = f
.properties
.iter()
.map(|(k, v)| (k.clone(), value_to_expr(v)))
.collect();
let ctx = ctx_from_props(&props, geometry_type(f), z);
PreparedFeature {
props: Arc::new(props),
ctx,
}
})
.collect();
PreparedLayer { features }
}
}
pub fn collect_groups(
shared: &SharedLayer,
filter_expr: Option<&Expr>,
min_zoom_field: &Option<String>,
z: u8,
) -> Vec<FeatureGroup> {
let prepared = shared.prepared(z);
let mut out = Vec::new();
for (f, pf) in shared.layer.features.iter().zip(&prepared.features) {
if let Some(expr) = filter_expr {
let ok = maplibre_expr::evaluate(expr, &pf.ctx)
.map(|v| v.is_truthy())
.unwrap_or(false);
if !ok {
continue;
}
}
if let Some(field) = min_zoom_field.as_ref() {
let ok = f
.properties
.get(field)
.and_then(value_as_i64)
.map(|mz| mz <= z as i64)
.unwrap_or(true); if !ok {
continue;
}
}
if f.geometry.polygons.is_empty()
&& f.geometry.lines.is_empty()
&& f.geometry.points.is_empty()
{
continue;
}
out.push(FeatureGroup {
properties: pf.props.clone(),
polygons: f.geometry.polygons.clone(),
lines: f.geometry.lines.clone(),
points: f.geometry.points.clone(),
});
}
out
}
fn ctx_from_props(
props: &BTreeMap<String, ExprValue>,
geometry_type: &str,
z: u8,
) -> EvaluationContext {
EvaluationContext::new()
.with_zoom(z as f64)
.with_feature(ExprFeature {
properties: props.clone(),
geometry_type: Some(geometry_type.to_string()),
..Default::default()
})
}
pub(crate) fn group_expr_context(
g: &crate::nodes::common::FeatureGroup,
z: u8,
) -> EvaluationContext {
let geometry_type = if !g.polygons.is_empty() {
"Polygon"
} else if !g.lines.is_empty() {
"LineString"
} else {
"Point"
};
EvaluationContext::new()
.with_zoom(z as f64)
.with_feature(ExprFeature {
properties: (*g.properties).clone(),
geometry_type: Some(geometry_type.to_string()),
..Default::default()
})
}
pub(crate) fn geometry_type(f: &Feature) -> &'static str {
if !f.geometry.polygons.is_empty() {
"Polygon"
} else if !f.geometry.lines.is_empty() {
"LineString"
} else {
"Point"
}
}
pub(crate) fn value_to_expr(v: &Value) -> ExprValue {
match v {
Value::String(s) => ExprValue::String(s.clone()),
Value::Bool(b) => ExprValue::Bool(*b),
Value::Float(n) => ExprValue::Number(*n as f64),
Value::Double(n) => ExprValue::Number(*n),
Value::Int(n) | Value::SInt(n) => ExprValue::Number(*n as f64),
Value::UInt(n) => ExprValue::Number(*n as f64),
Value::Null => ExprValue::Null,
}
}
fn value_as_i64(v: &Value) -> Option<i64> {
match v {
Value::Int(n) | Value::SInt(n) => Some(*n),
Value::UInt(n) => Some(*n as i64),
Value::Float(n) => Some(*n as i64),
Value::Double(n) => Some(*n as i64),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use ezu_features::{Geometry, Polygon};
use std::collections::HashMap;
fn feat(props: &[(&str, Value)]) -> Feature {
let mut properties = HashMap::new();
for (k, v) in props {
properties.insert(k.to_string(), v.clone());
}
let mut geometry = Geometry::default();
geometry.polygons.push(Polygon {
exterior: vec![(0, 0), (10, 0), (10, 10)],
holes: vec![],
});
Feature {
id: None,
geometry,
properties,
}
}
fn expr(j: serde_json::Value) -> Expr {
maplibre_expr::parse(&j).unwrap()
}
fn passes(f: &Feature, e: &Expr) -> bool {
let layer = ezu_features::FeatureLayer {
name: "t".into(),
extent: 4096,
features: vec![f.clone()],
};
let shared = SharedLayer::new(layer);
!collect_groups(&shared, Some(e), &None, 14).is_empty()
}
#[test]
fn expr_filter_comparison_and_has() {
let e = expr(serde_json::json!([
"all",
[">", ["get", "area"], 10],
["has", "name"]
]));
assert!(passes(
&feat(&[
("area", Value::Int(50)),
("name", Value::String("x".into()))
]),
&e
));
assert!(!passes(
&feat(&[("area", Value::Int(5)), ("name", Value::String("x".into()))]),
&e
)); assert!(!passes(&feat(&[("area", Value::Int(50))]), &e)); }
#[test]
fn expr_filter_any_and_geometry_type() {
let e = expr(serde_json::json!([
"all",
[
"any",
["==", ["get", "class"], "a"],
["==", ["get", "class"], "b"]
],
["==", ["geometry-type"], "Polygon"]
]));
assert!(passes(&feat(&[("class", Value::String("a".into()))]), &e));
assert!(passes(&feat(&[("class", Value::String("b".into()))]), &e));
assert!(!passes(&feat(&[("class", Value::String("c".into()))]), &e));
}
#[test]
fn expr_filter_all_combines_clauses() {
let e = expr(serde_json::json!([
"all",
["==", ["get", "class"], "road"],
[">=", ["get", "lanes"], 2]
]));
assert!(passes(
&feat(&[
("class", Value::String("road".into())),
("lanes", Value::Int(4)),
]),
&e
));
assert!(!passes(
&feat(&[
("class", Value::String("path".into())),
("lanes", Value::Int(4)),
]),
&e
)); assert!(!passes(
&feat(&[
("class", Value::String("road".into())),
("lanes", Value::Int(1)),
]),
&e
)); }
}