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}