Skip to main content

everymap_core/domains/
tour.rs

1use crate::domains::routing::TransportMode;
2use crate::error::EveryMapResult;
3use crate::types::Coordinate;
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7/// Options for tour/sequence optimization.
8///
9/// Tour optimization involves complex problem definitions (fleet, plan,
10/// configuration) that are entirely provider-specific. The core options
11/// type is minimal; all provider-specific data goes through `provider_extra`.
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
13pub struct TourOptions {
14    /// Transport mode for the tour (car, pedestrian, bicycle, etc.)
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub transport_mode: Option<TransportMode>,
17    /// Provider-specific problem definition (HERE: TourProblem JSON; other providers: their format)
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub provider_extra: Option<serde_json::Value>,
20}
21
22/// A stop in an optimized tour.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TourStop {
25    /// The coordinate of this stop
26    pub coordinate: Coordinate,
27    /// Arrival time (ISO 8601 string, if available)
28    pub arrival_time: Option<String>,
29    /// Departure time (ISO 8601 string, if available)
30    pub departure_time: Option<String>,
31    /// Duration at this stop in seconds
32    pub duration: Option<f64>,
33    /// Distance from previous stop in meters
34    pub distance_from_previous: Option<f64>,
35}
36
37/// A unified tour response from the core trait.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct TourResponse {
40    /// The optimized sequence of stops
41    pub stops: Vec<TourStop>,
42    /// Total tour distance in meters
43    pub total_distance: Option<f64>,
44    /// Total tour duration in seconds
45    pub total_duration: Option<f64>,
46    /// Number of unassigned stops (that couldn't be fit into the tour)
47    pub unassigned_count: Option<u32>,
48    /// Provider-specific raw data for advanced use cases
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub raw: Option<serde_json::Value>,
51}
52
53#[async_trait]
54pub trait TourPlanner: Send + Sync {
55    async fn optimize_tour(
56        &self,
57        stops: &[Coordinate],
58        options: &TourOptions,
59    ) -> EveryMapResult<TourResponse>;
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_tour_options_default() {
68        let options = TourOptions::default();
69        assert!(options.provider_extra.is_none());
70        assert!(options.transport_mode.is_none());
71    }
72
73    #[test]
74    fn test_tour_options_with_provider_extra() {
75        let options = TourOptions {
76            transport_mode: None,
77            provider_extra: Some(serde_json::json!({"fleet": {"types": []}})),
78        };
79        assert!(options.provider_extra.is_some());
80    }
81
82    #[test]
83    fn test_tour_stop_construction() {
84        let coordinate = Coordinate::new(52.5, 13.4).unwrap();
85        let stop = TourStop {
86            coordinate,
87            arrival_time: Some("2024-01-01T08:30:00".to_string()),
88            departure_time: Some("2024-01-01T09:00:00".to_string()),
89            duration: Some(1800.0),
90            distance_from_previous: Some(5000.0),
91        };
92        assert_eq!(stop.arrival_time, Some("2024-01-01T08:30:00".to_string()));
93        assert_eq!(stop.duration, Some(1800.0));
94    }
95
96    #[test]
97    fn test_tour_response_construction() {
98        let response = TourResponse {
99            stops: vec![],
100            total_distance: Some(15000.0),
101            total_duration: Some(3600.0),
102            unassigned_count: Some(0),
103            raw: None,
104        };
105        assert_eq!(response.total_distance, Some(15000.0));
106        assert!(response.stops.is_empty());
107    }
108
109    // --- TourResponse serde roundtrip ---
110
111    #[test]
112    fn test_tour_response_serde_roundtrip() {
113        let response = TourResponse {
114            stops: vec![TourStop {
115                coordinate: Coordinate::new(52.5, 13.4).unwrap(),
116                arrival_time: Some("2024-01-01T08:30:00".to_string()),
117                departure_time: Some("2024-01-01T09:00:00".to_string()),
118                duration: Some(1800.0),
119                distance_from_previous: Some(5000.0),
120            }],
121            total_distance: Some(15000.0),
122            total_duration: Some(3600.0),
123            unassigned_count: Some(0),
124            raw: Some(serde_json::json!({"optimization": "tsp"})),
125        };
126        let json = serde_json::to_string(&response).unwrap();
127        let back: TourResponse = serde_json::from_str(&json).unwrap();
128        assert_eq!(back.stops.len(), 1);
129        assert_eq!(back.total_distance, Some(15000.0));
130        assert_eq!(back.unassigned_count, Some(0));
131        assert!(back.raw.is_some());
132    }
133
134    #[test]
135    fn test_tour_response_empty_serde_roundtrip() {
136        let response = TourResponse {
137            stops: vec![],
138            total_distance: None,
139            total_duration: None,
140            unassigned_count: None,
141            raw: None,
142        };
143        let json = serde_json::to_string(&response).unwrap();
144        let back: TourResponse = serde_json::from_str(&json).unwrap();
145        assert!(back.stops.is_empty());
146        assert!(back.total_distance.is_none());
147    }
148
149    // --- TourStop serde roundtrip ---
150
151    #[test]
152    fn test_tour_stop_serde_roundtrip() {
153        let stop = TourStop {
154            coordinate: Coordinate::new(48.8566, 2.3522).unwrap(),
155            arrival_time: Some("2024-06-01T10:00:00".to_string()),
156            departure_time: Some("2024-06-01T10:30:00".to_string()),
157            duration: Some(1800.0),
158            distance_from_previous: Some(2500.0),
159        };
160        let json = serde_json::to_string(&stop).unwrap();
161        let back: TourStop = serde_json::from_str(&json).unwrap();
162        assert_eq!(back.coordinate, stop.coordinate);
163        assert_eq!(back.arrival_time, stop.arrival_time);
164        assert_eq!(back.duration, Some(1800.0));
165    }
166
167    // --- TourOptions serde roundtrip ---
168
169    #[test]
170    fn test_tour_options_serde_roundtrip() {
171        let options = TourOptions {
172            transport_mode: None,
173            provider_extra: Some(serde_json::json!({
174                "fleet": {"types": [{"id": "truck"}]},
175                "plan": {"jobs": []}
176            })),
177        };
178        let json = serde_json::to_string(&options).unwrap();
179        let back: TourOptions = serde_json::from_str(&json).unwrap();
180        assert!(back.provider_extra.is_some());
181    }
182
183    // --- Edge cases ---
184
185    #[test]
186    fn test_tour_stop_zero_distance() {
187        let stop = TourStop {
188            coordinate: Coordinate::ORIGIN,
189            arrival_time: None,
190            departure_time: None,
191            duration: Some(0.0),
192            distance_from_previous: Some(0.0),
193        };
194        assert_eq!(stop.distance_from_previous, Some(0.0));
195    }
196
197    #[test]
198    fn test_tour_response_multiple_stops() {
199        let stops: Vec<TourStop> = (0..5)
200            .map(|i| TourStop {
201                coordinate: Coordinate::new(52.0 + i as f64 * 0.1, 13.0 + i as f64 * 0.1).unwrap(),
202                arrival_time: None,
203                departure_time: None,
204                duration: None,
205                distance_from_previous: Some(i as f64 * 1000.0),
206            })
207            .collect();
208        let response = TourResponse {
209            stops,
210            total_distance: Some(10000.0),
211            total_duration: Some(1800.0),
212            unassigned_count: None,
213            raw: None,
214        };
215        assert_eq!(response.stops.len(), 5);
216    }
217}