use crate::domains::routing::{DepartureTime, 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 MatchingOptions {
pub transport_mode: Option<TransportMode>,
pub heading: Option<f64>,
pub departure_time: Option<DepartureTime>,
pub avoid: Vec<crate::domains::routing::AvoidType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_extra: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchedPoint {
pub coordinate: Coordinate,
pub confidence: Option<f64>,
pub road_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceResponse {
pub matched_points: Vec<MatchedPoint>,
pub distance: f64,
pub duration: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
#[async_trait]
pub trait RouteMatcher: Send + Sync {
async fn match_route(
&self,
points: &[Coordinate],
options: &MatchingOptions,
) -> EveryMapResult<TraceResponse>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_matching_options_default() {
let options = MatchingOptions::default();
assert!(options.transport_mode.is_none());
assert!(options.heading.is_none());
assert!(options.departure_time.is_none());
assert!(options.avoid.is_empty());
assert!(options.provider_extra.is_none());
}
#[test]
fn test_matching_options_with_fields() {
let options = MatchingOptions {
transport_mode: Some(crate::domains::routing::TransportMode::Car),
heading: Some(180.0),
departure_time: Some(DepartureTime::Iso8601("2024-01-01T08:00:00".to_string())),
avoid: vec![crate::domains::routing::AvoidType::Tolls],
provider_extra: Some(serde_json::json!({"map_match_radius": 50})),
};
assert_eq!(options.heading, Some(180.0));
assert!(options.avoid.len() == 1);
}
#[test]
fn test_matched_point_construction() {
let coordinate = Coordinate::new(52.5, 13.4).unwrap();
let point = MatchedPoint {
coordinate,
confidence: Some(0.95),
road_name: Some("Main Street".to_string()),
};
assert_eq!(point.confidence, Some(0.95));
assert_eq!(point.road_name, Some("Main Street".to_string()));
}
#[test]
fn test_trace_response_construction() {
let response = TraceResponse {
matched_points: vec![],
distance: 5000.0,
duration: Some(600.0),
raw: None,
};
assert_eq!(response.distance, 5000.0);
assert!(response.matched_points.is_empty());
}
#[test]
fn test_trace_response_serde_roundtrip() {
let response = TraceResponse {
matched_points: vec![MatchedPoint {
coordinate: Coordinate::new(52.5, 13.4).unwrap(),
confidence: Some(0.95),
road_name: Some("Friedrichstr".to_string()),
}],
distance: 12000.5,
duration: Some(900.0),
raw: Some(serde_json::json!({"trace_id": "t1"})),
};
let json = serde_json::to_string(&response).unwrap();
let back: TraceResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.matched_points.len(), 1);
assert_eq!(back.distance, 12000.5);
assert_eq!(back.duration, Some(900.0));
assert!(back.raw.is_some());
}
#[test]
fn test_trace_response_empty_serde_roundtrip() {
let response = TraceResponse {
matched_points: vec![],
distance: 0.0,
duration: None,
raw: None,
};
let json = serde_json::to_string(&response).unwrap();
let back: TraceResponse = serde_json::from_str(&json).unwrap();
assert!(back.matched_points.is_empty());
assert_eq!(back.distance, 0.0);
assert!(back.duration.is_none());
}
#[test]
fn test_matching_options_serde_roundtrip() {
let options = MatchingOptions {
transport_mode: Some(crate::domains::routing::TransportMode::Bicycle),
heading: Some(270.0),
departure_time: Some(DepartureTime::Iso8601("2024-03-15T10:00:00".to_string())),
avoid: vec![crate::domains::routing::AvoidType::Highways],
provider_extra: Some(serde_json::json!({"map_match_radius": 30})),
};
let json = serde_json::to_string(&options).unwrap();
let back: MatchingOptions = serde_json::from_str(&json).unwrap();
assert_eq!(
back.transport_mode,
Some(crate::domains::routing::TransportMode::Bicycle)
);
assert_eq!(back.heading, Some(270.0));
assert_eq!(back.avoid.len(), 1);
assert!(back.provider_extra.is_some());
}
#[test]
fn test_matched_point_serde_roundtrip() {
let point = MatchedPoint {
coordinate: Coordinate::new(48.8566, 2.3522).unwrap(),
confidence: Some(0.88),
road_name: Some("Champs-Elysees".to_string()),
};
let json = serde_json::to_string(&point).unwrap();
let back: MatchedPoint = serde_json::from_str(&json).unwrap();
assert_eq!(back.coordinate, point.coordinate);
assert_eq!(back.confidence, point.confidence);
assert_eq!(back.road_name, point.road_name);
}
#[test]
fn test_matched_point_zero_confidence() {
let point = MatchedPoint {
coordinate: Coordinate::ORIGIN,
confidence: Some(0.0),
road_name: None,
};
assert_eq!(point.confidence, Some(0.0));
assert!(point.road_name.is_none());
}
#[test]
fn test_trace_response_zero_distance() {
let response = TraceResponse {
matched_points: vec![],
distance: 0.0,
duration: Some(0.0),
raw: None,
};
assert_eq!(response.distance, 0.0);
assert_eq!(response.duration, Some(0.0));
}
}