Skip to main content

everymap_core/domains/
routing.rs

1use crate::error::EveryMapResult;
2use crate::types::{BoundingBox, Coordinate, Polyline};
3use async_trait::async_trait;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6
7impl fmt::Display for DepartureTime {
8    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9        match self {
10            DepartureTime::Now => f.write_str("now"),
11            DepartureTime::Timestamp(ts) => write!(f, "{}", ts),
12            DepartureTime::Iso8601(s) => f.write_str(s),
13        }
14    }
15}
16
17/// Typed departure time for routing, matching, and isoline requests.
18///
19/// Replaces fragile `Option<String>` guessing with an explicit enum.
20/// Serializes as a plain string for backward compatibility.
21#[derive(Debug, Clone, PartialEq)]
22pub enum DepartureTime {
23    /// Depart now.
24    Now,
25    /// Unix timestamp in seconds.
26    Timestamp(i64),
27    /// ISO 8601 formatted date/time string.
28    Iso8601(String),
29}
30
31impl Serialize for DepartureTime {
32    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
33        let s = match self {
34            DepartureTime::Now => "now".to_string(),
35            DepartureTime::Timestamp(ts) => ts.to_string(),
36            DepartureTime::Iso8601(s) => s.clone(),
37        };
38        serializer.serialize_str(&s)
39    }
40}
41
42impl<'de> Deserialize<'de> for DepartureTime {
43    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
44        let s = String::deserialize(deserializer)?;
45        if s == "now" {
46            Ok(DepartureTime::Now)
47        } else if let Ok(ts) = s.parse::<i64>() {
48            Ok(DepartureTime::Timestamp(ts))
49        } else {
50            Ok(DepartureTime::Iso8601(s))
51        }
52    }
53}
54
55/// Avoid types for routing restrictions.
56#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
57pub enum AvoidType {
58    Tolls,
59    Ferries,
60    Tunnels,
61    Highways,
62    DirtRoads,
63}
64
65/// Options for route calculation.
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct RouteOptions {
68    /// Transport mode for the route
69    pub transport_mode: Option<TransportMode>,
70    /// Number of alternative routes to compute
71    pub alternatives: Option<u32>,
72    /// Route restrictions (tolls, ferries, highways, etc.)
73    pub avoid: Vec<AvoidType>,
74    /// Departure time
75    pub departure_time: Option<DepartureTime>,
76    /// Arrival time
77    pub arrival_time: Option<DepartureTime>,
78    /// Preferred response language (BCP 47 language tag)
79    pub language: Option<String>,
80    /// Provider-specific options (HERE: routing_mode, spans, truck params; Google: waypoints, traffic_model)
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub provider_extra: Option<serde_json::Value>,
83}
84
85/// Transport mode for a route.
86#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
87pub enum TransportMode {
88    Car,
89    Truck,
90    Pedestrian,
91    Bicycle,
92    Scooter,
93    Bus,
94    Taxi,
95    Unknown,
96}
97
98/// A single leg/step in a route (e.g., a turn-by-turn instruction).
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct RouteStep {
101    /// Instruction text (e.g., "Turn right onto Main St")
102    pub instruction: Option<String>,
103    /// Distance of this step in meters
104    pub distance: Option<f64>,
105    /// Duration of this step in seconds
106    pub duration: Option<f64>,
107    /// Starting coordinate of this step
108    pub start_coordinate: Option<Coordinate>,
109    /// Ending coordinate of this step
110    pub end_coordinate: Option<Coordinate>,
111}
112
113/// A unified route result from the core trait.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct RouteResult {
116    /// Total route distance in meters
117    pub distance: f64,
118    /// Total route duration in seconds
119    pub duration: f64,
120    /// Route geometry as a polyline
121    pub geometry: Polyline,
122    /// Transport mode used for this route
123    pub transport_mode: Option<TransportMode>,
124    /// Turn-by-turn steps (if available)
125    pub steps: Vec<RouteStep>,
126    /// Bounding box for the route (if available)
127    pub bounding_box: Option<BoundingBox>,
128    /// Provider-specific raw data for advanced use cases
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub raw: Option<serde_json::Value>,
131}
132
133/// A unified route response from the core trait.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct RouteResponse {
136    /// The route results (may contain alternatives)
137    pub routes: Vec<RouteResult>,
138}
139
140#[async_trait]
141pub trait Router: Send + Sync {
142    async fn calculate_route(
143        &self,
144        start: &Coordinate,
145        end: &Coordinate,
146        options: &RouteOptions,
147    ) -> EveryMapResult<RouteResponse>;
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_route_options_default() {
156        let options = RouteOptions::default();
157        assert!(options.transport_mode.is_none());
158        assert!(options.alternatives.is_none());
159        assert!(options.avoid.is_empty());
160        assert!(options.departure_time.is_none());
161        assert!(options.arrival_time.is_none());
162        assert!(options.language.is_none());
163        assert!(options.provider_extra.is_none());
164    }
165
166    #[test]
167    fn test_route_options_with_fields() {
168        let options = RouteOptions {
169            transport_mode: Some(TransportMode::Car),
170            alternatives: Some(3),
171            avoid: vec![AvoidType::Tolls, AvoidType::Ferries],
172            language: Some("de-DE".to_string()),
173            provider_extra: Some(serde_json::json!({"routing_mode": "fast"})),
174            ..Default::default()
175        };
176        assert_eq!(options.transport_mode, Some(TransportMode::Car));
177        assert_eq!(options.alternatives, Some(3));
178        assert_eq!(options.avoid.len(), 2);
179    }
180
181    #[test]
182    fn test_transport_mode_serialization() {
183        assert_eq!(
184            serde_json::to_string(&TransportMode::Car).unwrap(),
185            "\"Car\""
186        );
187        let mode: TransportMode = serde_json::from_str("\"Truck\"").unwrap();
188        assert_eq!(mode, TransportMode::Truck);
189    }
190
191    #[test]
192    fn test_avoid_type_serialization() {
193        assert_eq!(
194            serde_json::to_string(&AvoidType::Tolls).unwrap(),
195            "\"Tolls\""
196        );
197        let avoid: AvoidType = serde_json::from_str("\"Highways\"").unwrap();
198        assert_eq!(avoid, AvoidType::Highways);
199    }
200
201    #[test]
202    fn test_route_result_construction() {
203        let result = RouteResult {
204            distance: 15000.0,
205            duration: 1800.0,
206            geometry: Polyline::new(vec![]),
207            transport_mode: Some(TransportMode::Car),
208            steps: vec![],
209            bounding_box: None,
210            raw: None,
211        };
212        assert_eq!(result.distance, 15000.0);
213        assert_eq!(result.duration, 1800.0);
214        assert_eq!(result.transport_mode, Some(TransportMode::Car));
215    }
216
217    // --- AvoidType all 5 variants serde roundtrip ---
218
219    #[test]
220    fn test_avoid_type_all_variants_serde() {
221        let variants = [
222            AvoidType::Tolls,
223            AvoidType::Ferries,
224            AvoidType::Tunnels,
225            AvoidType::Highways,
226            AvoidType::DirtRoads,
227        ];
228        for v in &variants {
229            let json = serde_json::to_string(v).unwrap();
230            let back: AvoidType = serde_json::from_str(&json).unwrap();
231            assert_eq!(*v, back, "Failed roundtrip for {:?}", v);
232        }
233    }
234
235    #[test]
236    fn test_avoid_type_all_variants_distinct() {
237        let variants = [
238            AvoidType::Tolls,
239            AvoidType::Ferries,
240            AvoidType::Tunnels,
241            AvoidType::Highways,
242            AvoidType::DirtRoads,
243        ];
244        for i in 0..variants.len() {
245            for j in 0..variants.len() {
246                if i != j {
247                    assert_ne!(variants[i], variants[j]);
248                }
249            }
250        }
251    }
252
253    // --- TransportMode all 8 variants serde roundtrip ---
254
255    #[test]
256    fn test_transport_mode_all_variants_serde() {
257        let variants = [
258            TransportMode::Car,
259            TransportMode::Truck,
260            TransportMode::Pedestrian,
261            TransportMode::Bicycle,
262            TransportMode::Scooter,
263            TransportMode::Bus,
264            TransportMode::Taxi,
265            TransportMode::Unknown,
266        ];
267        for v in &variants {
268            let json = serde_json::to_string(v).unwrap();
269            let back: TransportMode = serde_json::from_str(&json).unwrap();
270            assert_eq!(*v, back, "Failed roundtrip for {:?}", v);
271        }
272    }
273
274    #[test]
275    fn test_transport_mode_all_variants_distinct() {
276        let variants = [
277            TransportMode::Car,
278            TransportMode::Truck,
279            TransportMode::Pedestrian,
280            TransportMode::Bicycle,
281            TransportMode::Scooter,
282            TransportMode::Bus,
283            TransportMode::Taxi,
284            TransportMode::Unknown,
285        ];
286        for i in 0..variants.len() {
287            for j in 0..variants.len() {
288                if i != j {
289                    assert_ne!(variants[i], variants[j]);
290                }
291            }
292        }
293    }
294
295    // --- RouteResponse serde roundtrip ---
296
297    #[test]
298    fn test_route_response_serde_roundtrip() {
299        let response = RouteResponse {
300            routes: vec![RouteResult {
301                distance: 15000.0,
302                duration: 1800.0,
303                geometry: Polyline::new(vec![]),
304                transport_mode: Some(TransportMode::Car),
305                steps: vec![],
306                bounding_box: None,
307                raw: None,
308            }],
309        };
310        let json = serde_json::to_string(&response).unwrap();
311        let back: RouteResponse = serde_json::from_str(&json).unwrap();
312        assert_eq!(back.routes.len(), 1);
313        assert_eq!(back.routes[0].distance, 15000.0);
314        assert_eq!(back.routes[0].transport_mode, Some(TransportMode::Car));
315    }
316
317    #[test]
318    fn test_route_response_empty_routes() {
319        let response = RouteResponse { routes: vec![] };
320        let json = serde_json::to_string(&response).unwrap();
321        let back: RouteResponse = serde_json::from_str(&json).unwrap();
322        assert!(back.routes.is_empty());
323    }
324
325    // --- RouteOptions with provider_extra serde roundtrip ---
326
327    #[test]
328    fn test_route_options_serde_roundtrip() {
329        let options = RouteOptions {
330            transport_mode: Some(TransportMode::Truck),
331            alternatives: Some(2),
332            avoid: vec![AvoidType::Tolls, AvoidType::Ferries],
333            departure_time: Some(DepartureTime::Iso8601("2024-06-01T08:00:00".to_string())),
334            arrival_time: None,
335            language: Some("en".to_string()),
336            provider_extra: Some(
337                serde_json::json!({"routing_mode": "fast", "truck": {"weight": 18}}),
338            ),
339        };
340        let json = serde_json::to_string(&options).unwrap();
341        let back: RouteOptions = serde_json::from_str(&json).unwrap();
342        assert_eq!(back.transport_mode, Some(TransportMode::Truck));
343        assert_eq!(back.alternatives, Some(2));
344        assert_eq!(back.avoid.len(), 2);
345        assert!(back.provider_extra.is_some());
346    }
347
348    // --- RouteStep serde roundtrip ---
349
350    #[test]
351    fn test_route_step_serde_roundtrip() {
352        let step = RouteStep {
353            instruction: Some("Turn right onto Main St".to_string()),
354            distance: Some(500.0),
355            duration: Some(60.0),
356            start_coordinate: Some(Coordinate::new(52.5, 13.4).unwrap()),
357            end_coordinate: Some(Coordinate::new(52.51, 13.41).unwrap()),
358        };
359        let json = serde_json::to_string(&step).unwrap();
360        let back: RouteStep = serde_json::from_str(&json).unwrap();
361        assert_eq!(back.instruction.as_deref(), Some("Turn right onto Main St"));
362        assert_eq!(back.distance, Some(500.0));
363        assert_eq!(back.duration, Some(60.0));
364    }
365
366    // --- RouteResult with all fields populated ---
367
368    #[test]
369    fn test_route_result_full_serde_roundtrip() {
370        let result = RouteResult {
371            distance: 0.0,
372            duration: 0.0,
373            geometry: Polyline::new(vec![Coordinate::new(52.5, 13.4).unwrap()]),
374            transport_mode: Some(TransportMode::Pedestrian),
375            steps: vec![RouteStep {
376                instruction: Some("Walk north".to_string()),
377                distance: Some(100.0),
378                duration: Some(120.0),
379                start_coordinate: None,
380                end_coordinate: None,
381            }],
382            bounding_box: Some(BoundingBox::new(
383                Coordinate::new(52.6, 13.5).unwrap(),
384                Coordinate::new(52.4, 13.3).unwrap(),
385            )),
386            raw: Some(serde_json::json!({"legs": []})),
387        };
388        let json = serde_json::to_string(&result).unwrap();
389        let back: RouteResult = serde_json::from_str(&json).unwrap();
390        assert_eq!(back.distance, 0.0);
391        assert_eq!(back.steps.len(), 1);
392        assert!(back.bounding_box.is_some());
393        assert!(back.raw.is_some());
394    }
395
396    // --- Edge cases ---
397
398    #[test]
399    fn test_route_result_zero_distance_duration() {
400        let result = RouteResult {
401            distance: 0.0,
402            duration: 0.0,
403            geometry: Polyline::new(vec![]),
404            transport_mode: None,
405            steps: vec![],
406            bounding_box: None,
407            raw: None,
408        };
409        assert_eq!(result.distance, 0.0);
410        assert_eq!(result.duration, 0.0);
411    }
412
413    #[test]
414    fn test_route_options_all_avoid_types() {
415        let options = RouteOptions {
416            avoid: vec![
417                AvoidType::Tolls,
418                AvoidType::Ferries,
419                AvoidType::Tunnels,
420                AvoidType::Highways,
421                AvoidType::DirtRoads,
422            ],
423            ..Default::default()
424        };
425        assert_eq!(options.avoid.len(), 5);
426    }
427
428    // --- DepartureTime serde ---
429
430    #[test]
431    fn test_departure_time_now_serde() {
432        let json = serde_json::to_string(&DepartureTime::Now).unwrap();
433        assert_eq!(json, "\"now\"");
434        let back: DepartureTime = serde_json::from_str(&json).unwrap();
435        assert_eq!(back, DepartureTime::Now);
436    }
437
438    #[test]
439    fn test_departure_time_timestamp_serde() {
440        let dt = DepartureTime::Timestamp(1717200000);
441        let json = serde_json::to_string(&dt).unwrap();
442        assert_eq!(json, "\"1717200000\"");
443        let back: DepartureTime = serde_json::from_str(&json).unwrap();
444        assert_eq!(back, DepartureTime::Timestamp(1717200000));
445    }
446
447    #[test]
448    fn test_departure_time_iso8601_serde() {
449        let dt = DepartureTime::Iso8601("2024-06-01T08:00:00Z".to_string());
450        let json = serde_json::to_string(&dt).unwrap();
451        assert_eq!(json, "\"2024-06-01T08:00:00Z\"");
452        let back: DepartureTime = serde_json::from_str(&json).unwrap();
453        assert_eq!(
454            back,
455            DepartureTime::Iso8601("2024-06-01T08:00:00Z".to_string())
456        );
457    }
458}