Skip to main content

everymap_providers_mapbox/domain/matching/
mod.rs

1pub mod types;
2
3use crate::client::MapBoxClient;
4use async_trait::async_trait;
5use everymap_core::domains::matching::{
6    MatchedPoint, MatchingOptions, RouteMatcher, TraceResponse,
7};
8use everymap_core::domains::routing::TransportMode;
9use everymap_core::error::EveryMapResult;
10use everymap_core::types::Coordinate;
11use std::sync::Arc;
12
13pub use types::*;
14
15const MATCHING_BASE_URL: &str = "https://api.mapbox.com";
16
17/// Implementation of RouteMatcher for MapBox Map Matching API v5.
18pub struct MapBoxRouteMatcher {
19    pub(crate) client: Arc<MapBoxClient>,
20    pub(crate) base_url: String,
21}
22
23impl MapBoxRouteMatcher {
24    pub fn new(client: Arc<MapBoxClient>) -> Self {
25        Self {
26            client,
27            base_url: MATCHING_BASE_URL.to_string(),
28        }
29    }
30
31    pub fn with_base_url(client: Arc<MapBoxClient>, base_url: String) -> Self {
32        Self { client, base_url }
33    }
34}
35
36/// Convert core TransportMode to MapBox profile.
37fn transport_mode_to_profile(mode: &TransportMode) -> &'static str {
38    match mode {
39        TransportMode::Car => "driving",
40        TransportMode::Truck => {
41            log::warn!(
42                "MapBox Map Matching API does not support truck profile, falling back to driving"
43            );
44            "driving"
45        }
46        TransportMode::Pedestrian => "walking",
47        TransportMode::Bicycle => "cycling",
48        TransportMode::Bus => {
49            log::warn!(
50                "MapBox Map Matching API does not support bus profile, falling back to driving"
51            );
52            "driving"
53        }
54        TransportMode::Taxi => {
55            log::warn!(
56                "MapBox Map Matching API does not support taxi profile, falling back to driving"
57            );
58            "driving"
59        }
60        TransportMode::Scooter => {
61            log::warn!(
62                "MapBox Map Matching API does not support scooter profile, falling back to driving"
63            );
64            "driving"
65        }
66        TransportMode::Unknown => "driving",
67    }
68}
69
70#[async_trait]
71impl RouteMatcher for MapBoxRouteMatcher {
72    async fn match_route(
73        &self,
74        points: &[Coordinate],
75        options: &MatchingOptions,
76    ) -> EveryMapResult<TraceResponse> {
77        if points.len() < 2 {
78            return Err(everymap_core::error::EveryMapError::provider(
79                "mapbox",
80                "INVALID_INPUT",
81                "At least 2 points required for map matching",
82            ));
83        }
84
85        let profile = options
86            .transport_mode
87            .as_ref()
88            .map(|m| transport_mode_to_profile(m))
89            .unwrap_or("driving");
90        let coords: String = points
91            .iter()
92            .map(|p| format!("{},{}", p.lng, p.lat))
93            .collect::<Vec<_>>()
94            .join(";");
95        let url = format!(
96            "{}/matching/v5/mapbox/{}/{}.json",
97            self.base_url, profile, coords
98        );
99
100        let mut params: Vec<(&str, String)> = vec![
101            ("overview", "full".to_string()),
102            ("geometries", "polyline".to_string()),
103        ];
104
105        if options.heading.is_some() {
106            log::warn!("MapBox Map Matching API v5 does not support heading; ignoring");
107        }
108        if options.departure_time.is_some() {
109            log::warn!("MapBox Map Matching API v5 does not support departure_time; ignoring");
110        }
111        if !options.avoid.is_empty() {
112            log::warn!("MapBox Map Matching API v5 does not support avoid restrictions; ignoring");
113        }
114
115        if let Some(extra) = &options.provider_extra {
116            if let Some(obj) = extra.as_object() {
117                if let Some(v) = obj.get("tidy").and_then(|v| v.as_bool()) {
118                    params.push(("tidy", v.to_string()));
119                }
120                if let Some(v) = obj.get("radiuses").and_then(|v| v.as_str()) {
121                    params.push(("radiuses", v.to_string()));
122                }
123                if let Some(v) = obj.get("timestamps").and_then(|v| v.as_str()) {
124                    params.push(("timestamps", v.to_string()));
125                }
126            }
127        }
128
129        let builder = self
130            .client
131            .build_request(reqwest::Method::GET, &url)
132            .query(&params);
133
134        let result: MapBoxMatchResponse = self.client.request_json(builder).await?;
135
136        // Extract matched points from tracepoints
137        let matched_points: Vec<MatchedPoint> = result
138            .tracepoints
139            .into_iter()
140            .filter(|tp| tp.location.is_some())
141            .map(MatchedPoint::from)
142            .collect();
143
144        // Get total distance/duration from the best matching
145        let (distance, duration) = result
146            .matchings
147            .first()
148            .map(|m| (m.distance, m.duration))
149            .unwrap_or((0.0, 0.0));
150
151        Ok(TraceResponse {
152            matched_points,
153            distance,
154            duration: Some(duration),
155            raw: None,
156        })
157    }
158}