everymap_providers_mapbox/domain/matching/
mod.rs1pub 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
17pub 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
36fn 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(¶ms);
133
134 let result: MapBoxMatchResponse = self.client.request_json(builder).await?;
135
136 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 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}