use crate::geometry::PropertyValue;
use crate::query::FeatureState;
use std::collections::HashMap;
use std::fmt;
pub type FeatureProperties = HashMap<String, PropertyValue>;
#[derive(Debug, Clone, Copy)]
pub struct ExprEvalContext<'a> {
pub zoom: f32,
pub pitch: f32,
pub properties: Option<&'a FeatureProperties>,
pub feature_state: Option<&'a FeatureState>,
}
impl<'a> ExprEvalContext<'a> {
pub fn zoom_only(zoom: f32) -> Self {
Self {
zoom,
pitch: 0.0,
properties: None,
feature_state: None,
}
}
pub fn with_feature(zoom: f32, properties: &'a FeatureProperties) -> Self {
Self {
zoom,
pitch: 0.0,
properties: Some(properties),
feature_state: None,
}
}
pub fn and_state(mut self, state: &'a FeatureState) -> Self {
self.feature_state = Some(state);
self
}
pub fn and_pitch(mut self, pitch: f32) -> Self {
self.pitch = pitch;
self
}
pub fn get_property(&self, key: &str) -> Option<&PropertyValue> {
self.properties.and_then(|p| p.get(key))
}
pub fn get_state(&self, key: &str) -> Option<&PropertyValue> {
self.feature_state.and_then(|s| s.get(key))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expression<T> {
Constant(T),
ZoomStops(Vec<(f32, T)>),
FeatureState {
key: String,
fallback: T,
},
GetProperty {
key: String,
fallback: T,
},
Interpolate {
input: Box<NumericExpression>,
stops: Vec<(f32, T)>,
},
Step {
input: Box<NumericExpression>,
default: T,
stops: Vec<(f32, T)>,
},
Match {
input: Box<StringExpression>,
cases: Vec<(String, T)>,
fallback: T,
},
Case {
branches: Vec<(BoolExpression, T)>,
fallback: T,
},
Coalesce(Vec<Expression<T>>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum NumericExpression {
Literal(f64),
Zoom,
Pitch,
GetProperty {
key: String,
fallback: f64,
},
GetState {
key: String,
fallback: f64,
},
Add(Box<NumericExpression>, Box<NumericExpression>),
Sub(Box<NumericExpression>, Box<NumericExpression>),
Mul(Box<NumericExpression>, Box<NumericExpression>),
Div(Box<NumericExpression>, Box<NumericExpression>),
Mod(Box<NumericExpression>, Box<NumericExpression>),
Pow(Box<NumericExpression>, Box<NumericExpression>),
Abs(Box<NumericExpression>),
Ln(Box<NumericExpression>),
Sqrt(Box<NumericExpression>),
Min(Box<NumericExpression>, Box<NumericExpression>),
Max(Box<NumericExpression>, Box<NumericExpression>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum StringExpression {
Literal(String),
GetProperty {
key: String,
fallback: String,
},
GetState {
key: String,
fallback: String,
},
Concat(Box<StringExpression>, Box<StringExpression>),
Upcase(Box<StringExpression>),
Downcase(Box<StringExpression>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum BoolExpression {
Literal(bool),
GetProperty {
key: String,
fallback: bool,
},
GetState {
key: String,
fallback: bool,
},
Has(String),
Not(Box<BoolExpression>),
All(Vec<BoolExpression>),
Any(Vec<BoolExpression>),
Eq(NumericExpression, NumericExpression),
Neq(NumericExpression, NumericExpression),
Gt(NumericExpression, NumericExpression),
Gte(NumericExpression, NumericExpression),
Lt(NumericExpression, NumericExpression),
Lte(NumericExpression, NumericExpression),
StrEq(StringExpression, StringExpression),
}
impl NumericExpression {
pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> f64 {
match self {
NumericExpression::Literal(v) => *v,
NumericExpression::Zoom => ctx.zoom as f64,
NumericExpression::Pitch => ctx.pitch as f64,
NumericExpression::GetProperty { key, fallback } => ctx
.get_property(key)
.and_then(PropertyValue::as_f64)
.unwrap_or(*fallback),
NumericExpression::GetState { key, fallback } => ctx
.get_state(key)
.and_then(PropertyValue::as_f64)
.unwrap_or(*fallback),
NumericExpression::Add(a, b) => a.eval(ctx) + b.eval(ctx),
NumericExpression::Sub(a, b) => a.eval(ctx) - b.eval(ctx),
NumericExpression::Mul(a, b) => a.eval(ctx) * b.eval(ctx),
NumericExpression::Div(a, b) => {
let denom = b.eval(ctx);
if denom.abs() < f64::EPSILON {
0.0
} else {
a.eval(ctx) / denom
}
}
NumericExpression::Mod(a, b) => {
let denom = b.eval(ctx);
if denom.abs() < f64::EPSILON {
0.0
} else {
a.eval(ctx) % denom
}
}
NumericExpression::Pow(a, b) => a.eval(ctx).powf(b.eval(ctx)),
NumericExpression::Abs(a) => a.eval(ctx).abs(),
NumericExpression::Ln(a) => a.eval(ctx).ln(),
NumericExpression::Sqrt(a) => a.eval(ctx).sqrt(),
NumericExpression::Min(a, b) => a.eval(ctx).min(b.eval(ctx)),
NumericExpression::Max(a, b) => a.eval(ctx).max(b.eval(ctx)),
}
}
}
impl StringExpression {
pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> String {
match self {
StringExpression::Literal(v) => v.clone(),
StringExpression::GetProperty { key, fallback } => ctx
.get_property(key)
.and_then(PropertyValue::as_str)
.map(|s| s.to_owned())
.unwrap_or_else(|| fallback.clone()),
StringExpression::GetState { key, fallback } => ctx
.get_state(key)
.and_then(PropertyValue::as_str)
.map(|s| s.to_owned())
.unwrap_or_else(|| fallback.clone()),
StringExpression::Concat(a, b) => {
let mut s = a.eval(ctx);
s.push_str(&b.eval(ctx));
s
}
StringExpression::Upcase(a) => a.eval(ctx).to_uppercase(),
StringExpression::Downcase(a) => a.eval(ctx).to_lowercase(),
}
}
}
impl BoolExpression {
pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> bool {
match self {
BoolExpression::Literal(v) => *v,
BoolExpression::GetProperty { key, fallback } => ctx
.get_property(key)
.and_then(PropertyValue::as_bool)
.unwrap_or(*fallback),
BoolExpression::GetState { key, fallback } => ctx
.get_state(key)
.and_then(PropertyValue::as_bool)
.unwrap_or(*fallback),
BoolExpression::Has(key) => ctx
.properties
.map(|p| p.contains_key(key.as_str()))
.unwrap_or(false),
BoolExpression::Not(a) => !a.eval(ctx),
BoolExpression::All(exprs) => exprs.iter().all(|e| e.eval(ctx)),
BoolExpression::Any(exprs) => exprs.iter().any(|e| e.eval(ctx)),
BoolExpression::Eq(a, b) => (a.eval(ctx) - b.eval(ctx)).abs() < f64::EPSILON,
BoolExpression::Neq(a, b) => (a.eval(ctx) - b.eval(ctx)).abs() >= f64::EPSILON,
BoolExpression::Gt(a, b) => a.eval(ctx) > b.eval(ctx),
BoolExpression::Gte(a, b) => a.eval(ctx) >= b.eval(ctx),
BoolExpression::Lt(a, b) => a.eval(ctx) < b.eval(ctx),
BoolExpression::Lte(a, b) => a.eval(ctx) <= b.eval(ctx),
BoolExpression::StrEq(a, b) => a.eval(ctx) == b.eval(ctx),
}
}
}
fn eval_stops<T: super::style::StyleInterpolatable>(stops: &[(f32, T)], input: f32) -> T {
debug_assert!(!stops.is_empty(), "stop list must not be empty");
let (first_input, first_value) = &stops[0];
if input <= *first_input {
return first_value.clone();
}
for pair in stops.windows(2) {
let (i0, v0) = &pair[0];
let (i1, v1) = &pair[1];
if input <= *i1 {
let span = (*i1 - *i0).max(f32::EPSILON);
let t = (input - *i0) / span;
return T::interpolate(v0, v1, t);
}
}
stops.last().expect("non-empty stops").1.clone()
}
impl<T: super::style::StyleInterpolatable> Expression<T> {
pub fn evaluate(&self) -> T {
self.eval_full(&ExprEvalContext::zoom_only(0.0))
}
pub fn evaluate_with_context(&self, ctx: super::style::StyleEvalContext) -> T {
self.eval_full(&ExprEvalContext::zoom_only(ctx.zoom))
}
pub fn evaluate_with_full_context(&self, ctx: &super::style::StyleEvalContextFull<'_>) -> T {
let expr_ctx = ExprEvalContext {
zoom: ctx.zoom,
pitch: 0.0,
properties: None,
feature_state: Some(ctx.feature_state),
};
self.eval_full(&expr_ctx)
}
pub fn evaluate_with_properties(&self, ctx: &ExprEvalContext<'_>) -> T {
self.eval_full(ctx)
}
pub fn eval_full(&self, ctx: &ExprEvalContext<'_>) -> T {
match self {
Expression::Constant(value) => value.clone(),
Expression::ZoomStops(stops) => eval_stops(stops, ctx.zoom),
Expression::FeatureState { key, fallback } => ctx
.get_state(key)
.and_then(|prop| T::from_feature_state_property(prop))
.unwrap_or_else(|| fallback.clone()),
Expression::GetProperty { key, fallback } => ctx
.get_property(key)
.and_then(|prop| T::from_feature_state_property(prop))
.unwrap_or_else(|| fallback.clone()),
Expression::Interpolate { input, stops } => {
let input_val = input.eval(ctx) as f32;
eval_stops(stops, input_val)
}
Expression::Step {
input,
default,
stops,
} => {
let input_val = input.eval(ctx) as f32;
if stops.is_empty() || input_val < stops[0].0 {
return default.clone();
}
let mut result = default;
for (threshold, value) in stops {
if input_val >= *threshold {
result = value;
} else {
break;
}
}
result.clone()
}
Expression::Match {
input,
cases,
fallback,
} => {
let input_val = input.eval(ctx);
for (label, value) in cases {
if *label == input_val {
return value.clone();
}
}
fallback.clone()
}
Expression::Case { branches, fallback } => {
for (condition, value) in branches {
if condition.eval(ctx) {
return value.clone();
}
}
fallback.clone()
}
Expression::Coalesce(exprs) => {
if let Some(first) = exprs.first() {
first.eval_full(ctx)
} else {
panic!("Expression::Coalesce requires at least one sub-expression");
}
}
}
}
}
impl<T> Expression<T> {
pub fn feature_state_key(key: impl Into<String>, fallback: T) -> Self {
Expression::FeatureState {
key: key.into(),
fallback,
}
}
pub fn is_feature_state_driven(&self) -> bool {
match self {
Expression::FeatureState { .. } => true,
Expression::Case { branches, .. } => {
branches.iter().any(|(cond, _)| cond.uses_feature_state())
}
Expression::Coalesce(exprs) => exprs.iter().any(|e| e.is_feature_state_driven()),
_ => false,
}
}
pub fn is_data_driven(&self) -> bool {
match self {
Expression::GetProperty { .. } => true,
Expression::Match { .. } => true,
Expression::Interpolate { .. } => true,
Expression::Step { .. } => true,
Expression::Case { .. } => true,
Expression::Coalesce(exprs) => exprs.iter().any(|e| e.is_data_driven()),
_ => false,
}
}
}
impl<T> From<T> for Expression<T> {
fn from(value: T) -> Self {
Expression::Constant(value)
}
}
impl BoolExpression {
pub fn uses_feature_state(&self) -> bool {
match self {
BoolExpression::GetState { .. } => true,
BoolExpression::Not(a) => a.uses_feature_state(),
BoolExpression::All(exprs) => exprs.iter().any(|e| e.uses_feature_state()),
BoolExpression::Any(exprs) => exprs.iter().any(|e| e.uses_feature_state()),
_ => false,
}
}
}
impl<T: fmt::Debug> fmt::Display for Expression<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expression::Constant(v) => write!(f, "{v:?}"),
Expression::ZoomStops(stops) => {
write!(f, "zoom_stops[")?;
for (i, (z, v)) in stops.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{z}: {v:?}")?;
}
write!(f, "]")
}
Expression::FeatureState { key, fallback } => {
write!(f, "feature_state(\"{key}\", {fallback:?})")
}
Expression::GetProperty { key, fallback } => {
write!(f, "get(\"{key}\", {fallback:?})")
}
Expression::Interpolate { input, stops } => {
write!(f, "interpolate({input:?}, [")?;
for (i, (z, v)) in stops.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{z}: {v:?}")?;
}
write!(f, "])")
}
Expression::Step {
input,
default,
stops,
} => {
write!(f, "step({input:?}, {default:?}, [")?;
for (i, (z, v)) in stops.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{z}: {v:?}")?;
}
write!(f, "])")
}
Expression::Match {
input,
cases,
fallback,
} => {
write!(f, "match({input:?}, [")?;
for (i, (lbl, v)) in cases.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "\"{lbl}\": {v:?}")?;
}
write!(f, "], {fallback:?})")
}
Expression::Case { branches, fallback } => {
write!(f, "case([")?;
for (i, (cond, v)) in branches.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{cond:?} => {v:?}")?;
}
write!(f, "], {fallback:?})")
}
Expression::Coalesce(exprs) => {
write!(f, "coalesce(")?;
for (i, e) in exprs.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{e}")?;
}
write!(f, ")")
}
}
}
}
impl Expression<f32> {
pub fn zoom_interpolate(stops: Vec<(f32, f32)>) -> Self {
Expression::Interpolate {
input: Box::new(NumericExpression::Zoom),
stops,
}
}
pub fn zoom_step(default: f32, stops: Vec<(f32, f32)>) -> Self {
Expression::Step {
input: Box::new(NumericExpression::Zoom),
default,
stops,
}
}
pub fn property(key: impl Into<String>, fallback: f32) -> Self {
Expression::GetProperty {
key: key.into(),
fallback,
}
}
pub fn property_interpolate(
property: impl Into<String>,
fallback: f64,
stops: Vec<(f32, f32)>,
) -> Self {
Expression::Interpolate {
input: Box::new(NumericExpression::GetProperty {
key: property.into(),
fallback,
}),
stops,
}
}
}
impl Expression<[f32; 4]> {
pub fn zoom_interpolate(stops: Vec<(f32, [f32; 4])>) -> Self {
Expression::Interpolate {
input: Box::new(NumericExpression::Zoom),
stops,
}
}
pub fn zoom_step(default: [f32; 4], stops: Vec<(f32, [f32; 4])>) -> Self {
Expression::Step {
input: Box::new(NumericExpression::Zoom),
default,
stops,
}
}
pub fn property_match(
property: impl Into<String>,
cases: Vec<(String, [f32; 4])>,
fallback: [f32; 4],
) -> Self {
Expression::Match {
input: Box::new(StringExpression::GetProperty {
key: property.into(),
fallback: String::new(),
}),
cases,
fallback,
}
}
}
impl Expression<bool> {
pub fn property(key: impl Into<String>, fallback: bool) -> Self {
Expression::GetProperty {
key: key.into(),
fallback,
}
}
}
impl Expression<String> {
pub fn property(key: impl Into<String>, fallback: impl Into<String>) -> Self {
Expression::GetProperty {
key: key.into(),
fallback: fallback.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::PropertyValue;
use crate::style::{StyleEvalContext, StyleEvalContextFull};
#[test]
fn constant_evaluates_directly() {
let expr: Expression<f32> = Expression::Constant(42.0);
assert!((expr.evaluate() - 42.0).abs() < f32::EPSILON);
}
#[test]
fn constant_via_into() {
let expr: Expression<f32> = 42.0.into();
assert!((expr.evaluate() - 42.0).abs() < f32::EPSILON);
}
#[test]
fn zoom_stops_interpolates() {
let expr = Expression::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
let ctx = ExprEvalContext::zoom_only(5.0);
let result = expr.eval_full(&ctx);
assert!((result - 50.0).abs() < 0.1);
}
#[test]
fn zoom_stops_clamps_below() {
let expr = Expression::ZoomStops(vec![(5.0, 10.0_f32), (10.0, 20.0)]);
let ctx = ExprEvalContext::zoom_only(0.0);
assert!((expr.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
}
#[test]
fn zoom_stops_clamps_above() {
let expr = Expression::ZoomStops(vec![(5.0, 10.0_f32), (10.0, 20.0)]);
let ctx = ExprEvalContext::zoom_only(99.0);
assert!((expr.eval_full(&ctx) - 20.0).abs() < f32::EPSILON);
}
#[test]
fn feature_state_returns_fallback_without_state() {
let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
let ctx = ExprEvalContext::zoom_only(10.0);
assert!((expr.eval_full(&ctx) - 0.5).abs() < f32::EPSILON);
}
#[test]
fn feature_state_resolves_from_state_map() {
let mut state = HashMap::new();
state.insert("opacity".to_string(), PropertyValue::Number(0.8));
let ctx = ExprEvalContext::zoom_only(10.0).and_state(&state);
let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
assert!((expr.eval_full(&ctx) - 0.8).abs() < f32::EPSILON);
}
#[test]
fn legacy_evaluate_with_context() {
let expr = Expression::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
let result = expr.evaluate_with_context(StyleEvalContext::new(5.0));
assert!((result - 50.0).abs() < 0.1);
}
#[test]
fn legacy_evaluate_with_full_context() {
let mut state = HashMap::new();
state.insert("opacity".to_string(), PropertyValue::Number(0.8));
let ctx = StyleEvalContextFull::new(10.0, &state);
let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
assert!((expr.evaluate_with_full_context(&ctx) - 0.8).abs() < f32::EPSILON);
}
#[test]
fn get_property_reads_feature_property() {
let mut props = HashMap::new();
props.insert("height".to_string(), PropertyValue::Number(50.0));
let ctx = ExprEvalContext::with_feature(10.0, &props);
let expr = Expression::<f32>::property("height", 0.0);
assert!((expr.eval_full(&ctx) - 50.0).abs() < f32::EPSILON);
}
#[test]
fn get_property_returns_fallback_when_missing() {
let props = HashMap::new();
let ctx = ExprEvalContext::with_feature(10.0, &props);
let expr = Expression::<f32>::property("height", 10.0);
assert!((expr.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
}
#[test]
fn interpolate_on_property() {
let mut props = HashMap::new();
props.insert("population".to_string(), PropertyValue::Number(500.0));
let ctx = ExprEvalContext::with_feature(10.0, &props);
let expr = Expression::<f32>::property_interpolate(
"population",
0.0,
vec![(0.0, 2.0), (1000.0, 20.0)],
);
let result = expr.eval_full(&ctx);
assert!((result - 11.0).abs() < 0.1);
}
#[test]
fn zoom_interpolate_convenience() {
let expr = Expression::<f32>::zoom_interpolate(vec![(0.0, 1.0), (20.0, 10.0)]);
let ctx = ExprEvalContext::zoom_only(10.0);
assert!((expr.eval_full(&ctx) - 5.5).abs() < 0.1);
}
#[test]
fn step_below_first_returns_default() {
let expr = Expression::Step {
input: Box::new(NumericExpression::Zoom),
default: 1.0_f32,
stops: vec![(5.0, 2.0), (10.0, 3.0)],
};
let ctx = ExprEvalContext::zoom_only(3.0);
assert!((expr.eval_full(&ctx) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn step_between_stops() {
let expr = Expression::Step {
input: Box::new(NumericExpression::Zoom),
default: 1.0_f32,
stops: vec![(5.0, 2.0), (10.0, 3.0)],
};
let ctx = ExprEvalContext::zoom_only(7.0);
assert!((expr.eval_full(&ctx) - 2.0).abs() < f32::EPSILON);
}
#[test]
fn step_above_last() {
let expr = Expression::Step {
input: Box::new(NumericExpression::Zoom),
default: 1.0_f32,
stops: vec![(5.0, 2.0), (10.0, 3.0)],
};
let ctx = ExprEvalContext::zoom_only(15.0);
assert!((expr.eval_full(&ctx) - 3.0).abs() < f32::EPSILON);
}
#[test]
fn match_on_string_property() {
let mut props = HashMap::new();
props.insert(
"type".to_string(),
PropertyValue::String("residential".to_string()),
);
let ctx = ExprEvalContext::with_feature(10.0, &props);
let expr: Expression<[f32; 4]> = Expression::property_match(
"type",
vec![
("residential".to_string(), [0.0, 0.0, 1.0, 1.0]),
("commercial".to_string(), [1.0, 0.0, 0.0, 1.0]),
],
[0.5, 0.5, 0.5, 1.0],
);
let result = expr.eval_full(&ctx);
assert_eq!(result, [0.0, 0.0, 1.0, 1.0]);
}
#[test]
fn match_returns_fallback_when_no_case() {
let mut props = HashMap::new();
props.insert(
"type".to_string(),
PropertyValue::String("industrial".to_string()),
);
let ctx = ExprEvalContext::with_feature(10.0, &props);
let expr: Expression<[f32; 4]> = Expression::property_match(
"type",
vec![("residential".to_string(), [0.0, 0.0, 1.0, 1.0])],
[0.5, 0.5, 0.5, 1.0],
);
assert_eq!(expr.eval_full(&ctx), [0.5, 0.5, 0.5, 1.0]);
}
#[test]
fn case_with_bool_conditions() {
let mut props = HashMap::new();
props.insert("height".to_string(), PropertyValue::Number(150.0));
let ctx = ExprEvalContext::with_feature(10.0, &props);
let expr: Expression<[f32; 4]> = Expression::Case {
branches: vec![
(
BoolExpression::Gt(
NumericExpression::GetProperty {
key: "height".to_string(),
fallback: 0.0,
},
NumericExpression::Literal(100.0),
),
[1.0, 0.0, 0.0, 1.0], ),
(
BoolExpression::Gt(
NumericExpression::GetProperty {
key: "height".to_string(),
fallback: 0.0,
},
NumericExpression::Literal(50.0),
),
[1.0, 1.0, 0.0, 1.0], ),
],
fallback: [0.0, 1.0, 0.0, 1.0], };
assert_eq!(expr.eval_full(&ctx), [1.0, 0.0, 0.0, 1.0]);
}
#[test]
fn case_fallback_when_no_branch_matches() {
let props = HashMap::new();
let ctx = ExprEvalContext::with_feature(10.0, &props);
let expr: Expression<f32> = Expression::Case {
branches: vec![
(BoolExpression::Literal(false), 10.0),
(BoolExpression::Literal(false), 20.0),
],
fallback: 99.0,
};
assert!((expr.eval_full(&ctx) - 99.0).abs() < f32::EPSILON);
}
#[test]
fn numeric_arithmetic() {
let ctx = ExprEvalContext::zoom_only(10.0);
let add = NumericExpression::Add(
Box::new(NumericExpression::Literal(3.0)),
Box::new(NumericExpression::Literal(4.0)),
);
assert!((add.eval(&ctx) - 7.0).abs() < f64::EPSILON);
let mul = NumericExpression::Mul(
Box::new(NumericExpression::Zoom),
Box::new(NumericExpression::Literal(2.0)),
);
assert!((mul.eval(&ctx) - 20.0).abs() < f64::EPSILON);
}
#[test]
fn numeric_division_by_zero() {
let ctx = ExprEvalContext::zoom_only(10.0);
let div = NumericExpression::Div(
Box::new(NumericExpression::Literal(10.0)),
Box::new(NumericExpression::Literal(0.0)),
);
assert!((div.eval(&ctx) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn bool_has_checks_property_existence() {
let mut props = HashMap::new();
props.insert(
"name".to_string(),
PropertyValue::String("test".to_string()),
);
let ctx = ExprEvalContext::with_feature(10.0, &props);
assert!(BoolExpression::Has("name".to_string()).eval(&ctx));
assert!(!BoolExpression::Has("missing".to_string()).eval(&ctx));
}
#[test]
fn bool_all_and_any() {
let ctx = ExprEvalContext::zoom_only(10.0);
assert!(BoolExpression::All(vec![
BoolExpression::Literal(true),
BoolExpression::Literal(true),
])
.eval(&ctx));
assert!(!BoolExpression::All(vec![
BoolExpression::Literal(true),
BoolExpression::Literal(false),
])
.eval(&ctx));
assert!(BoolExpression::Any(vec![
BoolExpression::Literal(false),
BoolExpression::Literal(true),
])
.eval(&ctx));
}
#[test]
fn string_concat() {
let ctx = ExprEvalContext::zoom_only(10.0);
let concat = StringExpression::Concat(
Box::new(StringExpression::Literal("hello ".to_string())),
Box::new(StringExpression::Literal("world".to_string())),
);
assert_eq!(concat.eval(&ctx), "hello world");
}
#[test]
fn string_upcase_downcase() {
let ctx = ExprEvalContext::zoom_only(10.0);
let up = StringExpression::Upcase(Box::new(StringExpression::Literal("hello".to_string())));
assert_eq!(up.eval(&ctx), "HELLO");
let down =
StringExpression::Downcase(Box::new(StringExpression::Literal("HELLO".to_string())));
assert_eq!(down.eval(&ctx), "hello");
}
#[test]
fn is_data_driven_flags() {
let constant: Expression<f32> = Expression::Constant(1.0);
assert!(!constant.is_data_driven());
let get: Expression<f32> = Expression::GetProperty {
key: "height".into(),
fallback: 0.0,
};
assert!(get.is_data_driven());
let interp = Expression::<f32>::zoom_interpolate(vec![(0.0, 1.0), (10.0, 5.0)]);
assert!(interp.is_data_driven()); }
#[test]
fn is_feature_state_driven_flags() {
let constant: Expression<f32> = Expression::Constant(1.0);
assert!(!constant.is_feature_state_driven());
let driven: Expression<f32> = Expression::feature_state_key("opacity", 1.0);
assert!(driven.is_feature_state_driven());
}
#[test]
fn composite_expression_zoom_and_property() {
let mut props = HashMap::new();
props.insert("rank".to_string(), PropertyValue::Number(5.0));
let ctx = ExprEvalContext::with_feature(10.0, &props);
let expr: Expression<f32> = Expression::Case {
branches: vec![(
BoolExpression::Gte(
NumericExpression::GetProperty {
key: "rank".to_string(),
fallback: 0.0,
},
NumericExpression::Literal(3.0),
),
20.0, )],
fallback: 10.0, };
assert!((expr.eval_full(&ctx) - 20.0).abs() < f32::EPSILON);
}
}