valhalla_client/lib.rs
1#![forbid(unsafe_code)]
2#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
3
4/// [`costing`] model-configuration for different transport modes
5pub mod costing;
6/// Models connected to the [`elevation`]-api
7pub mod elevation;
8/// Models connected to the Time-distance [`matrix`]-api
9pub mod matrix;
10/// Models connected to the Turn-by-turn [`route`]ing-api
11pub mod route;
12/// Shape decoding support for [`route`] and [`elevation`]
13pub mod shapes;
14/// Models connected to the healthcheck via the [`status`]-API
15pub mod status;
16
17use log::trace;
18use serde::{Deserialize, Serialize};
19
20/// A longitude, latitude coordinate in degrees
21///
22/// See <https://en.wikipedia.org/wiki/Geographic_coordinate_system> for further context
23pub type Coordinate = (f32, f32);
24impl From<Coordinate> for shapes::ShapePoint {
25 fn from((lon, lat): Coordinate) -> Self {
26 Self {
27 lon: lon as f64,
28 lat: lat as f64,
29 }
30 }
31}
32
33#[derive(Deserialize, Debug, Clone)]
34pub struct CodedDescription {
35 pub code: u64,
36 pub description: String,
37}
38
39/// valhalla needs `date_time` fields to be in the `YYYY-MM-DDTHH:MM` format
40pub(crate) fn serialize_naive_date_time_opt<S>(
41 value: &Option<chrono::NaiveDateTime>,
42 serializer: S,
43) -> Result<S::Ok, S::Error>
44where
45 S: serde::Serializer,
46{
47 match value {
48 None => serializer.serialize_none(),
49 Some(value) => serialize_naive_date_time(value, serializer),
50 }
51}
52
53/// valhalla needs `date_time` fields to be in the `YYYY-MM-DDTHH:MM` format
54fn serialize_naive_date_time<S>(
55 value: &chrono::NaiveDateTime,
56 serializer: S,
57) -> Result<S::Ok, S::Error>
58where
59 S: serde::Serializer,
60{
61 serializer.serialize_str(&value.format("%Y-%m-%dT%H:%M").to_string())
62}
63
64#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Units {
66 #[default]
67 #[serde(rename = "kilometers")]
68 Metric,
69
70 #[serde(rename = "miles")]
71 Imperial,
72}
73/// The local date and time at the location
74#[derive(Serialize, Debug)]
75pub struct DateTime {
76 r#type: MatrixDateTimeType,
77 #[serde(serialize_with = "serialize_naive_date_time")]
78 value: chrono::NaiveDateTime,
79}
80
81impl DateTime {
82 /// Current departure time
83 pub fn from_current_departure_time() -> Self {
84 Self {
85 r#type: MatrixDateTimeType::CurrentDeparture,
86 value: chrono::Local::now().naive_local(),
87 }
88 }
89 /// Specified departure time
90 pub fn from_departure_time(depart_after: chrono::NaiveDateTime) -> Self {
91 Self {
92 r#type: MatrixDateTimeType::SpecifiedDeparture,
93 value: depart_after,
94 }
95 }
96 /// Specified arrival time
97 pub fn from_arrival_time(arrive_by: chrono::NaiveDateTime) -> Self {
98 Self {
99 r#type: MatrixDateTimeType::SpecifiedArrival,
100 value: arrive_by,
101 }
102 }
103}
104
105#[derive(serde_repr::Serialize_repr, Debug, Clone, Copy)]
106#[repr(u8)]
107enum MatrixDateTimeType {
108 CurrentDeparture = 0,
109 SpecifiedDeparture,
110 SpecifiedArrival,
111}
112
113#[derive(Debug)]
114pub enum Error {
115 Reqwest(reqwest::Error),
116 Url(url::ParseError),
117 Serde(serde_json::Error),
118 RemoteError(RemoteError),
119}
120
121impl std::fmt::Display for Error {
122 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
123 match self {
124 Self::Reqwest(e) => write!(f, "reqwest error: {e}"),
125 Self::Url(e) => write!(f, "url error: {e}"),
126 Self::Serde(e) => write!(f, "serde error: {e}"),
127 Self::RemoteError(e) => write!(f, "remote error: {e:?}"),
128 }
129 }
130}
131
132impl std::error::Error for Error {}
133
134#[derive(Debug, Deserialize)]
135pub struct RemoteError {
136 pub error_code: isize,
137 pub error: String,
138 pub status_code: isize,
139 pub status: String,
140}
141
142/// synchronous ("blocking") client implementation
143#[cfg(feature = "blocking")]
144pub mod blocking {
145 use crate::{elevation, matrix, route, status, Error, VALHALLA_PUBLIC_API_URL};
146 use std::sync::Arc;
147
148 #[derive(Debug, Clone)]
149 pub struct Valhalla {
150 runtime: Arc<tokio::runtime::Runtime>,
151 client: super::Valhalla,
152 }
153 impl Valhalla {
154 /// Create a sync [Valhalla](https://valhalla.github.io/valhalla/) client
155 pub fn new(base_url: url::Url) -> Self {
156 let runtime = tokio::runtime::Builder::new_current_thread()
157 .enable_io()
158 .build()
159 .expect("tokio runtime can be created");
160 Self {
161 runtime: Arc::new(runtime),
162 client: super::Valhalla::new(base_url),
163 }
164 }
165
166 /// Make a turn-by-turn routing request
167 ///
168 /// See <https://valhalla.github.io/valhalla/api/turn-by-turn/api-reference> for details
169 ///
170 /// # Example:
171 /// ```rust,no_run
172 /// use valhalla_client::blocking::Valhalla;
173 /// use valhalla_client::route::{Location, Manifest,};
174 /// use valhalla_client::costing::Costing;
175 ///
176 /// let amsterdam = Location::new(4.9041, 52.3676);
177 /// let utrecht = Location::new(5.1214, 52.0907);
178 ///
179 /// let manifest = Manifest::builder()
180 /// .locations([utrecht,amsterdam])
181 /// .alternates(2)
182 /// .costing(Costing::Auto(Default::default()))
183 /// .language("de-De");
184 ///
185 /// let response = Valhalla::default().route(manifest).unwrap();
186 /// # use valhalla_client::matrix::Response;
187 /// # assert!(response.warnings.is_none());
188 /// # assert_eq!(response.locations.len(), 2);
189 /// ```
190 pub fn route(&self, manifest: route::Manifest) -> Result<route::Trip, Error> {
191 self.runtime
192 .block_on(async move { self.client.route(manifest).await })
193 }
194 /// Make a time-distance matrix routing request
195 ///
196 /// See <https://valhalla.github.io/valhalla/api/matrix/api-reference> for details
197 ///
198 /// # Example:
199 /// ```rust,no_run
200 /// use valhalla_client::blocking::Valhalla;
201 /// use valhalla_client::matrix::{DateTime, Location, Manifest,};
202 /// use valhalla_client::costing::Costing;
203 ///
204 /// let amsterdam = Location::new(4.9041, 52.3676);
205 /// let utrecht = Location::new(5.1214, 52.0907);
206 /// let rotterdam = Location::new(4.4775302894411, 51.92485867761482);
207 /// let den_haag = Location::new(4.324908478055228, 52.07934071633195);
208 ///
209 /// let manifest = Manifest::builder()
210 /// .verbose_output(true)
211 /// .sources_to_targets([utrecht],[amsterdam,rotterdam,den_haag])
212 /// .date_time(DateTime::from_departure_time(chrono::Local::now().naive_local()))
213 /// .costing(Costing::Auto(Default::default()));
214 ///
215 /// let response = Valhalla::default()
216 /// .matrix(manifest)
217 /// .unwrap();
218 /// # use valhalla_client::matrix::Response;
219 /// # if let Response::Verbose(r) = response{
220 /// # assert!(r.warnings.is_empty());
221 /// # assert_eq!(r.sources.len(),1);
222 /// # assert_eq!(r.targets.len(),3);
223 /// # };
224 /// ```
225 pub fn matrix(&self, manifest: matrix::Manifest) -> Result<matrix::Response, Error> {
226 self.runtime
227 .block_on(async move { self.client.matrix(manifest).await })
228 }
229 /// Make an elevation request
230 ///
231 /// Valhalla's elevation lookup service provides digital elevation model (DEM) data as the result of a query.
232 /// The elevation service data has many applications when combined with other routing and navigation data, including computing the steepness of roads and paths or generating an elevation profile chart along a route.
233 ///
234 /// For example, you can get elevation data for a point, a trail, or a trip.
235 /// You might use the results to consider hills for your bicycle trip, or when estimating battery usage for trips in electric vehicles.
236 ///
237 /// See <https://valhalla.github.io/valhalla/api/elevation/api-reference/> for details
238 ///
239 /// # Example:
240 ///
241 /// ```rust,no_run
242 /// use valhalla_client::blocking::Valhalla;
243 /// use valhalla_client::elevation::Manifest;
244 ///
245 /// let request = Manifest::builder()
246 /// .shape([
247 /// (40.712431, -76.504916),
248 /// (40.712275, -76.605259),
249 /// (40.712122, -76.805694),
250 /// (40.722431, -76.884916),
251 /// (40.812275, -76.905259),
252 /// (40.912122, -76.965694),
253 /// ])
254 /// .include_range();
255 /// let response = Valhalla::default()
256 /// .elevation(request).unwrap();
257 /// # assert!(response.height.is_empty());
258 /// # assert_eq!(response.range_height.len(), 6);
259 /// # assert!(response.encoded_polyline.is_none());
260 /// # assert!(response.warnings.is_empty());
261 /// # assert_eq!(response.x_coordinate, None);
262 /// # assert_eq!(response.y_coordinate, None);
263 /// # assert_eq!(response.shape.map(|s|s.len()),Some(6));
264 /// ```
265 pub fn elevation(
266 &self,
267 manifest: elevation::Manifest,
268 ) -> Result<elevation::Response, Error> {
269 self.runtime
270 .block_on(async move { self.client.elevation(manifest).await })
271 }
272 /// Make a status request
273 ///
274 /// This can be used as a health endpoint for the HTTP API or to toggle features in a frontend.
275 ///
276 /// See <https://valhalla.github.io/valhalla/api/status/api-reference/> for details
277 ///
278 /// # Example:
279 /// ```rust,no_run
280 /// use valhalla_client::blocking::Valhalla;
281 /// use valhalla_client::status::Manifest;
282 ///
283 /// let request = Manifest::builder()
284 /// .verbose_output(false);
285 /// let response = Valhalla::default()
286 /// .status(request).unwrap();
287 /// # assert!(response.version >= semver::Version::parse("3.1.4").unwrap());
288 /// # assert!(response.tileset_last_modified.timestamp() > 0);
289 /// # assert!(response.verbose.is_none());
290 /// ```
291 pub fn status(&self, manifest: status::Manifest) -> Result<status::Response, Error> {
292 self.runtime
293 .block_on(async move { self.client.status(manifest).await })
294 }
295 }
296 impl Default for Valhalla {
297 fn default() -> Self {
298 Self::new(
299 url::Url::parse(VALHALLA_PUBLIC_API_URL)
300 .expect("VALHALLA_PUBLIC_API_URL is not a valid url"),
301 )
302 }
303 }
304}
305
306const VALHALLA_PUBLIC_API_URL: &str = "https://valhalla1.openstreetmap.de/";
307#[derive(Debug, Clone)]
308pub struct Valhalla {
309 client: reqwest::Client,
310 base_url: url::Url,
311}
312
313impl Valhalla {
314 /// Create an async [Valhalla](https://valhalla.github.io/valhalla/) client
315 pub fn new(base_url: url::Url) -> Self {
316 Self {
317 client: reqwest::Client::new(),
318 base_url,
319 }
320 }
321
322 /// Make a turn-by-turn routing request
323 ///
324 /// See <https://valhalla.github.io/valhalla/api/turn-by-turn/api-reference> for details
325 ///
326 /// # Example:
327 /// ```rust
328 /// # async fn route(){
329 /// use valhalla_client::Valhalla;
330 /// use valhalla_client::route::{Location, Manifest,};
331 /// use valhalla_client::costing::Costing;
332 ///
333 /// let amsterdam = Location::new(4.9041, 52.3676);
334 /// let utrecht = Location::new(5.1214, 52.0907);
335 ///
336 /// let manifest = Manifest::builder()
337 /// .locations([utrecht,amsterdam])
338 /// .alternates(2)
339 /// .costing(Costing::Auto(Default::default()))
340 /// .language("de-De");
341 ///
342 /// let response = Valhalla::default().route(manifest).await.unwrap();
343 /// # assert!(response.warnings.is_none());
344 /// # assert_eq!(response.locations.len(), 2);
345 /// # }
346 /// ```
347 pub async fn route(&self, manifest: route::Manifest) -> Result<route::Trip, Error> {
348 let response: route::Response = self.do_request(manifest, "route", "route").await?;
349 Ok(response.trip)
350 }
351
352 /// Make a time-distance matrix routing request
353 ///
354 /// See <https://valhalla.github.io/valhalla/api/matrix/api-reference> for details
355 ///
356 /// # Example:
357 /// ```rust
358 /// # async fn matrix(){
359 /// use valhalla_client::Valhalla;
360 /// use valhalla_client::matrix::{DateTime, Location, Manifest,};
361 /// use valhalla_client::costing::Costing;
362 ///
363 /// let amsterdam = Location::new(4.9041, 52.3676);
364 /// let utrecht = Location::new(5.1214, 52.0907);
365 /// let rotterdam = Location::new(4.4775302894411, 51.92485867761482);
366 /// let den_haag = Location::new(4.324908478055228, 52.07934071633195);
367 ///
368 /// let manifest = Manifest::builder()
369 /// .verbose_output(true)
370 /// .sources_to_targets([utrecht],[amsterdam,rotterdam,den_haag])
371 /// .date_time(DateTime::from_departure_time(chrono::Local::now().naive_local()))
372 /// .costing(Costing::Auto(Default::default()));
373 ///
374 /// let response = Valhalla::default()
375 /// .matrix(manifest).await
376 /// .unwrap();
377 /// # use valhalla_client::matrix::Response;
378 /// # if let Response::Verbose(r) = response{
379 /// # assert!(r.warnings.is_empty());
380 /// # assert_eq!(r.sources.len(),1);
381 /// # assert_eq!(r.targets.len(),3);
382 /// # };
383 /// # }
384 /// ```
385 pub async fn matrix(&self, manifest: matrix::Manifest) -> Result<matrix::Response, Error> {
386 debug_assert_ne!(
387 manifest.targets.len(),
388 0,
389 "a matrix route needs at least one target specified"
390 );
391 debug_assert_ne!(
392 manifest.sources.len(),
393 0,
394 "a matrix route needs at least one source specified"
395 );
396
397 self.do_request(manifest, "sources_to_targets", "matrix")
398 .await
399 }
400 /// Make an elevation request
401 ///
402 /// Valhalla's elevation lookup service provides digital elevation model (DEM) data as the result of a query.
403 /// The elevation service data has many applications when combined with other routing and navigation data, including computing the steepness of roads and paths or generating an elevation profile chart along a route.
404 ///
405 /// For example, you can get elevation data for a point, a trail, or a trip.
406 /// You might use the results to consider hills for your bicycle trip, or when estimating battery usage for trips in electric vehicles.
407 ///
408 /// See <https://valhalla.github.io/valhalla/api/elevation/api-reference/> for details
409 ///
410 /// # Example:
411 ///
412 /// ```rust,no_run
413 /// # async fn elevation() {
414 /// use valhalla_client::Valhalla;
415 /// use valhalla_client::elevation::Manifest;
416 ///
417 /// let request = Manifest::builder()
418 /// .shape([
419 /// (40.712431, -76.504916),
420 /// (40.712275, -76.605259),
421 /// (40.712122, -76.805694),
422 /// (40.722431, -76.884916),
423 /// (40.812275, -76.905259),
424 /// (40.912122, -76.965694),
425 /// ])
426 /// .include_range();
427 /// let response = Valhalla::default()
428 /// .elevation(request).await.unwrap();
429 /// # assert!(response.height.is_empty());
430 /// # assert_eq!(response.range_height.len(), 6);
431 /// # assert!(response.encoded_polyline.is_none());
432 /// # assert!(response.warnings.is_empty());
433 /// # assert_eq!(response.x_coordinate, None);
434 /// # assert_eq!(response.y_coordinate, None);
435 /// # assert_eq!(response.shape.map(|s|s.len()),Some(6));
436 /// # }
437 /// ```
438 pub async fn elevation(
439 &self,
440 manifest: elevation::Manifest,
441 ) -> Result<elevation::Response, Error> {
442 self.do_request(manifest, "height", "elevation").await
443 }
444 /// Make a status request
445 ///
446 /// This can be used as a health endpoint for the HTTP API or to toggle features in a frontend.
447 ///
448 /// See <https://valhalla.github.io/valhalla/api/status/api-reference/> for details
449 ///
450 /// # Example:
451 /// ```rust,no_run
452 /// # async fn status(){
453 /// use valhalla_client::Valhalla;
454 /// use valhalla_client::status::Manifest;
455 ///
456 /// let request = Manifest::builder()
457 /// .verbose_output(false);
458 /// let response = Valhalla::default()
459 /// .status(request).await.unwrap();
460 /// # assert!(response.version >= semver::Version::parse("3.1.4").unwrap());
461 /// # assert!(response.tileset_last_modified.timestamp() > 0);
462 /// # assert!(response.verbose.is_none());
463 /// # }
464 /// ```
465 pub async fn status(&self, manifest: status::Manifest) -> Result<status::Response, Error> {
466 self.do_request(manifest, "status", "status").await
467 }
468
469 async fn do_request<Resp: for<'de> serde::Deserialize<'de>>(
470 &self,
471 manifest: impl serde::Serialize,
472 path: &'static str,
473 name: &'static str,
474 ) -> Result<Resp, Error> {
475 if log::log_enabled!(log::Level::Trace) {
476 let request = serde_json::to_string(&manifest).unwrap();
477 trace!("Sending {name} request: {request}");
478 }
479 let mut url = self.base_url.clone();
480 url.path_segments_mut()
481 .expect("base_url is not a valid base url")
482 .push(path);
483 let response = self
484 .client
485 .post(url)
486 .json(&manifest)
487 .send()
488 .await
489 .map_err(Error::Reqwest)?;
490 if response.status().is_client_error() {
491 return Err(Error::RemoteError(
492 response.json().await.map_err(Error::Reqwest)?,
493 ));
494 }
495 response.error_for_status_ref().map_err(Error::Reqwest)?;
496 let text = response.text().await.map_err(Error::Reqwest)?;
497 trace!("{name} responded: {text}");
498 let response: Resp = serde_json::from_str(&text).map_err(Error::Serde)?;
499 Ok(response)
500 }
501}
502
503impl Default for Valhalla {
504 fn default() -> Self {
505 Self::new(
506 url::Url::parse(VALHALLA_PUBLIC_API_URL)
507 .expect("VALHALLA_PUBLIC_API_URL is not a valid url"),
508 )
509 }
510}