everymap_providers_mapbox/domain/isoline/
mod.rs1pub mod types;
2
3use crate::client::MapBoxClient;
4use async_trait::async_trait;
5use everymap_core::domains::isoline::{
6 IsolineOptions, IsolineProvider, IsolineResponse, IsolineResult, RangeType,
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 ISOCHRONE_BASE_URL: &str = "https://api.mapbox.com";
16
17pub struct MapBoxIsoline {
19 pub(crate) client: Arc<MapBoxClient>,
20 pub(crate) base_url: String,
21}
22
23impl MapBoxIsoline {
24 pub fn new(client: Arc<MapBoxClient>) -> Self {
25 Self {
26 client,
27 base_url: ISOCHRONE_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 Isochrone 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 Isochrone API does not support bus profile, falling back to driving"
51 );
52 "driving"
53 }
54 TransportMode::Taxi => {
55 log::warn!(
56 "MapBox Isochrone API does not support taxi profile, falling back to driving"
57 );
58 "driving"
59 }
60 TransportMode::Scooter => {
61 log::warn!(
62 "MapBox Isochrone API does not support scooter profile, falling back to driving"
63 );
64 "driving"
65 }
66 TransportMode::Unknown => "driving",
67 }
68}
69
70fn extract_polygon_coordinates(geom: &serde_json::Value) -> Vec<Coordinate> {
71 geom.as_array()
73 .and_then(|rings| rings.first())
74 .and_then(|ring| ring.as_array())
75 .map(|ring| {
76 ring.iter()
77 .filter_map(|coordinate| {
78 let arr = coordinate.as_array()?;
79 if arr.len() >= 2 {
80 Some(
81 Coordinate::new(arr[1].as_f64()?, arr[0].as_f64()?)
82 .unwrap_or(Coordinate::ORIGIN),
83 )
84 } else {
85 None
86 }
87 })
88 .collect()
89 })
90 .unwrap_or_default()
91}
92
93#[async_trait]
94impl IsolineProvider for MapBoxIsoline {
95 async fn get_isoline(
96 &self,
97 center: &Coordinate,
98 range: f64,
99 options: &IsolineOptions,
100 ) -> EveryMapResult<IsolineResponse> {
101 let profile = options
102 .transport_mode
103 .as_ref()
104 .map(|m| transport_mode_to_profile(m))
105 .unwrap_or("driving");
106 let coords = format!("{},{}", center.lng, center.lat);
107 let url = format!(
108 "{}/isochrone/v1/mapbox/{}/{}",
109 self.base_url, profile, coords
110 );
111
112 let mut params: Vec<(&str, String)> = Vec::new();
113
114 match options.range_type.as_ref().unwrap_or(&RangeType::Time) {
115 RangeType::Time => {
116 let minutes = (range / 60.0).round() as u64;
118 params.push(("contours_minutes", minutes.to_string()));
119 }
120 RangeType::Distance => {
121 let meters = range.round() as u64;
123 params.push(("contours_meters", meters.to_string()));
124 }
125 RangeType::Consumption => {
126 return Err(everymap_core::error::EveryMapError::provider(
127 "mapbox",
128 "UNSUPPORTED_RANGE_TYPE",
129 "MapBox Isochrone API does not support consumption-based ranges",
130 ));
131 }
132 }
133
134 if let Some(departure) = &options.departure_time {
135 log::warn!(
136 "MapBox Isochrone API does not support departure_time; \
137 departure_time ({}) will be ignored",
138 departure
139 );
140 }
141 if !options.avoid.is_empty() {
142 log::warn!(
143 "MapBox Isochrone API does not support avoid restrictions; \
144 avoid will be ignored"
145 );
146 }
147
148 if let Some(extra) = &options.provider_extra {
149 if let Some(obj) = extra.as_object() {
150 if let Some(v) = obj.get("denoise").and_then(|v| v.as_f64()) {
151 params.push(("denoise", v.to_string()));
152 }
153 if let Some(v) = obj.get("generalize").and_then(|v| v.as_f64()) {
154 params.push(("generalize", v.to_string()));
155 }
156 }
157 }
158
159 params.push(("polygons", "true".to_string()));
160
161 let builder = self
162 .client
163 .build_request(reqwest::Method::GET, &url)
164 .query(¶ms);
165
166 let result: MapBoxIsochroneResponse = self.client.request_json(builder).await?;
167
168 let isolines: Vec<IsolineResult> = result
169 .features
170 .into_iter()
171 .map(|f| {
172 let polygon = f
173 .geometry
174 .and_then(|g| g.coordinates)
175 .map(|coords| extract_polygon_coordinates(&coords))
176 .unwrap_or_default();
177
178 let range_val = f.properties.and_then(|p| p.contour).map(|c| {
179 match options.range_type.as_ref().unwrap_or(&RangeType::Time) {
180 RangeType::Time => (c as f64) * 60.0, RangeType::Distance => c as f64, RangeType::Consumption => c as f64,
183 }
184 });
185
186 IsolineResult {
187 polygon,
188 range: range_val,
189 }
190 })
191 .collect();
192
193 Ok(IsolineResponse {
194 isolines,
195 raw: None,
196 })
197 }
198}