Skip to main content

everymap_providers_mapbox/domain/tour/
mod.rs

1pub mod types;
2
3use crate::client::MapBoxClient;
4use async_trait::async_trait;
5use everymap_core::domains::tour::{TourOptions, TourPlanner, TourResponse, TourStop};
6use everymap_core::error::EveryMapResult;
7use everymap_core::types::Coordinate;
8use std::sync::Arc;
9
10pub use types::*;
11
12const OPTIMIZATION_BASE_URL: &str = "https://api.mapbox.com";
13
14/// Implementation of TourPlanner for MapBox Optimization API v1.
15pub struct MapBoxTourPlanner {
16    pub(crate) client: Arc<MapBoxClient>,
17    pub(crate) base_url: String,
18}
19
20impl MapBoxTourPlanner {
21    pub fn new(client: Arc<MapBoxClient>) -> Self {
22        Self {
23            client,
24            base_url: OPTIMIZATION_BASE_URL.to_string(),
25        }
26    }
27
28    pub fn with_base_url(client: Arc<MapBoxClient>, base_url: String) -> Self {
29        Self { client, base_url }
30    }
31}
32
33#[async_trait]
34impl TourPlanner for MapBoxTourPlanner {
35    async fn optimize_tour(
36        &self,
37        stops: &[Coordinate],
38        options: &TourOptions,
39    ) -> EveryMapResult<TourResponse> {
40        if stops.len() < 2 {
41            return Err(everymap_core::error::EveryMapError::provider(
42                "mapbox",
43                "INVALID_INPUT",
44                "At least 2 stops required for tour optimization",
45            ));
46        }
47
48        let profile = options
49            .transport_mode
50            .as_ref()
51            .map(|m| match m {
52                everymap_core::domains::routing::TransportMode::Car => "driving",
53                everymap_core::domains::routing::TransportMode::Truck => "driving",
54                everymap_core::domains::routing::TransportMode::Pedestrian => "walking",
55                everymap_core::domains::routing::TransportMode::Bicycle => "cycling",
56                _ => "driving",
57            })
58            .unwrap_or("driving");
59
60        let coords: String = stops
61            .iter()
62            .map(|c| format!("{},{}", c.lng, c.lat))
63            .collect::<Vec<_>>()
64            .join(";");
65        let url = format!(
66            "{}/optimized-trips/v1/mapbox/{}/{}",
67            self.base_url, profile, coords
68        );
69
70        let mut params: Vec<(&str, String)> = vec![
71            ("overview", "false".to_string()),
72            ("roundtrip", "false".to_string()),
73        ];
74
75        if let Some(extra) = &options.provider_extra {
76            if let Some(obj) = extra.as_object() {
77                if let Some(v) = obj.get("source").and_then(|v| v.as_str()) {
78                    params.push(("source", v.to_string()));
79                }
80                if let Some(v) = obj.get("destination").and_then(|v| v.as_str()) {
81                    params.push(("destination", v.to_string()));
82                }
83            }
84        }
85
86        let builder = self
87            .client
88            .build_request(reqwest::Method::GET, &url)
89            .query(&params);
90
91        let result: MapBoxOptimizationResponse = self.client.request_json(builder).await?;
92
93        // Build tour stops from waypoints in optimized order
94        let mut waypoints: Vec<(usize, Option<Coordinate>)> = result
95            .waypoints
96            .into_iter()
97            .map(|wp| {
98                let coordinate = wp.location.as_ref().and_then(|loc| {
99                    if loc.len() >= 2 {
100                        Some(Coordinate::new(loc[1], loc[0]).unwrap_or(Coordinate::ORIGIN))
101                    } else {
102                        None
103                    }
104                });
105                let waypoint_index = wp.waypoint_index.unwrap_or(0) as usize;
106                (waypoint_index, coordinate)
107            })
108            .collect();
109        waypoints.sort_by_key(|(waypoint_index, _)| *waypoint_index);
110
111        let tour_stops: Vec<TourStop> = waypoints
112            .into_iter()
113            .map(|(_, coordinate)| TourStop {
114                coordinate: coordinate.unwrap_or(Coordinate::ORIGIN),
115                arrival_time: None,
116                departure_time: None,
117                duration: None,
118                distance_from_previous: None,
119            })
120            .collect();
121
122        let (total_distance, total_duration) = result
123            .trips
124            .first()
125            .map(|t| (t.distance, t.duration))
126            .unwrap_or((0.0, 0.0));
127
128        Ok(TourResponse {
129            stops: tour_stops,
130            total_distance: Some(total_distance),
131            total_duration: Some(total_duration),
132            unassigned_count: Some(0),
133            raw: None,
134        })
135    }
136}