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}