everymap_providers_mapbox/domain/tour/
mod.rs1pub 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
14pub 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(¶ms);
90
91 let result: MapBoxOptimizationResponse = self.client.request_json(builder).await?;
92
93 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}