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}