valhalla_client/
lib.rs

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