use crate::domains::routing::TransportMode;
use crate::error::EveryMapResult;
use crate::types::Coordinate;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TourOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_mode: Option<TransportMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_extra: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TourStop {
pub coordinate: Coordinate,
pub arrival_time: Option<String>,
pub departure_time: Option<String>,
pub duration: Option<f64>,
pub distance_from_previous: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TourResponse {
pub stops: Vec<TourStop>,
pub total_distance: Option<f64>,
pub total_duration: Option<f64>,
pub unassigned_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
#[async_trait]
pub trait TourPlanner: Send + Sync {
async fn optimize_tour(
&self,
stops: &[Coordinate],
options: &TourOptions,
) -> EveryMapResult<TourResponse>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tour_options_default() {
let options = TourOptions::default();
assert!(options.provider_extra.is_none());
assert!(options.transport_mode.is_none());
}
#[test]
fn test_tour_options_with_provider_extra() {
let options = TourOptions {
transport_mode: None,
provider_extra: Some(serde_json::json!({"fleet": {"types": []}})),
};
assert!(options.provider_extra.is_some());
}
#[test]
fn test_tour_stop_construction() {
let coordinate = Coordinate::new(52.5, 13.4).unwrap();
let stop = TourStop {
coordinate,
arrival_time: Some("2024-01-01T08:30:00".to_string()),
departure_time: Some("2024-01-01T09:00:00".to_string()),
duration: Some(1800.0),
distance_from_previous: Some(5000.0),
};
assert_eq!(stop.arrival_time, Some("2024-01-01T08:30:00".to_string()));
assert_eq!(stop.duration, Some(1800.0));
}
#[test]
fn test_tour_response_construction() {
let response = TourResponse {
stops: vec![],
total_distance: Some(15000.0),
total_duration: Some(3600.0),
unassigned_count: Some(0),
raw: None,
};
assert_eq!(response.total_distance, Some(15000.0));
assert!(response.stops.is_empty());
}
#[test]
fn test_tour_response_serde_roundtrip() {
let response = TourResponse {
stops: vec![TourStop {
coordinate: Coordinate::new(52.5, 13.4).unwrap(),
arrival_time: Some("2024-01-01T08:30:00".to_string()),
departure_time: Some("2024-01-01T09:00:00".to_string()),
duration: Some(1800.0),
distance_from_previous: Some(5000.0),
}],
total_distance: Some(15000.0),
total_duration: Some(3600.0),
unassigned_count: Some(0),
raw: Some(serde_json::json!({"optimization": "tsp"})),
};
let json = serde_json::to_string(&response).unwrap();
let back: TourResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.stops.len(), 1);
assert_eq!(back.total_distance, Some(15000.0));
assert_eq!(back.unassigned_count, Some(0));
assert!(back.raw.is_some());
}
#[test]
fn test_tour_response_empty_serde_roundtrip() {
let response = TourResponse {
stops: vec![],
total_distance: None,
total_duration: None,
unassigned_count: None,
raw: None,
};
let json = serde_json::to_string(&response).unwrap();
let back: TourResponse = serde_json::from_str(&json).unwrap();
assert!(back.stops.is_empty());
assert!(back.total_distance.is_none());
}
#[test]
fn test_tour_stop_serde_roundtrip() {
let stop = TourStop {
coordinate: Coordinate::new(48.8566, 2.3522).unwrap(),
arrival_time: Some("2024-06-01T10:00:00".to_string()),
departure_time: Some("2024-06-01T10:30:00".to_string()),
duration: Some(1800.0),
distance_from_previous: Some(2500.0),
};
let json = serde_json::to_string(&stop).unwrap();
let back: TourStop = serde_json::from_str(&json).unwrap();
assert_eq!(back.coordinate, stop.coordinate);
assert_eq!(back.arrival_time, stop.arrival_time);
assert_eq!(back.duration, Some(1800.0));
}
#[test]
fn test_tour_options_serde_roundtrip() {
let options = TourOptions {
transport_mode: None,
provider_extra: Some(serde_json::json!({
"fleet": {"types": [{"id": "truck"}]},
"plan": {"jobs": []}
})),
};
let json = serde_json::to_string(&options).unwrap();
let back: TourOptions = serde_json::from_str(&json).unwrap();
assert!(back.provider_extra.is_some());
}
#[test]
fn test_tour_stop_zero_distance() {
let stop = TourStop {
coordinate: Coordinate::ORIGIN,
arrival_time: None,
departure_time: None,
duration: Some(0.0),
distance_from_previous: Some(0.0),
};
assert_eq!(stop.distance_from_previous, Some(0.0));
}
#[test]
fn test_tour_response_multiple_stops() {
let stops: Vec<TourStop> = (0..5)
.map(|i| TourStop {
coordinate: Coordinate::new(52.0 + i as f64 * 0.1, 13.0 + i as f64 * 0.1).unwrap(),
arrival_time: None,
departure_time: None,
duration: None,
distance_from_previous: Some(i as f64 * 1000.0),
})
.collect();
let response = TourResponse {
stops,
total_distance: Some(10000.0),
total_duration: Some(1800.0),
unassigned_count: None,
raw: None,
};
assert_eq!(response.stops.len(), 5);
}
}