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}