everymap_providers_mapbox/domain/routing/
mod.rs1pub mod types;
2
3use crate::client::MapBoxClient;
4use async_trait::async_trait;
5use everymap_core::domains::routing::{
6 RouteOptions, RouteResponse, RouteResult, RouteStep, Router, TransportMode,
7};
8use everymap_core::error::EveryMapResult;
9use everymap_core::types::{Coordinate, Polyline};
10use std::sync::Arc;
11
12pub use types::*;
13
14const ROUTING_BASE_URL: &str = "https://api.mapbox.com";
15
16pub struct MapBoxRouter {
18 pub(crate) client: Arc<MapBoxClient>,
19 pub(crate) base_url: String,
20}
21
22impl MapBoxRouter {
23 pub fn new(client: Arc<MapBoxClient>) -> Self {
24 Self {
25 client,
26 base_url: ROUTING_BASE_URL.to_string(),
27 }
28 }
29
30 pub fn with_base_url(client: Arc<MapBoxClient>, base_url: String) -> Self {
31 Self { client, base_url }
32 }
33}
34
35fn transport_mode_to_profile(mode: &TransportMode) -> &'static str {
37 match mode {
38 TransportMode::Car => "driving",
39 TransportMode::Truck => {
40 log::warn!(
41 "MapBox Directions API does not support truck profile, falling back to driving"
42 );
43 "driving"
44 }
45 TransportMode::Pedestrian => "walking",
46 TransportMode::Bicycle => "cycling",
47 TransportMode::Scooter => {
48 log::warn!(
49 "MapBox Directions API does not support scooter profile, falling back to driving"
50 );
51 "driving"
52 }
53 TransportMode::Bus => {
54 log::warn!(
55 "MapBox Directions API does not support bus profile, falling back to driving"
56 );
57 "driving"
58 }
59 TransportMode::Taxi => {
60 log::warn!(
61 "MapBox Directions API does not support taxi profile, falling back to driving"
62 );
63 "driving"
64 }
65 TransportMode::Unknown => "driving",
66 }
67}
68
69impl From<MapBoxRoute> for RouteResult {
70 fn from(route: MapBoxRoute) -> Self {
71 let points: Vec<Coordinate> = route
73 .geometry
74 .as_ref()
75 .and_then(|encoded| everymap_core::types::FlexiblePolyline::decode(encoded).ok())
76 .unwrap_or_default();
77
78 let steps: Vec<RouteStep> = route
79 .legs
80 .iter()
81 .flat_map(|leg| leg.steps.iter())
82 .map(|step| RouteStep {
83 instruction: step.instruction.clone().or(step.name.clone()),
84 distance: Some(step.distance),
85 duration: Some(step.duration),
86 start_coordinate: step.maneuver.as_ref().and_then(|m| {
87 m.location.as_ref().and_then(|loc| {
88 if loc.len() >= 2 {
89 Some(Coordinate::new(loc[1], loc[0]).unwrap_or(Coordinate::ORIGIN))
90 } else {
91 None
92 }
93 })
94 }),
95 end_coordinate: None,
96 })
97 .collect();
98
99 RouteResult {
100 distance: route.distance,
101 duration: route.duration,
102 geometry: Polyline::new(points),
103 transport_mode: None,
104 steps,
105 bounding_box: None,
106 raw: None,
107 }
108 }
109}
110
111#[async_trait]
112impl Router for MapBoxRouter {
113 async fn calculate_route(
114 &self,
115 start: &Coordinate,
116 end: &Coordinate,
117 options: &RouteOptions,
118 ) -> EveryMapResult<RouteResponse> {
119 let profile = options
120 .transport_mode
121 .as_ref()
122 .map(|m| transport_mode_to_profile(m))
123 .unwrap_or("driving");
124 let coords = format!("{},{};{},{}", start.lng, start.lat, end.lng, end.lat);
125 let url = format!(
126 "{}/directions/v5/mapbox/{}/{}",
127 self.base_url, profile, coords
128 );
129
130 let mut params: Vec<(&str, String)> = vec![
131 ("overview", "full".to_string()),
132 ("geometries", "polyline".to_string()),
133 ("steps", "true".to_string()),
134 ];
135
136 if let Some(alternatives) = options.alternatives {
137 params.push(("alternatives", alternatives.to_string()));
138 }
139 if !options.avoid.is_empty() {
140 log::warn!(
141 "MapBox Directions API v5 does not support avoid restrictions; \
142 avoid will be ignored"
143 );
144 }
145 if options.language.is_some() {
146 log::warn!(
147 "MapBox Directions API v5 does not support a language parameter; \
148 language will be ignored"
149 );
150 }
151 if options.departure_time.is_some() {
152 log::warn!(
153 "MapBox Directions API v5 does not support departure_time; \
154 departure_time will be ignored"
155 );
156 }
157 if options.arrival_time.is_some() {
158 log::warn!(
159 "MapBox Directions API v5 does not support arrival_time; \
160 arrival_time will be ignored"
161 );
162 }
163 if let Some(extra) = &options.provider_extra {
164 if let Some(obj) = extra.as_object() {
165 if let Some(v) = obj.get("annotations").and_then(|v| v.as_str()) {
166 params.push(("annotations", v.to_string()));
167 }
168 if let Some(v) = obj.get("continue_straight").and_then(|v| v.as_bool()) {
169 params.push(("continue_straight", v.to_string()));
170 }
171 if let Some(v) = obj.get("exclude").and_then(|v| v.as_str()) {
172 params.push(("exclude", v.to_string()));
173 }
174 }
175 }
176
177 let builder = self
178 .client
179 .build_request(reqwest::Method::GET, &url)
180 .query(¶ms);
181
182 let result: MapBoxRouteResponse = self.client.request_json(builder).await?;
183 Ok(RouteResponse {
184 routes: result.routes.into_iter().map(|r| r.into()).collect(),
185 })
186 }
187}