ichen_openprotocol/geo_location.rs
1use super::utils::*;
2use super::R32;
3use derive_more::*;
4use serde::{Deserialize, Serialize};
5use std::convert::{TryFrom, TryInto};
6
7/// A data structure containing a single physical geo-location.
8///
9#[derive(Display, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize)]
10#[display(fmt = "({},{})", geo_latitude, geo_longitude)]
11#[serde(try_from = "GeoWrapper", into = "GeoWrapper")]
12pub struct GeoLocation {
13 /// Latitude
14 geo_latitude: R32,
15 //
16 /// Longitude
17 geo_longitude: R32,
18}
19
20impl std::fmt::Debug for GeoLocation {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(f, "{}", self)
23 }
24}
25
26impl GeoLocation {
27 /// Get the latitude.
28 ///
29 /// # Examples
30 ///
31 /// ~~~
32 /// # use ichen_openprotocol::*;
33 /// # fn main() -> std::result::Result<(), String> {
34 /// let geo = GeoLocation::new(12.345, -98.765)?;
35 /// assert_eq!(12.345, geo.latitude());
36 /// # Ok(())
37 /// # }
38 /// ~~~
39 pub fn latitude(self) -> f32 {
40 self.geo_latitude.into()
41 }
42
43 /// Get the longitude
44 ///
45 /// # Examples
46 ///
47 /// ~~~
48 /// # use ichen_openprotocol::*;
49 /// # fn main() -> std::result::Result<(), String> {
50 /// let geo = GeoLocation::new(12.345, -98.765)?;
51 /// assert_eq!(-98.765, geo.longitude());
52 /// # Ok(())
53 /// # }
54 /// ~~~
55 pub fn longitude(self) -> f32 {
56 self.geo_longitude.into()
57 }
58
59 /// Create a new `GeoLocation`.
60 ///
61 /// # Errors
62 ///
63 /// Returns `Err(String)` if either `latitude` or `longitude` is not a valid floating-point
64 /// number, or when they together do not represent a valid geo-location position.
65 ///
66 /// ## Error Examples
67 ///
68 /// ~~~
69 /// # use ichen_openprotocol::*;
70 /// assert_eq!(
71 /// Err("Infinity is not a supported value for longitude".into()),
72 /// GeoLocation::new(23.456, std::f32::NEG_INFINITY)
73 /// );
74 ///
75 /// assert_eq!(
76 /// Err("invalid latitude: 123.456 (must be between -90 and 90)".into()),
77 /// GeoLocation::new(123.456, 987.654)
78 /// );
79 /// ~~~
80 ///
81 /// # Examples
82 ///
83 /// ~~~
84 /// # use ichen_openprotocol::*;
85 /// # fn main() -> std::result::Result<(), String> {
86 /// let geo = GeoLocation::new(12.345, -98.765)?;
87 /// assert_eq!(12.345, geo.latitude());
88 /// assert_eq!(-98.765, geo.longitude());
89 /// # Ok(())
90 /// # }
91 /// ~~~
92 pub fn new(latitude: f32, longitude: f32) -> std::result::Result<Self, String> {
93 check_f32(latitude).map_err(|e| format!("{} for latitude", e))?;
94 check_f32(longitude).map_err(|e| format!("{} for longitude", e))?;
95
96 Self::check_constraints(latitude, longitude)?;
97
98 Ok(Self {
99 geo_latitude: latitude.try_into().unwrap(),
100 geo_longitude: longitude.try_into().unwrap(),
101 })
102 }
103
104 // Check if the latitude/longitude pair is with constraints.
105 fn check_constraints(latitude: f32, longitude: f32) -> Result<(), String> {
106 if !(-90.0..=90.0).contains(&latitude) {
107 Err(format!("invalid latitude: {} (must be between -90 and 90)", latitude))
108 } else if !(-180.0..=180.0).contains(&longitude) {
109 Err(format!("invalid longitude: {} (must be between -180 and 180)", longitude))
110 } else {
111 Ok(())
112 }
113 }
114}
115
116// Wrapper for serialization/deserialization
117#[derive(Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119struct GeoWrapper {
120 pub geo_latitude: f32,
121 pub geo_longitude: f32,
122}
123
124impl TryFrom<GeoWrapper> for GeoLocation {
125 type Error = String;
126
127 fn try_from(value: GeoWrapper) -> Result<Self, Self::Error> {
128 Self::new(value.geo_latitude, value.geo_longitude)
129 }
130}
131
132impl From<GeoLocation> for GeoWrapper {
133 fn from(value: GeoLocation) -> Self {
134 Self { geo_latitude: value.latitude(), geo_longitude: value.longitude() }
135 }
136}