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),
128
129 ZoomStops(Vec<(f32, T)>),
131
132 FeatureState {
134 key: String,
136 fallback: T,
138 },
139
140 GetProperty {
149 key: String,
151 fallback: T,
153 },
154
155 Interpolate {
159 input: Box<NumericExpression>,
161 stops: Vec<(f32, T)>,
163 },
164
165 Step {
169 input: Box<NumericExpression>,
171 default: T,
173 stops: Vec<(f32, T)>,
175 },
176
177 Match {
181 input: Box<StringExpression>,
183 cases: Vec<(String, T)>,
185 fallback: T,
187 },
188
189 Case {
193 branches: Vec<(BoolExpression, T)>,
195 fallback: T,
197 },
198
199 Coalesce(Vec<Expression<T>>),
203}
204
205#[derive(Debug, Clone, PartialEq)]
214pub enum NumericExpression {
215 Literal(f64),
217 Zoom,
219 Pitch,
221 GetProperty {
223 key: String,
225 fallback: f64,
227 },
228 GetState {
230 key: String,
232 fallback: f64,
234 },
235 Add(Box<NumericExpression>, Box<NumericExpression>),
237 Sub(Box<NumericExpression>, Box<NumericExpression>),
239 Mul(Box<NumericExpression>, Box<NumericExpression>),
241 Div(Box<NumericExpression>, Box<NumericExpression>),
243 Mod(Box<NumericExpression>, Box<NumericExpression>),
245 Pow(Box<NumericExpression>, Box<NumericExpression>),
247 Abs(Box<NumericExpression>),
249 Ln(Box<NumericExpression>),
251 Sqrt(Box<NumericExpression>),
253 Min(Box<NumericExpression>, Box<NumericExpression>),
255 Max(Box<NumericExpression>, Box<NumericExpression>),
257}
258
259#[derive(Debug, Clone, PartialEq)]
267pub enum StringExpression {
268 Literal(String),
270 GetProperty {
272 key: String,
274 fallback: String,
276 },
277 GetState {
279 key: String,
281 fallback: String,
283 },
284 Concat(Box<StringExpression>, Box<StringExpression>),
286 Upcase(Box<StringExpression>),
288 Downcase(Box<StringExpression>),
290}
291
292#[derive(Debug, Clone, PartialEq)]
300pub enum BoolExpression {
301 Literal(bool),
303 GetProperty {
305 key: String,
307 fallback: bool,
309 },
310 GetState {
312 key: String,
314 fallback: bool,
316 },
317 Has(String),
319 Not(Box<BoolExpression>),
321 All(Vec<BoolExpression>),
323 Any(Vec<BoolExpression>),
325 Eq(NumericExpression, NumericExpression),
327 Neq(NumericExpression, NumericExpression),
329 Gt(NumericExpression, NumericExpression),
331 Gte(NumericExpression, NumericExpression),
333 Lt(NumericExpression, NumericExpression),
335 Lte(NumericExpression, NumericExpression),
337 StrEq(StringExpression, StringExpression),
339}
340
341impl NumericExpression {
346 pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> f64 {
348 match self {
349 NumericExpression::Literal(v) => *v,
350 NumericExpression::Zoom => ctx.zoom as f64,
351 NumericExpression::Pitch => ctx.pitch as f64,
352 NumericExpression::GetProperty { key, fallback } => ctx
353 .get_property(key)
354 .and_then(PropertyValue::as_f64)
355 .unwrap_or(*fallback),
356 NumericExpression::GetState { key, fallback } => ctx
357 .get_state(key)
358 .and_then(PropertyValue::as_f64)
359 .unwrap_or(*fallback),
360 NumericExpression::Add(a, b) => a.eval(ctx) + b.eval(ctx),
361 NumericExpression::Sub(a, b) => a.eval(ctx) - b.eval(ctx),
362 NumericExpression::Mul(a, b) => a.eval(ctx) * b.eval(ctx),
363 NumericExpression::Div(a, b) => {
364 let denom = b.eval(ctx);
365 if denom.abs() < f64::EPSILON {
366 0.0
367 } else {
368 a.eval(ctx) / denom
369 }
370 }
371 NumericExpression::Mod(a, b) => {
372 let denom = b.eval(ctx);
373 if denom.abs() < f64::EPSILON {
374 0.0
375 } else {
376 a.eval(ctx) % denom
377 }
378 }
379 NumericExpression::Pow(a, b) => a.eval(ctx).powf(b.eval(ctx)),
380 NumericExpression::Abs(a) => a.eval(ctx).abs(),
381 NumericExpression::Ln(a) => a.eval(ctx).ln(),
382 NumericExpression::Sqrt(a) => a.eval(ctx).sqrt(),
383 NumericExpression::Min(a, b) => a.eval(ctx).min(b.eval(ctx)),
384 NumericExpression::Max(a, b) => a.eval(ctx).max(b.eval(ctx)),
385 }
386 }
387}
388
389impl StringExpression {
394 pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> String {
396 match self {
397 StringExpression::Literal(v) => v.clone(),
398 StringExpression::GetProperty { key, fallback } => ctx
399 .get_property(key)
400 .and_then(PropertyValue::as_str)
401 .map(|s| s.to_owned())
402 .unwrap_or_else(|| fallback.clone()),
403 StringExpression::GetState { key, fallback } => ctx
404 .get_state(key)
405 .and_then(PropertyValue::as_str)
406 .map(|s| s.to_owned())
407 .unwrap_or_else(|| fallback.clone()),
408 StringExpression::Concat(a, b) => {
409 let mut s = a.eval(ctx);
410 s.push_str(&b.eval(ctx));
411 s
412 }
413 StringExpression::Upcase(a) => a.eval(ctx).to_uppercase(),
414 StringExpression::Downcase(a) => a.eval(ctx).to_lowercase(),
415 }
416 }
417}
418
419impl BoolExpression {
424 pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> bool {
426 match self {
427 BoolExpression::Literal(v) => *v,
428 BoolExpression::GetProperty { key, fallback } => ctx
429 .get_property(key)
430 .and_then(PropertyValue::as_bool)
431 .unwrap_or(*fallback),
432 BoolExpression::GetState { key, fallback } => ctx
433 .get_state(key)
434 .and_then(PropertyValue::as_bool)
435 .unwrap_or(*fallback),
436 BoolExpression::Has(key) => ctx
437 .properties
438 .map(|p| p.contains_key(key.as_str()))
439 .unwrap_or(false),
440 BoolExpression::Not(a) => !a.eval(ctx),
441 BoolExpression::All(exprs) => exprs.iter().all(|e| e.eval(ctx)),
442 BoolExpression::Any(exprs) => exprs.iter().any(|e| e.eval(ctx)),
443 BoolExpression::Eq(a, b) => (a.eval(ctx) - b.eval(ctx)).abs() < f64::EPSILON,
444 BoolExpression::Neq(a, b) => (a.eval(ctx) - b.eval(ctx)).abs() >= f64::EPSILON,
445 BoolExpression::Gt(a, b) => a.eval(ctx) > b.eval(ctx),
446 BoolExpression::Gte(a, b) => a.eval(ctx) >= b.eval(ctx),
447 BoolExpression::Lt(a, b) => a.eval(ctx) < b.eval(ctx),
448 BoolExpression::Lte(a, b) => a.eval(ctx) <= b.eval(ctx),
449 BoolExpression::StrEq(a, b) => a.eval(ctx) == b.eval(ctx),
450 }
451 }
452}
453
454fn eval_stops<T: super::style::StyleInterpolatable>(stops: &[(f32, T)], input: f32) -> T {
460 debug_assert!(!stops.is_empty(), "stop list must not be empty");
461 let (first_input, first_value) = &stops[0];
462 if input <= *first_input {
463 return first_value.clone();
464 }
465 for pair in stops.windows(2) {
466 let (i0, v0) = &pair[0];
467 let (i1, v1) = &pair[1];
468 if input <= *i1 {
469 let span = (*i1 - *i0).max(f32::EPSILON);
470 let t = (input - *i0) / span;
471 return T::interpolate(v0, v1, t);
472 }
473 }
474 stops.last().expect("non-empty stops").1.clone()
475}
476
477impl<T: super::style::StyleInterpolatable> Expression<T> {
478 pub fn evaluate(&self) -> T {
480 self.eval_full(&ExprEvalContext::zoom_only(0.0))
481 }
482
483 pub fn evaluate_with_context(&self, ctx: super::style::StyleEvalContext) -> T {
485 self.eval_full(&ExprEvalContext::zoom_only(ctx.zoom))
486 }
487
488 pub fn evaluate_with_full_context(&self, ctx: &super::style::StyleEvalContextFull<'_>) -> T {
490 let expr_ctx = ExprEvalContext {
491 zoom: ctx.zoom,
492 pitch: 0.0,
493 properties: None,
494 feature_state: Some(ctx.feature_state),
495 };
496 self.eval_full(&expr_ctx)
497 }
498
499 pub fn evaluate_with_properties(&self, ctx: &ExprEvalContext<'_>) -> T {
501 self.eval_full(ctx)
502 }
503
504 pub fn eval_full(&self, ctx: &ExprEvalContext<'_>) -> T {
506 match self {
507 Expression::Constant(value) => value.clone(),
509
510 Expression::ZoomStops(stops) => eval_stops(stops, ctx.zoom),
511
512 Expression::FeatureState { key, fallback } => ctx
513 .get_state(key)
514 .and_then(|prop| T::from_feature_state_property(prop))
515 .unwrap_or_else(|| fallback.clone()),
516
517 Expression::GetProperty { key, fallback } => ctx
519 .get_property(key)
520 .and_then(|prop| T::from_feature_state_property(prop))
521 .unwrap_or_else(|| fallback.clone()),
522
523 Expression::Interpolate { input, stops } => {
524 let input_val = input.eval(ctx) as f32;
525 eval_stops(stops, input_val)
526 }
527
528 Expression::Step {
529 input,
530 default,
531 stops,
532 } => {
533 let input_val = input.eval(ctx) as f32;
534 if stops.is_empty() || input_val < stops[0].0 {
535 return default.clone();
536 }
537 let mut result = default;
539 for (threshold, value) in stops {
540 if input_val >= *threshold {
541 result = value;
542 } else {
543 break;
544 }
545 }
546 result.clone()
547 }
548
549 Expression::Match {
550 input,
551 cases,
552 fallback,
553 } => {
554 let input_val = input.eval(ctx);
555 for (label, value) in cases {
556 if *label == input_val {
557 return value.clone();
558 }
559 }
560 fallback.clone()
561 }
562
563 Expression::Case { branches, fallback } => {
564 for (condition, value) in branches {
565 if condition.eval(ctx) {
566 return value.clone();
567 }
568 }
569 fallback.clone()
570 }
571
572 Expression::Coalesce(exprs) => {
573 if let Some(first) = exprs.first() {
579 first.eval_full(ctx)
580 } else {
581 panic!("Expression::Coalesce requires at least one sub-expression");
584 }
585 }
586 }
587 }
588}
589
590impl<T> Expression<T> {
595 pub fn feature_state_key(key: impl Into<String>, fallback: T) -> Self {
597 Expression::FeatureState {
598 key: key.into(),
599 fallback,
600 }
601 }
602
603 pub fn is_feature_state_driven(&self) -> bool {
605 match self {
606 Expression::FeatureState { .. } => true,
607 Expression::Case { branches, .. } => {
608 branches.iter().any(|(cond, _)| cond.uses_feature_state())
609 }
610 Expression::Coalesce(exprs) => exprs.iter().any(|e| e.is_feature_state_driven()),
611 _ => false,
612 }
613 }
614
615 pub fn is_data_driven(&self) -> bool {
617 match self {
618 Expression::GetProperty { .. } => true,
619 Expression::Match { .. } => true,
620 Expression::Interpolate { .. } => true,
621 Expression::Step { .. } => true,
622 Expression::Case { .. } => true,
623 Expression::Coalesce(exprs) => exprs.iter().any(|e| e.is_data_driven()),
624 _ => false,
625 }
626 }
627}
628
629impl<T> From<T> for Expression<T> {
630 fn from(value: T) -> Self {
631 Expression::Constant(value)
632 }
633}
634
635impl BoolExpression {
636 pub fn uses_feature_state(&self) -> bool {
638 match self {
639 BoolExpression::GetState { .. } => true,
640 BoolExpression::Not(a) => a.uses_feature_state(),
641 BoolExpression::All(exprs) => exprs.iter().any(|e| e.uses_feature_state()),
642 BoolExpression::Any(exprs) => exprs.iter().any(|e| e.uses_feature_state()),
643 _ => false,
644 }
645 }
646}
647
648impl<T: fmt::Debug> fmt::Display for Expression<T> {
653 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
654 match self {
655 Expression::Constant(v) => write!(f, "{v:?}"),
656 Expression::ZoomStops(stops) => {
657 write!(f, "zoom_stops[")?;
658 for (i, (z, v)) in stops.iter().enumerate() {
659 if i > 0 {
660 write!(f, ", ")?;
661 }
662 write!(f, "{z}: {v:?}")?;
663 }
664 write!(f, "]")
665 }
666 Expression::FeatureState { key, fallback } => {
667 write!(f, "feature_state(\"{key}\", {fallback:?})")
668 }
669 Expression::GetProperty { key, fallback } => {
670 write!(f, "get(\"{key}\", {fallback:?})")
671 }
672 Expression::Interpolate { input, stops } => {
673 write!(f, "interpolate({input:?}, [")?;
674 for (i, (z, v)) in stops.iter().enumerate() {
675 if i > 0 {
676 write!(f, ", ")?;
677 }
678 write!(f, "{z}: {v:?}")?;
679 }
680 write!(f, "])")
681 }
682 Expression::Step {
683 input,
684 default,
685 stops,
686 } => {
687 write!(f, "step({input:?}, {default:?}, [")?;
688 for (i, (z, v)) in stops.iter().enumerate() {
689 if i > 0 {
690 write!(f, ", ")?;
691 }
692 write!(f, "{z}: {v:?}")?;
693 }
694 write!(f, "])")
695 }
696 Expression::Match {
697 input,
698 cases,
699 fallback,
700 } => {
701 write!(f, "match({input:?}, [")?;
702 for (i, (lbl, v)) in cases.iter().enumerate() {
703 if i > 0 {
704 write!(f, ", ")?;
705 }
706 write!(f, "\"{lbl}\": {v:?}")?;
707 }
708 write!(f, "], {fallback:?})")
709 }
710 Expression::Case { branches, fallback } => {
711 write!(f, "case([")?;
712 for (i, (cond, v)) in branches.iter().enumerate() {
713 if i > 0 {
714 write!(f, ", ")?;
715 }
716 write!(f, "{cond:?} => {v:?}")?;
717 }
718 write!(f, "], {fallback:?})")
719 }
720 Expression::Coalesce(exprs) => {
721 write!(f, "coalesce(")?;
722 for (i, e) in exprs.iter().enumerate() {
723 if i > 0 {
724 write!(f, ", ")?;
725 }
726 write!(f, "{e}")?;
727 }
728 write!(f, ")")
729 }
730 }
731 }
732}
733
734impl Expression<f32> {
739 pub fn zoom_interpolate(stops: Vec<(f32, f32)>) -> Self {
741 Expression::Interpolate {
742 input: Box::new(NumericExpression::Zoom),
743 stops,
744 }
745 }
746
747 pub fn zoom_step(default: f32, stops: Vec<(f32, f32)>) -> Self {
749 Expression::Step {
750 input: Box::new(NumericExpression::Zoom),
751 default,
752 stops,
753 }
754 }
755
756 pub fn property(key: impl Into<String>, fallback: f32) -> Self {
758 Expression::GetProperty {
759 key: key.into(),
760 fallback,
761 }
762 }
763
764 pub fn property_interpolate(
766 property: impl Into<String>,
767 fallback: f64,
768 stops: Vec<(f32, f32)>,
769 ) -> Self {
770 Expression::Interpolate {
771 input: Box::new(NumericExpression::GetProperty {
772 key: property.into(),
773 fallback,
774 }),
775 stops,
776 }
777 }
778}
779
780impl Expression<[f32; 4]> {
781 pub fn zoom_interpolate(stops: Vec<(f32, [f32; 4])>) -> Self {
783 Expression::Interpolate {
784 input: Box::new(NumericExpression::Zoom),
785 stops,
786 }
787 }
788
789 pub fn zoom_step(default: [f32; 4], stops: Vec<(f32, [f32; 4])>) -> Self {
791 Expression::Step {
792 input: Box::new(NumericExpression::Zoom),
793 default,
794 stops,
795 }
796 }
797
798 pub fn property_match(
800 property: impl Into<String>,
801 cases: Vec<(String, [f32; 4])>,
802 fallback: [f32; 4],
803 ) -> Self {
804 Expression::Match {
805 input: Box::new(StringExpression::GetProperty {
806 key: property.into(),
807 fallback: String::new(),
808 }),
809 cases,
810 fallback,
811 }
812 }
813}
814
815impl Expression<bool> {
816 pub fn property(key: impl Into<String>, fallback: bool) -> Self {
818 Expression::GetProperty {
819 key: key.into(),
820 fallback,
821 }
822 }
823}
824
825impl Expression<String> {
826 pub fn property(key: impl Into<String>, fallback: impl Into<String>) -> Self {
828 Expression::GetProperty {
829 key: key.into(),
830 fallback: fallback.into(),
831 }
832 }
833}
834
835#[cfg(test)]
840mod tests {
841 use super::*;
842 use crate::geometry::PropertyValue;
843 use crate::style::{StyleEvalContext, StyleEvalContextFull};
844
845 #[test]
848 fn constant_evaluates_directly() {
849 let expr: Expression<f32> = Expression::Constant(42.0);
850 assert!((expr.evaluate() - 42.0).abs() < f32::EPSILON);
851 }
852
853 #[test]
854 fn constant_via_into() {
855 let expr: Expression<f32> = 42.0.into();
856 assert!((expr.evaluate() - 42.0).abs() < f32::EPSILON);
857 }
858
859 #[test]
862 fn zoom_stops_interpolates() {
863 let expr = Expression::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
864 let ctx = ExprEvalContext::zoom_only(5.0);
865 let result = expr.eval_full(&ctx);
866 assert!((result - 50.0).abs() < 0.1);
867 }
868
869 #[test]
870 fn zoom_stops_clamps_below() {
871 let expr = Expression::ZoomStops(vec![(5.0, 10.0_f32), (10.0, 20.0)]);
872 let ctx = ExprEvalContext::zoom_only(0.0);
873 assert!((expr.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
874 }
875
876 #[test]
877 fn zoom_stops_clamps_above() {
878 let expr = Expression::ZoomStops(vec![(5.0, 10.0_f32), (10.0, 20.0)]);
879 let ctx = ExprEvalContext::zoom_only(99.0);
880 assert!((expr.eval_full(&ctx) - 20.0).abs() < f32::EPSILON);
881 }
882
883 #[test]
886 fn feature_state_returns_fallback_without_state() {
887 let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
888 let ctx = ExprEvalContext::zoom_only(10.0);
889 assert!((expr.eval_full(&ctx) - 0.5).abs() < f32::EPSILON);
890 }
891
892 #[test]
893 fn feature_state_resolves_from_state_map() {
894 let mut state = HashMap::new();
895 state.insert("opacity".to_string(), PropertyValue::Number(0.8));
896 let ctx = ExprEvalContext::zoom_only(10.0).and_state(&state);
897 let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
898 assert!((expr.eval_full(&ctx) - 0.8).abs() < f32::EPSILON);
899 }
900
901 #[test]
904 fn legacy_evaluate_with_context() {
905 let expr = Expression::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
906 let result = expr.evaluate_with_context(StyleEvalContext::new(5.0));
907 assert!((result - 50.0).abs() < 0.1);
908 }
909
910 #[test]
911 fn legacy_evaluate_with_full_context() {
912 let mut state = HashMap::new();
913 state.insert("opacity".to_string(), PropertyValue::Number(0.8));
914 let ctx = StyleEvalContextFull::new(10.0, &state);
915 let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
916 assert!((expr.evaluate_with_full_context(&ctx) - 0.8).abs() < f32::EPSILON);
917 }
918
919 #[test]
922 fn get_property_reads_feature_property() {
923 let mut props = HashMap::new();
924 props.insert("height".to_string(), PropertyValue::Number(50.0));
925 let ctx = ExprEvalContext::with_feature(10.0, &props);
926
927 let expr = Expression::<f32>::property("height", 0.0);
928 assert!((expr.eval_full(&ctx) - 50.0).abs() < f32::EPSILON);
929 }
930
931 #[test]
932 fn get_property_returns_fallback_when_missing() {
933 let props = HashMap::new();
934 let ctx = ExprEvalContext::with_feature(10.0, &props);
935 let expr = Expression::<f32>::property("height", 10.0);
936 assert!((expr.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
937 }
938
939 #[test]
942 fn interpolate_on_property() {
943 let mut props = HashMap::new();
944 props.insert("population".to_string(), PropertyValue::Number(500.0));
945 let ctx = ExprEvalContext::with_feature(10.0, &props);
946
947 let expr = Expression::<f32>::property_interpolate(
948 "population",
949 0.0,
950 vec![(0.0, 2.0), (1000.0, 20.0)],
951 );
952 let result = expr.eval_full(&ctx);
953 assert!((result - 11.0).abs() < 0.1);
954 }
955
956 #[test]
959 fn zoom_interpolate_convenience() {
960 let expr = Expression::<f32>::zoom_interpolate(vec![(0.0, 1.0), (20.0, 10.0)]);
961 let ctx = ExprEvalContext::zoom_only(10.0);
962 assert!((expr.eval_full(&ctx) - 5.5).abs() < 0.1);
963 }
964
965 #[test]
968 fn step_below_first_returns_default() {
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(3.0);
975 assert!((expr.eval_full(&ctx) - 1.0).abs() < f32::EPSILON);
976 }
977
978 #[test]
979 fn step_between_stops() {
980 let expr = Expression::Step {
981 input: Box::new(NumericExpression::Zoom),
982 default: 1.0_f32,
983 stops: vec![(5.0, 2.0), (10.0, 3.0)],
984 };
985 let ctx = ExprEvalContext::zoom_only(7.0);
986 assert!((expr.eval_full(&ctx) - 2.0).abs() < f32::EPSILON);
987 }
988
989 #[test]
990 fn step_above_last() {
991 let expr = Expression::Step {
992 input: Box::new(NumericExpression::Zoom),
993 default: 1.0_f32,
994 stops: vec![(5.0, 2.0), (10.0, 3.0)],
995 };
996 let ctx = ExprEvalContext::zoom_only(15.0);
997 assert!((expr.eval_full(&ctx) - 3.0).abs() < f32::EPSILON);
998 }
999
1000 #[test]
1003 fn match_on_string_property() {
1004 let mut props = HashMap::new();
1005 props.insert(
1006 "type".to_string(),
1007 PropertyValue::String("residential".to_string()),
1008 );
1009 let ctx = ExprEvalContext::with_feature(10.0, &props);
1010
1011 let expr: Expression<[f32; 4]> = Expression::property_match(
1012 "type",
1013 vec![
1014 ("residential".to_string(), [0.0, 0.0, 1.0, 1.0]),
1015 ("commercial".to_string(), [1.0, 0.0, 0.0, 1.0]),
1016 ],
1017 [0.5, 0.5, 0.5, 1.0],
1018 );
1019 let result = expr.eval_full(&ctx);
1020 assert_eq!(result, [0.0, 0.0, 1.0, 1.0]);
1021 }
1022
1023 #[test]
1024 fn match_returns_fallback_when_no_case() {
1025 let mut props = HashMap::new();
1026 props.insert(
1027 "type".to_string(),
1028 PropertyValue::String("industrial".to_string()),
1029 );
1030 let ctx = ExprEvalContext::with_feature(10.0, &props);
1031
1032 let expr: Expression<[f32; 4]> = Expression::property_match(
1033 "type",
1034 vec![("residential".to_string(), [0.0, 0.0, 1.0, 1.0])],
1035 [0.5, 0.5, 0.5, 1.0],
1036 );
1037 assert_eq!(expr.eval_full(&ctx), [0.5, 0.5, 0.5, 1.0]);
1038 }
1039
1040 #[test]
1043 fn case_with_bool_conditions() {
1044 let mut props = HashMap::new();
1045 props.insert("height".to_string(), PropertyValue::Number(150.0));
1046 let ctx = ExprEvalContext::with_feature(10.0, &props);
1047
1048 let expr: Expression<[f32; 4]> = Expression::Case {
1049 branches: vec![
1050 (
1051 BoolExpression::Gt(
1052 NumericExpression::GetProperty {
1053 key: "height".to_string(),
1054 fallback: 0.0,
1055 },
1056 NumericExpression::Literal(100.0),
1057 ),
1058 [1.0, 0.0, 0.0, 1.0], ),
1060 (
1061 BoolExpression::Gt(
1062 NumericExpression::GetProperty {
1063 key: "height".to_string(),
1064 fallback: 0.0,
1065 },
1066 NumericExpression::Literal(50.0),
1067 ),
1068 [1.0, 1.0, 0.0, 1.0], ),
1070 ],
1071 fallback: [0.0, 1.0, 0.0, 1.0], };
1073 assert_eq!(expr.eval_full(&ctx), [1.0, 0.0, 0.0, 1.0]);
1074 }
1075
1076 #[test]
1077 fn case_fallback_when_no_branch_matches() {
1078 let props = HashMap::new();
1079 let ctx = ExprEvalContext::with_feature(10.0, &props);
1080
1081 let expr: Expression<f32> = Expression::Case {
1082 branches: vec![
1083 (BoolExpression::Literal(false), 10.0),
1084 (BoolExpression::Literal(false), 20.0),
1085 ],
1086 fallback: 99.0,
1087 };
1088 assert!((expr.eval_full(&ctx) - 99.0).abs() < f32::EPSILON);
1089 }
1090
1091 #[test]
1094 fn numeric_arithmetic() {
1095 let ctx = ExprEvalContext::zoom_only(10.0);
1096
1097 let add = NumericExpression::Add(
1098 Box::new(NumericExpression::Literal(3.0)),
1099 Box::new(NumericExpression::Literal(4.0)),
1100 );
1101 assert!((add.eval(&ctx) - 7.0).abs() < f64::EPSILON);
1102
1103 let mul = NumericExpression::Mul(
1104 Box::new(NumericExpression::Zoom),
1105 Box::new(NumericExpression::Literal(2.0)),
1106 );
1107 assert!((mul.eval(&ctx) - 20.0).abs() < f64::EPSILON);
1108 }
1109
1110 #[test]
1111 fn numeric_division_by_zero() {
1112 let ctx = ExprEvalContext::zoom_only(10.0);
1113 let div = NumericExpression::Div(
1114 Box::new(NumericExpression::Literal(10.0)),
1115 Box::new(NumericExpression::Literal(0.0)),
1116 );
1117 assert!((div.eval(&ctx) - 0.0).abs() < f64::EPSILON);
1118 }
1119
1120 #[test]
1123 fn bool_has_checks_property_existence() {
1124 let mut props = HashMap::new();
1125 props.insert(
1126 "name".to_string(),
1127 PropertyValue::String("test".to_string()),
1128 );
1129 let ctx = ExprEvalContext::with_feature(10.0, &props);
1130
1131 assert!(BoolExpression::Has("name".to_string()).eval(&ctx));
1132 assert!(!BoolExpression::Has("missing".to_string()).eval(&ctx));
1133 }
1134
1135 #[test]
1136 fn bool_all_and_any() {
1137 let ctx = ExprEvalContext::zoom_only(10.0);
1138
1139 assert!(BoolExpression::All(vec![
1140 BoolExpression::Literal(true),
1141 BoolExpression::Literal(true),
1142 ])
1143 .eval(&ctx));
1144
1145 assert!(!BoolExpression::All(vec![
1146 BoolExpression::Literal(true),
1147 BoolExpression::Literal(false),
1148 ])
1149 .eval(&ctx));
1150
1151 assert!(BoolExpression::Any(vec![
1152 BoolExpression::Literal(false),
1153 BoolExpression::Literal(true),
1154 ])
1155 .eval(&ctx));
1156 }
1157
1158 #[test]
1161 fn string_concat() {
1162 let ctx = ExprEvalContext::zoom_only(10.0);
1163 let concat = StringExpression::Concat(
1164 Box::new(StringExpression::Literal("hello ".to_string())),
1165 Box::new(StringExpression::Literal("world".to_string())),
1166 );
1167 assert_eq!(concat.eval(&ctx), "hello world");
1168 }
1169
1170 #[test]
1171 fn string_upcase_downcase() {
1172 let ctx = ExprEvalContext::zoom_only(10.0);
1173 let up = StringExpression::Upcase(Box::new(StringExpression::Literal("hello".to_string())));
1174 assert_eq!(up.eval(&ctx), "HELLO");
1175
1176 let down =
1177 StringExpression::Downcase(Box::new(StringExpression::Literal("HELLO".to_string())));
1178 assert_eq!(down.eval(&ctx), "hello");
1179 }
1180
1181 #[test]
1184 fn is_data_driven_flags() {
1185 let constant: Expression<f32> = Expression::Constant(1.0);
1186 assert!(!constant.is_data_driven());
1187
1188 let get: Expression<f32> = Expression::GetProperty {
1189 key: "height".into(),
1190 fallback: 0.0,
1191 };
1192 assert!(get.is_data_driven());
1193
1194 let interp = Expression::<f32>::zoom_interpolate(vec![(0.0, 1.0), (10.0, 5.0)]);
1195 assert!(interp.is_data_driven()); }
1197
1198 #[test]
1199 fn is_feature_state_driven_flags() {
1200 let constant: Expression<f32> = Expression::Constant(1.0);
1201 assert!(!constant.is_feature_state_driven());
1202
1203 let driven: Expression<f32> = Expression::feature_state_key("opacity", 1.0);
1204 assert!(driven.is_feature_state_driven());
1205 }
1206
1207 #[test]
1210 fn composite_expression_zoom_and_property() {
1211 let mut props = HashMap::new();
1214 props.insert("rank".to_string(), PropertyValue::Number(5.0));
1215 let ctx = ExprEvalContext::with_feature(10.0, &props);
1216
1217 let expr: Expression<f32> = Expression::Case {
1220 branches: vec![(
1221 BoolExpression::Gte(
1222 NumericExpression::GetProperty {
1223 key: "rank".to_string(),
1224 fallback: 0.0,
1225 },
1226 NumericExpression::Literal(3.0),
1227 ),
1228 20.0, )],
1230 fallback: 10.0, };
1232 assert!((expr.eval_full(&ctx) - 20.0).abs() < f32::EPSILON);
1233 }
1234}