Skip to main content

everymap_providers_mapbox/domain/routing/
mod.rs

1pub 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
16/// Implementation of Router for MapBox Directions API v5.
17pub 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
35/// Convert core TransportMode to MapBox profile string.
36fn 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        // Decode polyline geometry if present; otherwise extract from legs
72        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(&params);
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}