Skip to main content

everymap_core/domains/
isoline.rs

1use crate::domains::routing::{DepartureTime, TransportMode};
2use crate::error::EveryMapResult;
3use crate::types::Coordinate;
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7/// Type of range for isoline calculation.
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
9pub enum RangeType {
10    Distance,
11    Time,
12    Consumption,
13}
14
15/// Options for isoline (reachability polygon) calculation.
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct IsolineOptions {
18    /// Type of range (distance, time, or consumption)
19    pub range_type: Option<RangeType>,
20    /// Transport mode for the isoline calculation
21    pub transport_mode: Option<TransportMode>,
22    /// Departure time
23    pub departure_time: Option<DepartureTime>,
24    /// Route restrictions
25    pub avoid: Vec<crate::domains::routing::AvoidType>,
26    /// Provider-specific options (HERE: routing_mode, optimize_for, vehicle params)
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub provider_extra: Option<serde_json::Value>,
29}
30
31/// A unified isoline result from the core trait.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct IsolineResult {
34    /// The polygon points defining the isoline boundary
35    pub polygon: Vec<Coordinate>,
36    /// The range value (in meters or seconds, depending on range type)
37    pub range: Option<f64>,
38}
39
40/// A unified isoline response from the core trait.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct IsolineResponse {
43    /// The isoline results (may contain multiple ranges)
44    pub isolines: Vec<IsolineResult>,
45    /// Provider-specific raw data for advanced use cases
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub raw: Option<serde_json::Value>,
48}
49
50#[async_trait]
51pub trait IsolineProvider: Send + Sync {
52    async fn get_isoline(
53        &self,
54        center: &Coordinate,
55        range: f64,
56        options: &IsolineOptions,
57    ) -> EveryMapResult<IsolineResponse>;
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_isoline_options_default() {
66        let options = IsolineOptions::default();
67        assert!(options.range_type.is_none());
68        assert!(options.transport_mode.is_none());
69        assert!(options.departure_time.is_none());
70        assert!(options.avoid.is_empty());
71        assert!(options.provider_extra.is_none());
72    }
73
74    #[test]
75    fn test_isoline_options_with_fields() {
76        let options = IsolineOptions {
77            range_type: Some(RangeType::Time),
78            transport_mode: Some(crate::domains::routing::TransportMode::Car),
79            departure_time: Some(DepartureTime::Iso8601("2024-01-01T08:00:00".to_string())),
80            avoid: vec![crate::domains::routing::AvoidType::Tolls],
81            provider_extra: Some(serde_json::json!({"routing_mode": "fast"})),
82        };
83        assert_eq!(options.range_type, Some(RangeType::Time));
84        assert!(options.avoid.len() == 1);
85    }
86
87    #[test]
88    fn test_isoline_result_construction() {
89        let result = IsolineResult {
90            polygon: vec![],
91            range: Some(1000.0),
92        };
93        assert_eq!(result.range, Some(1000.0));
94        assert!(result.polygon.is_empty());
95    }
96
97    #[test]
98    fn test_range_type_serialization() {
99        assert_eq!(
100            serde_json::to_string(&RangeType::Distance).unwrap(),
101            "\"Distance\""
102        );
103        let rt: RangeType = serde_json::from_str("\"Time\"").unwrap();
104        assert_eq!(rt, RangeType::Time);
105    }
106
107    // --- RangeType all 3 variants serde roundtrip ---
108
109    #[test]
110    fn test_range_type_all_variants_serde() {
111        let variants = [RangeType::Distance, RangeType::Time, RangeType::Consumption];
112        for v in &variants {
113            let json = serde_json::to_string(v).unwrap();
114            let back: RangeType = serde_json::from_str(&json).unwrap();
115            assert_eq!(*v, back, "Failed roundtrip for {:?}", v);
116        }
117    }
118
119    #[test]
120    fn test_range_type_all_variants_distinct() {
121        let variants = [RangeType::Distance, RangeType::Time, RangeType::Consumption];
122        for i in 0..variants.len() {
123            for j in 0..variants.len() {
124                if i != j {
125                    assert_ne!(variants[i], variants[j]);
126                }
127            }
128        }
129    }
130
131    // --- IsolineResponse serde roundtrip ---
132
133    #[test]
134    fn test_isoline_response_serde_roundtrip() {
135        let response = IsolineResponse {
136            isolines: vec![IsolineResult {
137                polygon: vec![
138                    Coordinate::new(52.5, 13.4).unwrap(),
139                    Coordinate::new(52.6, 13.5).unwrap(),
140                ],
141                range: Some(5000.0),
142            }],
143            raw: Some(serde_json::json!({"center": "52.5,13.4"})),
144        };
145        let json = serde_json::to_string(&response).unwrap();
146        let back: IsolineResponse = serde_json::from_str(&json).unwrap();
147        assert_eq!(back.isolines.len(), 1);
148        assert_eq!(back.isolines[0].polygon.len(), 2);
149        assert_eq!(back.isolines[0].range, Some(5000.0));
150        assert!(back.raw.is_some());
151    }
152
153    #[test]
154    fn test_isoline_response_empty() {
155        let response = IsolineResponse {
156            isolines: vec![],
157            raw: None,
158        };
159        let json = serde_json::to_string(&response).unwrap();
160        let back: IsolineResponse = serde_json::from_str(&json).unwrap();
161        assert!(back.isolines.is_empty());
162        assert!(back.raw.is_none());
163    }
164
165    // --- IsolineOptions serde roundtrip ---
166
167    #[test]
168    fn test_isoline_options_serde_roundtrip() {
169        let options = IsolineOptions {
170            range_type: Some(RangeType::Consumption),
171            transport_mode: Some(crate::domains::routing::TransportMode::Truck),
172            departure_time: Some(DepartureTime::Iso8601("2024-01-01T08:00:00".to_string())),
173            avoid: vec![
174                crate::domains::routing::AvoidType::Tolls,
175                crate::domains::routing::AvoidType::Highways,
176            ],
177            provider_extra: Some(serde_json::json!({"optimize_for": "quality"})),
178        };
179        let json = serde_json::to_string(&options).unwrap();
180        let back: IsolineOptions = serde_json::from_str(&json).unwrap();
181        assert_eq!(back.range_type, Some(RangeType::Consumption));
182        assert_eq!(back.avoid.len(), 2);
183        assert!(back.provider_extra.is_some());
184    }
185
186    // --- Edge cases ---
187
188    #[test]
189    fn test_isoline_result_empty_polygon() {
190        let result = IsolineResult {
191            polygon: vec![],
192            range: None,
193        };
194        assert!(result.polygon.is_empty());
195        assert!(result.range.is_none());
196    }
197
198    #[test]
199    fn test_isoline_result_zero_range() {
200        let result = IsolineResult {
201            polygon: vec![Coordinate::ORIGIN],
202            range: Some(0.0),
203        };
204        assert_eq!(result.range, Some(0.0));
205    }
206}