Skip to main content

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