Skip to main content

everymap_core/domains/
matching.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/// Options for GPS trace matching to the road network.
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct MatchingOptions {
10    /// Transport mode for matching
11    pub transport_mode: Option<TransportMode>,
12    /// Heading angle in degrees (0-360)
13    pub heading: Option<f64>,
14    /// Departure time
15    pub departure_time: Option<DepartureTime>,
16    /// Route restrictions
17    pub avoid: Vec<crate::domains::routing::AvoidType>,
18    /// Provider-specific options (HERE: map_match_radius, route_match, vehicle params; Google: interpolation, snapping)
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub provider_extra: Option<serde_json::Value>,
21}
22
23/// A matched point from route matching.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct MatchedPoint {
26    /// The matched/snapped coordinate on the road network
27    pub coordinate: Coordinate,
28    /// Confidence score for this match (0.0-1.0)
29    pub confidence: Option<f64>,
30    /// Matched road name (if available)
31    pub road_name: Option<String>,
32}
33
34/// A unified matching response from the core trait.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct TraceResponse {
37    /// The snapped/matched points
38    pub matched_points: Vec<MatchedPoint>,
39    /// Total matched route distance in meters
40    pub distance: f64,
41    /// Total matched route duration in seconds
42    pub duration: Option<f64>,
43    /// Provider-specific raw data for advanced use cases
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub raw: Option<serde_json::Value>,
46}
47
48#[async_trait]
49pub trait RouteMatcher: Send + Sync {
50    async fn match_route(
51        &self,
52        points: &[Coordinate],
53        options: &MatchingOptions,
54    ) -> EveryMapResult<TraceResponse>;
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_matching_options_default() {
63        let options = MatchingOptions::default();
64        assert!(options.transport_mode.is_none());
65        assert!(options.heading.is_none());
66        assert!(options.departure_time.is_none());
67        assert!(options.avoid.is_empty());
68        assert!(options.provider_extra.is_none());
69    }
70
71    #[test]
72    fn test_matching_options_with_fields() {
73        let options = MatchingOptions {
74            transport_mode: Some(crate::domains::routing::TransportMode::Car),
75            heading: Some(180.0),
76            departure_time: Some(DepartureTime::Iso8601("2024-01-01T08:00:00".to_string())),
77            avoid: vec![crate::domains::routing::AvoidType::Tolls],
78            provider_extra: Some(serde_json::json!({"map_match_radius": 50})),
79        };
80        assert_eq!(options.heading, Some(180.0));
81        assert!(options.avoid.len() == 1);
82    }
83
84    #[test]
85    fn test_matched_point_construction() {
86        let coordinate = Coordinate::new(52.5, 13.4).unwrap();
87        let point = MatchedPoint {
88            coordinate,
89            confidence: Some(0.95),
90            road_name: Some("Main Street".to_string()),
91        };
92        assert_eq!(point.confidence, Some(0.95));
93        assert_eq!(point.road_name, Some("Main Street".to_string()));
94    }
95
96    #[test]
97    fn test_trace_response_construction() {
98        let response = TraceResponse {
99            matched_points: vec![],
100            distance: 5000.0,
101            duration: Some(600.0),
102            raw: None,
103        };
104        assert_eq!(response.distance, 5000.0);
105        assert!(response.matched_points.is_empty());
106    }
107
108    // --- TraceResponse serde roundtrip ---
109
110    #[test]
111    fn test_trace_response_serde_roundtrip() {
112        let response = TraceResponse {
113            matched_points: vec![MatchedPoint {
114                coordinate: Coordinate::new(52.5, 13.4).unwrap(),
115                confidence: Some(0.95),
116                road_name: Some("Friedrichstr".to_string()),
117            }],
118            distance: 12000.5,
119            duration: Some(900.0),
120            raw: Some(serde_json::json!({"trace_id": "t1"})),
121        };
122        let json = serde_json::to_string(&response).unwrap();
123        let back: TraceResponse = serde_json::from_str(&json).unwrap();
124        assert_eq!(back.matched_points.len(), 1);
125        assert_eq!(back.distance, 12000.5);
126        assert_eq!(back.duration, Some(900.0));
127        assert!(back.raw.is_some());
128    }
129
130    #[test]
131    fn test_trace_response_empty_serde_roundtrip() {
132        let response = TraceResponse {
133            matched_points: vec![],
134            distance: 0.0,
135            duration: None,
136            raw: None,
137        };
138        let json = serde_json::to_string(&response).unwrap();
139        let back: TraceResponse = serde_json::from_str(&json).unwrap();
140        assert!(back.matched_points.is_empty());
141        assert_eq!(back.distance, 0.0);
142        assert!(back.duration.is_none());
143    }
144
145    // --- MatchingOptions serde roundtrip ---
146
147    #[test]
148    fn test_matching_options_serde_roundtrip() {
149        let options = MatchingOptions {
150            transport_mode: Some(crate::domains::routing::TransportMode::Bicycle),
151            heading: Some(270.0),
152            departure_time: Some(DepartureTime::Iso8601("2024-03-15T10:00:00".to_string())),
153            avoid: vec![crate::domains::routing::AvoidType::Highways],
154            provider_extra: Some(serde_json::json!({"map_match_radius": 30})),
155        };
156        let json = serde_json::to_string(&options).unwrap();
157        let back: MatchingOptions = serde_json::from_str(&json).unwrap();
158        assert_eq!(
159            back.transport_mode,
160            Some(crate::domains::routing::TransportMode::Bicycle)
161        );
162        assert_eq!(back.heading, Some(270.0));
163        assert_eq!(back.avoid.len(), 1);
164        assert!(back.provider_extra.is_some());
165    }
166
167    // --- MatchedPoint serde roundtrip ---
168
169    #[test]
170    fn test_matched_point_serde_roundtrip() {
171        let point = MatchedPoint {
172            coordinate: Coordinate::new(48.8566, 2.3522).unwrap(),
173            confidence: Some(0.88),
174            road_name: Some("Champs-Elysees".to_string()),
175        };
176        let json = serde_json::to_string(&point).unwrap();
177        let back: MatchedPoint = serde_json::from_str(&json).unwrap();
178        assert_eq!(back.coordinate, point.coordinate);
179        assert_eq!(back.confidence, point.confidence);
180        assert_eq!(back.road_name, point.road_name);
181    }
182
183    // --- Edge cases ---
184
185    #[test]
186    fn test_matched_point_zero_confidence() {
187        let point = MatchedPoint {
188            coordinate: Coordinate::ORIGIN,
189            confidence: Some(0.0),
190            road_name: None,
191        };
192        assert_eq!(point.confidence, Some(0.0));
193        assert!(point.road_name.is_none());
194    }
195
196    #[test]
197    fn test_trace_response_zero_distance() {
198        let response = TraceResponse {
199            matched_points: vec![],
200            distance: 0.0,
201            duration: Some(0.0),
202            raw: None,
203        };
204        assert_eq!(response.distance, 0.0);
205        assert_eq!(response.duration, Some(0.0));
206    }
207}