gufo_common/
geography.rs

1#[derive(Debug, Clone, Copy)]
2pub struct Location {
3    pub lat: Coord,
4    pub lon: Coord,
5}
6
7impl Location {
8    pub fn new_from_coord(lat: Coord, lon: Coord) -> Self {
9        Self { lat, lon }
10    }
11
12    pub fn from_ref_coord(
13        lat_ref: LatRef,
14        lat: (f64, f64, f64),
15        lon_ref: LonRef,
16        lon: (f64, f64, f64),
17    ) -> Self {
18        let lat = Coord::from_sign_deg_min_sec(lat_ref.as_sign(), lat);
19        let lon = Coord::from_sign_deg_min_sec(lon_ref.as_sign(), lon);
20
21        Self { lat, lon }
22    }
23
24    pub fn lat_ref_deg_min_sec(&self) -> (LatRef, (f64, f64, f64)) {
25        let (deg, min, sec) = self.lat.as_deg_min_sec();
26        (LatRef::from_sign(deg), (deg.abs(), min, sec))
27    }
28
29    pub fn lon_ref_deg_min_sec(&self) -> (LonRef, (f64, f64, f64)) {
30        let (deg, min, sec) = self.lon.as_deg_min_sec();
31        (LonRef::from_sign(deg), (deg.abs(), min, sec))
32    }
33
34    /// Return coordinate according to ISO 6709 Annex D
35    ///
36    /// <https://en.wikipedia.org/wiki/ISO_6709>
37    ///
38    /// ```
39    /// # use gufo_common::geography::*;
40    /// let lat = Coord::from_deg_min_sec((-46., 14., 6.));
41    /// let lon = Coord::from_deg_min_sec((126., 4., 6.70234));
42    /// let loc = Location::new_from_coord(lat, lon);
43    /// assert_eq!(loc.iso_6709(), r#"46°14'06"S 126°04'06.7"E"#);
44    pub fn iso_6709(&self) -> String {
45        let (lat_ref, (lat_deg, lat_min, lat_sec)) = self.lat_ref_deg_min_sec();
46        let (lon_ref, (lon_deg, lon_min, lon_sec)) = self.lon_ref_deg_min_sec();
47
48        fn pad_one_0(v: f64) -> String {
49            let s = format!("{v}");
50
51            let pre_decimal = s.split_once('.').map_or(s.as_str(), |x| x.0);
52
53            if pre_decimal.len() == 1 {
54                format!("0{s}")
55            } else {
56                s
57            }
58        }
59
60        let lat_sec = pad_one_0(lat_sec);
61        let lon_sec = pad_one_0(lon_sec);
62
63        format!("{lat_deg}°{lat_min:02}'{lat_sec}\"{lat_ref} {lon_deg}°{lon_min:02}'{lon_sec}\"{lon_ref}")
64    }
65
66    /// Locations as `geo:` URI
67    ///
68    /// The precision of the coordinates is limited to six decimal places.
69    pub fn geo_uri(&self) -> String {
70        let lat = self.lat.0;
71        let lon = self.lon.0;
72        // six decimal places gives as more than a meter accuracy
73        format!("geo:{lat:.6},{lon:.6}")
74    }
75}
76
77#[derive(Debug, Clone, Copy)]
78pub struct Coord(pub f64);
79
80impl Coord {
81    /// Return coordinate as degrees, minutes, seconds
82    ///
83    /// ```
84    /// # use gufo_common::geography::*;
85    /// let ang = Coord::from_deg_min_sec((-46., 14., 6.70));
86    /// assert_eq!(ang.as_deg_min_sec(), (-46., 14., 6.70));
87    /// ```
88    pub fn as_deg_min_sec(&self) -> (f64, f64, f64) {
89        let deg = self.0;
90        let h = deg.fract().abs() * 60.;
91        let s = h.fract() * 60.;
92
93        (deg.trunc(), h.trunc(), (s * 100.).round() / 100.)
94    }
95
96    ///
97    ///
98    /// ```
99    /// # use gufo_common::geography::*;
100    /// let ang = Coord::from_deg_min_sec((-89., 24., 2.2));
101    /// assert_eq!((ang.0 * 100_000.).round() / 100_000., -89.40061);
102    /// ```
103    pub fn from_deg_min_sec((deg, min, sec): (f64, f64, f64)) -> Self {
104        let sign = deg.signum();
105        Coord(deg + sign * min / 60. + sign * sec / 60. / 60.)
106    }
107
108    ///
109    ///
110    /// ```
111    /// # use gufo_common::geography::*;
112    /// let ang = Coord::from_sign_deg_min_sec(LatRef::South.as_sign(), (89., 24., 2.2));
113    /// assert_eq!((ang.0 * 100_000.).round() / 100_000., -89.40061);
114    /// ```
115    pub fn from_sign_deg_min_sec(sign: f64, deg_min_sec: (f64, f64, f64)) -> Self {
116        Self(sign * Self::from_deg_min_sec(deg_min_sec).0)
117    }
118}
119
120#[derive(Debug, Clone, Copy)]
121pub enum LatRef {
122    North,
123    South,
124}
125
126impl LatRef {
127    pub fn from_sign(sign: f64) -> Self {
128        if sign >= 0. {
129            Self::North
130        } else {
131            Self::South
132        }
133    }
134
135    pub fn as_sign(&self) -> f64 {
136        match self {
137            Self::North => 1.,
138            Self::South => -1.,
139        }
140    }
141}
142
143impl TryFrom<&str> for LatRef {
144    type Error = InvalidLatRef;
145    fn try_from(value: &str) -> Result<Self, Self::Error> {
146        match value {
147            "N" => Ok(Self::North),
148            "S" => Ok(Self::South),
149            v => Err(Self::Error::InvalidLatitudeRef(v.to_string())),
150        }
151    }
152}
153
154#[derive(Debug, Clone, thiserror::Error)]
155pub enum InvalidLatRef {
156    #[error("Invalid latitude reference: '{0}'. Must be 'N' or 'S'.")]
157    InvalidLatitudeRef(String),
158}
159
160impl std::fmt::Display for LatRef {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        match self {
163            Self::North => f.write_str("N"),
164            Self::South => f.write_str("S"),
165        }
166    }
167}
168
169#[derive(Debug, Clone, Copy)]
170pub enum LonRef {
171    East,
172    West,
173}
174
175impl LonRef {
176    pub fn from_sign(sign: f64) -> Self {
177        if sign >= 0. {
178            Self::East
179        } else {
180            Self::West
181        }
182    }
183
184    pub fn as_sign(&self) -> f64 {
185        match self {
186            Self::East => 1.,
187            Self::West => -1.,
188        }
189    }
190}
191
192impl TryFrom<&str> for LonRef {
193    type Error = InvalidLonRef;
194    fn try_from(value: &str) -> Result<Self, Self::Error> {
195        match value {
196            "E" => Ok(Self::East),
197            "W" => Ok(Self::West),
198            v => Err(Self::Error::InvalidLonRef(v.to_string())),
199        }
200    }
201}
202
203#[derive(Debug, Clone, thiserror::Error)]
204pub enum InvalidLonRef {
205    #[error("Invalid latitude reference: '{0}'. Must be 'E' or 'W'.")]
206    InvalidLonRef(String),
207}
208
209impl std::fmt::Display for LonRef {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        match self {
212            Self::East => f.write_str("E"),
213            Self::West => f.write_str("W"),
214        }
215    }
216}