1use crate::geometry::PropertyValue;
28use crate::query::FeatureState;
29use std::collections::HashMap;
30use std::fmt;
31
32pub type FeatureProperties = HashMap<String, PropertyValue>;
41
42#[derive(Debug, Clone, Copy)]
47pub struct ExprEvalContext<'a> {
48 pub zoom: f32,
50 pub pitch: f32,
52 pub properties: Option<&'a FeatureProperties>,
56 pub feature_state: Option<&'a FeatureState>,
60}
61
62impl<'a> ExprEvalContext<'a> {
63 pub fn zoom_only(zoom: f32) -> Self {
65 Self {
66 zoom,
67 pitch: 0.0,
68 properties: None,
69 feature_state: None,
70 }
71 }
72
73 pub fn with_feature(zoom: f32, properties: &'a FeatureProperties) -> Self {
75 Self {
76 zoom,
77 pitch: 0.0,
78 properties: Some(properties),
79 feature_state: None,
80 }
81 }
82
83 pub fn and_state(mut self, state: &'a FeatureState) -> Self {
85 self.feature_state = Some(state);
86 self
87 }
88
89 pub fn and_pitch(mut self, pitch: f32) -> Self {
91 self.pitch = pitch;
92 self
93 }
94
95 pub fn get_property(&self, key: &str) -> Option<&PropertyValue> {
97 self.properties.and_then(|p| p.get(key))
98 }
99
100 pub fn get_state(&self, key: &str) -> Option<&PropertyValue> {
102 self.feature_state.and_then(|s| s.get(key))
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
122pub enum Expression<T> {
123 Constant(T),
129
130 ZoomStops(Vec<(f32, T)>),
132
133 FeatureState {
135 key: String,
137 fallback: T,
139 },
140
141 GetProperty {
151 key: String,
153 fallback: T,
155 },
156
157 Interpolate {
161 input: Box<NumericExpression>,
163 stops: Vec<(f32, T)>,
165 },
166
167 Step {
171 input: Box<NumericExpression>,
173 default: T,
175 stops: Vec<(f32, T)>,
177 },
178
179 Match {
183 input: Box<StringExpression>,
185 cases: Vec<(String, T)>,
187 fallback: T,
189 },
190
191 Case {
195 branches: Vec<(BoolExpression, T)>,
197 fallback: T,
199 },
200
201 Coalesce(Vec<Expression<T>>),
205}
206
207#[derive(Debug, Clone, PartialEq)]
216pub enum NumericExpression {
217 Literal(f64),
219 Zoom,
221 Pitch,
223 GetProperty {
225 key: String,
227 fallback: f64,
229 },
230 GetState {
232 key: String,
234 fallback: f64,
236 },
237 Add(Box<NumericExpression>, Box<NumericExpression>),
239 Sub(Box<NumericExpression>, Box<NumericExpression>),
241 Mul(Box<NumericExpression>, Box<NumericExpression>),
243 Div(Box<NumericExpression>, Box<NumericExpression>),
245 Mod(Box<NumericExpression>, Box<NumericExpression>),
247 Pow(Box<NumericExpression>, Box<NumericExpression>),
249 Abs(Box<NumericExpression>),
251 Ln(Box<NumericExpression>),
253 Sqrt(Box<NumericExpression>),
255 Min(Box<NumericExpression>, Box<NumericExpression>),
257 Max(Box<NumericExpression>, Box<NumericExpression>),
259}
260
261#[derive(Debug, Clone, PartialEq)]
269pub enum StringExpression {
270 Literal(String),
272 GetProperty {
274 key: String,
276 fallback: String,
278 },
279 GetState {
281 key: String,
283 fallback: String,
285 },
286 Concat(Box<StringExpression>, Box<StringExpression>),
288 Upcase(Box<StringExpression>),
290 Downcase(Box<StringExpression>),
292}
293
294#[derive(Debug, Clone, PartialEq)]
302pub enum BoolExpression {
303 Literal(bool),
305 GetProperty {
307 key: String,
309 fallback: bool,
311 },
312 GetState {
314 key: String,
316 fallback: bool,
318 },
319 Has(String),
321 Not(Box<BoolExpression>),
323 All(Vec<BoolExpression>),
325 Any(Vec<BoolExpression>),
327 Eq(NumericExpression, NumericExpression),
329 Neq(NumericExpression, NumericExpression),
331 Gt(NumericExpression, NumericExpression),
333 Gte(NumericExpression, NumericExpression),
335 Lt(NumericExpression, NumericExpression),
337 Lte(NumericExpression, NumericExpression),
339 StrEq(StringExpression, StringExpression),
341}
342
343impl NumericExpression {
348 pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> f64 {
350 match self {
351 NumericExpression::Literal(v) => *v,
352 NumericExpression::Zoom => ctx.zoom as f64,
353 NumericExpression::Pitch => ctx.pitch as f64,
354 NumericExpression::GetProperty { key, fallback } => {
355 ctx.get_property(key)
356 .and_then(PropertyValue::as_f64)
357 .unwrap_or(*fallback)
358 }
359 NumericExpression::GetState { key, fallback } => {
360 ctx.get_state(key)
361 .and_then(PropertyValue::as_f64)
362 .unwrap_or(*fallback)
363 }
364 NumericExpression::Add(a, b) => a.eval(ctx) + b.eval(ctx),
365 NumericExpression::Sub(a, b) => a.eval(ctx) - b.eval(ctx),
366 NumericExpression::Mul(a, b) => a.eval(ctx) * b.eval(ctx),
367 NumericExpression::Div(a, b) => {
368 let denom = b.eval(ctx);
369 if denom.abs() < f64::EPSILON { 0.0 } else { a.eval(ctx) / denom }
370 }
371 NumericExpression::Mod(a, b) => {
372 let denom = b.eval(ctx);
373 if denom.abs() < f64::EPSILON { 0.0 } else { a.eval(ctx) % denom }
374 }
375 NumericExpression::Pow(a, b) => a.eval(ctx).powf(b.eval(ctx)),
376 NumericExpression::Abs(a) => a.eval(ctx).abs(),
377 NumericExpression::Ln(a) => a.eval(ctx).ln(),
378 NumericExpression::Sqrt(a) => a.eval(ctx).sqrt(),
379 NumericExpression::Min(a, b) => a.eval(ctx).min(b.eval(ctx)),
380 NumericExpression::Max(a, b) => a.eval(ctx).max(b.eval(ctx)),
381 }
382 }
383}
384
385impl StringExpression {
390 pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> String {
392 match self {
393 StringExpression::Literal(v) => v.clone(),
394 StringExpression::GetProperty { key, fallback } => {
395 ctx.get_property(key)
396 .and_then(PropertyValue::as_str)
397 .map(|s| s.to_owned())
398 .unwrap_or_else(|| fallback.clone())
399 }
400 StringExpression::GetState { key, fallback } => {
401 ctx.get_state(key)
402 .and_then(PropertyValue::as_str)
403 .map(|s| s.to_owned())
404 .unwrap_or_else(|| fallback.clone())
405 }
406 StringExpression::Concat(a, b) => {
407 let mut s = a.eval(ctx);
408 s.push_str(&b.eval(ctx));
409 s
410 }
411 StringExpression::Upcase(a) => a.eval(ctx).to_uppercase(),
412 StringExpression::Downcase(a) => a.eval(ctx).to_lowercase(),
413 }
414 }
415}
416
417impl BoolExpression {
422 pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> bool {
424 match self {
425 BoolExpression::Literal(v) => *v,
426 BoolExpression::GetProperty { key, fallback } => {
427 ctx.get_property(key)
428 .and_then(PropertyValue::as_bool)
429 .unwrap_or(*fallback)
430 }
431 BoolExpression::GetState { key, fallback } => {
432 ctx.get_state(key)
433 .and_then(PropertyValue::as_bool)
434 .unwrap_or(*fallback)
435 }
436 BoolExpression::Has(key) => {
437 ctx.properties
438 .map(|p| p.contains_key(key.as_str()))
439 .unwrap_or(false)
440 }
441 BoolExpression::Not(a) => !a.eval(ctx),
442 BoolExpression::All(exprs) => exprs.iter().all(|e| e.eval(ctx)),
443 BoolExpression::Any(exprs) => exprs.iter().any(|e| e.eval(ctx)),
444 BoolExpression::Eq(a, b) => (a.eval(ctx) - b.eval(ctx)).abs() < f64::EPSILON,
445 BoolExpression::Neq(a, b) => (a.eval(ctx) - b.eval(ctx)).abs() >= f64::EPSILON,
446 BoolExpression::Gt(a, b) => a.eval(ctx) > b.eval(ctx),
447 BoolExpression::Gte(a, b) => a.eval(ctx) >= b.eval(ctx),
448 BoolExpression::Lt(a, b) => a.eval(ctx) < b.eval(ctx),
449 BoolExpression::Lte(a, b) => a.eval(ctx) <= b.eval(ctx),
450 BoolExpression::StrEq(a, b) => a.eval(ctx) == b.eval(ctx),
451 }
452 }
453}
454
455fn eval_stops<T: super::style::StyleInterpolatable>(stops: &[(f32, T)], input: f32) -> T {
461 debug_assert!(!stops.is_empty(), "stop list must not be empty");
462 let (first_input, first_value) = &stops[0];
463 if input <= *first_input {
464 return first_value.clone();
465 }
466 for pair in stops.windows(2) {
467 let (i0, v0) = &pair[0];
468 let (i1, v1) = &pair[1];
469 if input <= *i1 {
470 let span = (*i1 - *i0).max(f32::EPSILON);
471 let t = (input - *i0) / span;
472 return T::interpolate(v0, v1, t);
473 }
474 }
475 stops.last().expect("non-empty stops").1.clone()
476}
477
478impl<T: super::style::StyleInterpolatable> Expression<T> {
479 pub fn evaluate(&self) -> T {
481 self.eval_full(&ExprEvalContext::zoom_only(0.0))
482 }
483
484 pub fn evaluate_with_context(&self, ctx: super::style::StyleEvalContext) -> T {
486 self.eval_full(&ExprEvalContext::zoom_only(ctx.zoom))
487 }
488
489 pub fn evaluate_with_full_context(&self, ctx: &super::style::StyleEvalContextFull<'_>) -> T {
491 let expr_ctx = ExprEvalContext {
492 zoom: ctx.zoom,
493 pitch: 0.0,
494 properties: None,
495 feature_state: Some(ctx.feature_state),
496 };
497 self.eval_full(&expr_ctx)
498 }
499
500 pub fn evaluate_with_properties(&self, ctx: &ExprEvalContext<'_>) -> T {
502 self.eval_full(ctx)
503 }
504
505 pub fn eval_full(&self, ctx: &ExprEvalContext<'_>) -> T {
507 match self {
508 Expression::Constant(value) => value.clone(),
510
511 Expression::ZoomStops(stops) => eval_stops(stops, ctx.zoom),
512
513 Expression::FeatureState { key, fallback } => {
514 ctx.get_state(key)
515 .and_then(|prop| T::from_feature_state_property(prop))
516 .unwrap_or_else(|| fallback.clone())
517 }
518
519 Expression::GetProperty { key, fallback } => {
521 ctx.get_property(key)
522 .and_then(|prop| T::from_feature_state_property(prop))
523 .unwrap_or_else(|| fallback.clone())
524 }
525
526 Expression::Interpolate { input, stops } => {
527 let input_val = input.eval(ctx) as f32;
528 eval_stops(stops, input_val)
529 }
530
531 Expression::Step { input, default, stops } => {
532 let input_val = input.eval(ctx) as f32;
533 if stops.is_empty() || input_val < stops[0].0 {
534 return default.clone();
535 }
536 let mut result = default;
538 for (threshold, value) in stops {
539 if input_val >= *threshold {
540 result = value;
541 } else {
542 break;
543 }
544 }
545 result.clone()
546 }
547
548 Expression::Match { input, cases, fallback } => {
549 let input_val = input.eval(ctx);
550 for (label, value) in cases {
551 if *label == input_val {
552 return value.clone();
553 }
554 }
555 fallback.clone()
556 }
557
558 Expression::Case { branches, fallback } => {
559 for (condition, value) in branches {
560 if condition.eval(ctx) {
561 return value.clone();
562 }
563 }
564 fallback.clone()
565 }
566
567 Expression::Coalesce(exprs) => {
568 if let Some(first) = exprs.first() {
574 first.eval_full(ctx)
575 } else {
576 panic!("Expression::Coalesce requires at least one sub-expression");
579 }
580 }
581 }
582 }
583}
584
585impl<T> Expression<T> {
590 pub fn feature_state_key(key: impl Into<String>, fallback: T) -> Self {
592 Expression::FeatureState {
593 key: key.into(),
594 fallback,
595 }
596 }
597
598 pub fn is_feature_state_driven(&self) -> bool {
600 match self {
601 Expression::FeatureState { .. } => true,
602 Expression::Case { branches, .. } => {
603 branches.iter().any(|(cond, _)| cond.uses_feature_state())
604 }
605 Expression::Coalesce(exprs) => exprs.iter().any(|e| e.is_feature_state_driven()),
606 _ => false,
607 }
608 }
609
610 pub fn is_data_driven(&self) -> bool {
612 match self {
613 Expression::GetProperty { .. } => true,
614 Expression::Match { .. } => true,
615 Expression::Interpolate { .. } => true,
616 Expression::Step { .. } => true,
617 Expression::Case { .. } => true,
618 Expression::Coalesce(exprs) => exprs.iter().any(|e| e.is_data_driven()),
619 _ => false,
620 }
621 }
622}
623
624impl<T> From<T> for Expression<T> {
625 fn from(value: T) -> Self {
626 Expression::Constant(value)
627 }
628}
629
630impl BoolExpression {
631 pub fn uses_feature_state(&self) -> bool {
633 match self {
634 BoolExpression::GetState { .. } => true,
635 BoolExpression::Not(a) => a.uses_feature_state(),
636 BoolExpression::All(exprs) => exprs.iter().any(|e| e.uses_feature_state()),
637 BoolExpression::Any(exprs) => exprs.iter().any(|e| e.uses_feature_state()),
638 _ => false,
639 }
640 }
641}
642
643impl<T: fmt::Debug> fmt::Display for Expression<T> {
648 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
649 match self {
650 Expression::Constant(v) => write!(f, "{v:?}"),
651 Expression::ZoomStops(stops) => {
652 write!(f, "zoom_stops[")?;
653 for (i, (z, v)) in stops.iter().enumerate() {
654 if i > 0 { write!(f, ", ")?; }
655 write!(f, "{z}: {v:?}")?;
656 }
657 write!(f, "]")
658 }
659 Expression::FeatureState { key, fallback } => {
660 write!(f, "feature_state(\"{key}\", {fallback:?})")
661 }
662 Expression::GetProperty { key, fallback } => {
663 write!(f, "get(\"{key}\", {fallback:?})")
664 }
665 Expression::Interpolate { input, stops } => {
666 write!(f, "interpolate({input:?}, [")?;
667 for (i, (z, v)) in stops.iter().enumerate() {
668 if i > 0 { write!(f, ", ")?; }
669 write!(f, "{z}: {v:?}")?;
670 }
671 write!(f, "])")
672 }
673 Expression::Step { input, default, stops } => {
674 write!(f, "step({input:?}, {default:?}, [")?;
675 for (i, (z, v)) in stops.iter().enumerate() {
676 if i > 0 { write!(f, ", ")?; }
677 write!(f, "{z}: {v:?}")?;
678 }
679 write!(f, "])")
680 }
681 Expression::Match { input, cases, fallback } => {
682 write!(f, "match({input:?}, [")?;
683 for (i, (lbl, v)) in cases.iter().enumerate() {
684 if i > 0 { write!(f, ", ")?; }
685 write!(f, "\"{lbl}\": {v:?}")?;
686 }
687 write!(f, "], {fallback:?})")
688 }
689 Expression::Case { branches, fallback } => {
690 write!(f, "case([")?;
691 for (i, (cond, v)) in branches.iter().enumerate() {
692 if i > 0 { write!(f, ", ")?; }
693 write!(f, "{cond:?} => {v:?}")?;
694 }
695 write!(f, "], {fallback:?})")
696 }
697 Expression::Coalesce(exprs) => {
698 write!(f, "coalesce(")?;
699 for (i, e) in exprs.iter().enumerate() {
700 if i > 0 { write!(f, ", ")?; }
701 write!(f, "{e}")?;
702 }
703 write!(f, ")")
704 }
705 }
706 }
707}
708
709impl Expression<f32> {
714 pub fn zoom_interpolate(stops: Vec<(f32, f32)>) -> Self {
716 Expression::Interpolate {
717 input: Box::new(NumericExpression::Zoom),
718 stops,
719 }
720 }
721
722 pub fn zoom_step(default: f32, stops: Vec<(f32, f32)>) -> Self {
724 Expression::Step {
725 input: Box::new(NumericExpression::Zoom),
726 default,
727 stops,
728 }
729 }
730
731 pub fn property(key: impl Into<String>, fallback: f32) -> Self {
733 Expression::GetProperty {
734 key: key.into(),
735 fallback,
736 }
737 }
738
739 pub fn property_interpolate(
741 property: impl Into<String>,
742 fallback: f64,
743 stops: Vec<(f32, f32)>,
744 ) -> Self {
745 Expression::Interpolate {
746 input: Box::new(NumericExpression::GetProperty {
747 key: property.into(),
748 fallback,
749 }),
750 stops,
751 }
752 }
753}
754
755impl Expression<[f32; 4]> {
756 pub fn zoom_interpolate(stops: Vec<(f32, [f32; 4])>) -> Self {
758 Expression::Interpolate {
759 input: Box::new(NumericExpression::Zoom),
760 stops,
761 }
762 }
763
764 pub fn zoom_step(default: [f32; 4], stops: Vec<(f32, [f32; 4])>) -> Self {
766 Expression::Step {
767 input: Box::new(NumericExpression::Zoom),
768 default,
769 stops,
770 }
771 }
772
773 pub fn property_match(
775 property: impl Into<String>,
776 cases: Vec<(String, [f32; 4])>,
777 fallback: [f32; 4],
778 ) -> Self {
779 Expression::Match {
780 input: Box::new(StringExpression::GetProperty {
781 key: property.into(),
782 fallback: String::new(),
783 }),
784 cases,
785 fallback,
786 }
787 }
788}
789
790impl Expression<bool> {
791 pub fn property(key: impl Into<String>, fallback: bool) -> Self {
793 Expression::GetProperty {
794 key: key.into(),
795 fallback,
796 }
797 }
798}
799
800impl Expression<String> {
801 pub fn property(key: impl Into<String>, fallback: impl Into<String>) -> Self {
803 Expression::GetProperty {
804 key: key.into(),
805 fallback: fallback.into(),
806 }
807 }
808}
809
810#[cfg(test)]
815mod tests {
816 use super::*;
817 use crate::geometry::PropertyValue;
818 use crate::style::{StyleEvalContext, StyleEvalContextFull};
819
820 #[test]
823 fn constant_evaluates_directly() {
824 let expr: Expression<f32> = Expression::Constant(42.0);
825 assert!((expr.evaluate() - 42.0).abs() < f32::EPSILON);
826 }
827
828 #[test]
829 fn constant_via_into() {
830 let expr: Expression<f32> = 42.0.into();
831 assert!((expr.evaluate() - 42.0).abs() < f32::EPSILON);
832 }
833
834 #[test]
837 fn zoom_stops_interpolates() {
838 let expr = Expression::ZoomStops(vec![
839 (0.0, 0.0_f32),
840 (10.0, 100.0),
841 ]);
842 let ctx = ExprEvalContext::zoom_only(5.0);
843 let result = expr.eval_full(&ctx);
844 assert!((result - 50.0).abs() < 0.1);
845 }
846
847 #[test]
848 fn zoom_stops_clamps_below() {
849 let expr = Expression::ZoomStops(vec![(5.0, 10.0_f32), (10.0, 20.0)]);
850 let ctx = ExprEvalContext::zoom_only(0.0);
851 assert!((expr.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
852 }
853
854 #[test]
855 fn zoom_stops_clamps_above() {
856 let expr = Expression::ZoomStops(vec![(5.0, 10.0_f32), (10.0, 20.0)]);
857 let ctx = ExprEvalContext::zoom_only(99.0);
858 assert!((expr.eval_full(&ctx) - 20.0).abs() < f32::EPSILON);
859 }
860
861 #[test]
864 fn feature_state_returns_fallback_without_state() {
865 let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
866 let ctx = ExprEvalContext::zoom_only(10.0);
867 assert!((expr.eval_full(&ctx) - 0.5).abs() < f32::EPSILON);
868 }
869
870 #[test]
871 fn feature_state_resolves_from_state_map() {
872 let mut state = HashMap::new();
873 state.insert("opacity".to_string(), PropertyValue::Number(0.8));
874 let ctx = ExprEvalContext::zoom_only(10.0).and_state(&state);
875 let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
876 assert!((expr.eval_full(&ctx) - 0.8).abs() < f32::EPSILON);
877 }
878
879 #[test]
882 fn legacy_evaluate_with_context() {
883 let expr = Expression::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
884 let result = expr.evaluate_with_context(StyleEvalContext::new(5.0));
885 assert!((result - 50.0).abs() < 0.1);
886 }
887
888 #[test]
889 fn legacy_evaluate_with_full_context() {
890 let mut state = HashMap::new();
891 state.insert("opacity".to_string(), PropertyValue::Number(0.8));
892 let ctx = StyleEvalContextFull::new(10.0, &state);
893 let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
894 assert!((expr.evaluate_with_full_context(&ctx) - 0.8).abs() < f32::EPSILON);
895 }
896
897 #[test]
900 fn get_property_reads_feature_property() {
901 let mut props = HashMap::new();
902 props.insert("height".to_string(), PropertyValue::Number(50.0));
903 let ctx = ExprEvalContext::with_feature(10.0, &props);
904
905 let expr = Expression::<f32>::property("height", 0.0);
906 assert!((expr.eval_full(&ctx) - 50.0).abs() < f32::EPSILON);
907 }
908
909 #[test]
910 fn get_property_returns_fallback_when_missing() {
911 let props = HashMap::new();
912 let ctx = ExprEvalContext::with_feature(10.0, &props);
913 let expr = Expression::<f32>::property("height", 10.0);
914 assert!((expr.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
915 }
916
917 #[test]
920 fn interpolate_on_property() {
921 let mut props = HashMap::new();
922 props.insert("population".to_string(), PropertyValue::Number(500.0));
923 let ctx = ExprEvalContext::with_feature(10.0, &props);
924
925 let expr = Expression::<f32>::property_interpolate(
926 "population",
927 0.0,
928 vec![(0.0, 2.0), (1000.0, 20.0)],
929 );
930 let result = expr.eval_full(&ctx);
931 assert!((result - 11.0).abs() < 0.1);
932 }
933
934 #[test]
937 fn zoom_interpolate_convenience() {
938 let expr = Expression::<f32>::zoom_interpolate(vec![(0.0, 1.0), (20.0, 10.0)]);
939 let ctx = ExprEvalContext::zoom_only(10.0);
940 assert!((expr.eval_full(&ctx) - 5.5).abs() < 0.1);
941 }
942
943 #[test]
946 fn step_below_first_returns_default() {
947 let expr = Expression::Step {
948 input: Box::new(NumericExpression::Zoom),
949 default: 1.0_f32,
950 stops: vec![(5.0, 2.0), (10.0, 3.0)],
951 };
952 let ctx = ExprEvalContext::zoom_only(3.0);
953 assert!((expr.eval_full(&ctx) - 1.0).abs() < f32::EPSILON);
954 }
955
956 #[test]
957 fn step_between_stops() {
958 let expr = Expression::Step {
959 input: Box::new(NumericExpression::Zoom),
960 default: 1.0_f32,
961 stops: vec![(5.0, 2.0), (10.0, 3.0)],
962 };
963 let ctx = ExprEvalContext::zoom_only(7.0);
964 assert!((expr.eval_full(&ctx) - 2.0).abs() < f32::EPSILON);
965 }
966
967 #[test]
968 fn step_above_last() {
969 let expr = Expression::Step {
970 input: Box::new(NumericExpression::Zoom),
971 default: 1.0_f32,
972 stops: vec![(5.0, 2.0), (10.0, 3.0)],
973 };
974 let ctx = ExprEvalContext::zoom_only(15.0);
975 assert!((expr.eval_full(&ctx) - 3.0).abs() < f32::EPSILON);
976 }
977
978 #[test]
981 fn match_on_string_property() {
982 let mut props = HashMap::new();
983 props.insert("type".to_string(), PropertyValue::String("residential".to_string()));
984 let ctx = ExprEvalContext::with_feature(10.0, &props);
985
986 let expr: Expression<[f32; 4]> = Expression::property_match(
987 "type",
988 vec![
989 ("residential".to_string(), [0.0, 0.0, 1.0, 1.0]),
990 ("commercial".to_string(), [1.0, 0.0, 0.0, 1.0]),
991 ],
992 [0.5, 0.5, 0.5, 1.0],
993 );
994 let result = expr.eval_full(&ctx);
995 assert_eq!(result, [0.0, 0.0, 1.0, 1.0]);
996 }
997
998 #[test]
999 fn match_returns_fallback_when_no_case() {
1000 let mut props = HashMap::new();
1001 props.insert("type".to_string(), PropertyValue::String("industrial".to_string()));
1002 let ctx = ExprEvalContext::with_feature(10.0, &props);
1003
1004 let expr: Expression<[f32; 4]> = Expression::property_match(
1005 "type",
1006 vec![("residential".to_string(), [0.0, 0.0, 1.0, 1.0])],
1007 [0.5, 0.5, 0.5, 1.0],
1008 );
1009 assert_eq!(expr.eval_full(&ctx), [0.5, 0.5, 0.5, 1.0]);
1010 }
1011
1012 #[test]
1015 fn case_with_bool_conditions() {
1016 let mut props = HashMap::new();
1017 props.insert("height".to_string(), PropertyValue::Number(150.0));
1018 let ctx = ExprEvalContext::with_feature(10.0, &props);
1019
1020 let expr: Expression<[f32; 4]> = Expression::Case {
1021 branches: vec![
1022 (
1023 BoolExpression::Gt(
1024 NumericExpression::GetProperty { key: "height".to_string(), fallback: 0.0 },
1025 NumericExpression::Literal(100.0),
1026 ),
1027 [1.0, 0.0, 0.0, 1.0], ),
1029 (
1030 BoolExpression::Gt(
1031 NumericExpression::GetProperty { key: "height".to_string(), fallback: 0.0 },
1032 NumericExpression::Literal(50.0),
1033 ),
1034 [1.0, 1.0, 0.0, 1.0], ),
1036 ],
1037 fallback: [0.0, 1.0, 0.0, 1.0], };
1039 assert_eq!(expr.eval_full(&ctx), [1.0, 0.0, 0.0, 1.0]);
1040 }
1041
1042 #[test]
1043 fn case_fallback_when_no_branch_matches() {
1044 let props = HashMap::new();
1045 let ctx = ExprEvalContext::with_feature(10.0, &props);
1046
1047 let expr: Expression<f32> = Expression::Case {
1048 branches: vec![
1049 (BoolExpression::Literal(false), 10.0),
1050 (BoolExpression::Literal(false), 20.0),
1051 ],
1052 fallback: 99.0,
1053 };
1054 assert!((expr.eval_full(&ctx) - 99.0).abs() < f32::EPSILON);
1055 }
1056
1057 #[test]
1060 fn numeric_arithmetic() {
1061 let ctx = ExprEvalContext::zoom_only(10.0);
1062
1063 let add = NumericExpression::Add(
1064 Box::new(NumericExpression::Literal(3.0)),
1065 Box::new(NumericExpression::Literal(4.0)),
1066 );
1067 assert!((add.eval(&ctx) - 7.0).abs() < f64::EPSILON);
1068
1069 let mul = NumericExpression::Mul(
1070 Box::new(NumericExpression::Zoom),
1071 Box::new(NumericExpression::Literal(2.0)),
1072 );
1073 assert!((mul.eval(&ctx) - 20.0).abs() < f64::EPSILON);
1074 }
1075
1076 #[test]
1077 fn numeric_division_by_zero() {
1078 let ctx = ExprEvalContext::zoom_only(10.0);
1079 let div = NumericExpression::Div(
1080 Box::new(NumericExpression::Literal(10.0)),
1081 Box::new(NumericExpression::Literal(0.0)),
1082 );
1083 assert!((div.eval(&ctx) - 0.0).abs() < f64::EPSILON);
1084 }
1085
1086 #[test]
1089 fn bool_has_checks_property_existence() {
1090 let mut props = HashMap::new();
1091 props.insert("name".to_string(), PropertyValue::String("test".to_string()));
1092 let ctx = ExprEvalContext::with_feature(10.0, &props);
1093
1094 assert!(BoolExpression::Has("name".to_string()).eval(&ctx));
1095 assert!(!BoolExpression::Has("missing".to_string()).eval(&ctx));
1096 }
1097
1098 #[test]
1099 fn bool_all_and_any() {
1100 let ctx = ExprEvalContext::zoom_only(10.0);
1101
1102 assert!(BoolExpression::All(vec![
1103 BoolExpression::Literal(true),
1104 BoolExpression::Literal(true),
1105 ]).eval(&ctx));
1106
1107 assert!(!BoolExpression::All(vec![
1108 BoolExpression::Literal(true),
1109 BoolExpression::Literal(false),
1110 ]).eval(&ctx));
1111
1112 assert!(BoolExpression::Any(vec![
1113 BoolExpression::Literal(false),
1114 BoolExpression::Literal(true),
1115 ]).eval(&ctx));
1116 }
1117
1118 #[test]
1121 fn string_concat() {
1122 let ctx = ExprEvalContext::zoom_only(10.0);
1123 let concat = StringExpression::Concat(
1124 Box::new(StringExpression::Literal("hello ".to_string())),
1125 Box::new(StringExpression::Literal("world".to_string())),
1126 );
1127 assert_eq!(concat.eval(&ctx), "hello world");
1128 }
1129
1130 #[test]
1131 fn string_upcase_downcase() {
1132 let ctx = ExprEvalContext::zoom_only(10.0);
1133 let up = StringExpression::Upcase(
1134 Box::new(StringExpression::Literal("hello".to_string())),
1135 );
1136 assert_eq!(up.eval(&ctx), "HELLO");
1137
1138 let down = StringExpression::Downcase(
1139 Box::new(StringExpression::Literal("HELLO".to_string())),
1140 );
1141 assert_eq!(down.eval(&ctx), "hello");
1142 }
1143
1144 #[test]
1147 fn is_data_driven_flags() {
1148 let constant: Expression<f32> = Expression::Constant(1.0);
1149 assert!(!constant.is_data_driven());
1150
1151 let get: Expression<f32> = Expression::GetProperty { key: "height".into(), fallback: 0.0 };
1152 assert!(get.is_data_driven());
1153
1154 let interp = Expression::<f32>::zoom_interpolate(vec![(0.0, 1.0), (10.0, 5.0)]);
1155 assert!(interp.is_data_driven()); }
1157
1158 #[test]
1159 fn is_feature_state_driven_flags() {
1160 let constant: Expression<f32> = Expression::Constant(1.0);
1161 assert!(!constant.is_feature_state_driven());
1162
1163 let driven: Expression<f32> = Expression::feature_state_key("opacity", 1.0);
1164 assert!(driven.is_feature_state_driven());
1165 }
1166
1167 #[test]
1170 fn composite_expression_zoom_and_property() {
1171 let mut props = HashMap::new();
1174 props.insert("rank".to_string(), PropertyValue::Number(5.0));
1175 let ctx = ExprEvalContext::with_feature(10.0, &props);
1176
1177 let expr: Expression<f32> = Expression::Case {
1180 branches: vec![(
1181 BoolExpression::Gte(
1182 NumericExpression::GetProperty { key: "rank".to_string(), fallback: 0.0 },
1183 NumericExpression::Literal(3.0),
1184 ),
1185 20.0, )],
1187 fallback: 10.0, };
1189 assert!((expr.eval_full(&ctx) - 20.0).abs() < f32::EPSILON);
1190 }
1191}