1use std::str::FromStr;
2
3use iso6709parse::ISO6709Coord;
4
5use crate::values::{IRational, URational};
6
7#[derive(Debug, Default, Clone, PartialEq, Eq)]
10pub struct GPSInfo {
11 pub latitude_ref: char,
13 pub latitude: LatLng,
15
16 pub longitude_ref: char,
18 pub longitude: LatLng,
20
21 pub altitude_ref: u8,
24 pub altitude: URational,
26
27 pub speed_ref: Option<char>,
32 pub speed: Option<URational>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Default)]
37pub struct LatLng(pub URational, pub URational, pub URational);
38
39impl GPSInfo {
40 pub fn format_iso6709(&self) -> String {
43 let latitude = self.latitude.0.as_float()
44 + self.latitude.1.as_float() / 60.0
45 + self.latitude.2.as_float() / 3600.0;
46 let longitude = self.longitude.0.as_float()
47 + self.longitude.1.as_float() / 60.0
48 + self.longitude.2.as_float() / 3600.0;
49 let altitude = self.altitude.as_float();
50 format!(
51 "{}{latitude:08.5}{}{longitude:09.5}{}/",
52 if self.latitude_ref == 'N' { '+' } else { '-' },
53 if self.longitude_ref == 'E' { '+' } else { '-' },
54 if self.altitude.0 == 0 {
55 "".to_string()
56 } else {
57 format!(
58 "{}{}CRSWGS_84",
59 if self.altitude_ref == 0 { "+" } else { "-" },
60 Self::format_float(altitude)
61 )
62 }
63 )
64 }
65
66 fn format_float(f: f64) -> String {
67 if f.fract() == 0.0 {
68 f.to_string()
69 } else {
70 format!("{f:.3}")
71 }
72 }
73
74 #[deprecated(since = "1.2.3", note = "please use `format_iso6709` instead")]
77 #[allow(clippy::wrong_self_convention)]
78 pub fn to_iso6709(&self) -> String {
79 self.format_iso6709()
80 }
81}
82
83impl From<[(u32, u32); 3]> for LatLng {
84 fn from(value: [(u32, u32); 3]) -> Self {
85 let res: [URational; 3] = value.map(|x| x.into());
86 res.into()
87
88 }
95}
96
97impl From<[URational; 3]> for LatLng {
98 fn from(value: [URational; 3]) -> Self {
99 Self(value[0], value[1], value[2])
100 }
101}
102
103impl FromIterator<(u32, u32)> for LatLng {
104 fn from_iter<T: IntoIterator<Item = (u32, u32)>>(iter: T) -> Self {
105 let rationals: Vec<URational> = iter.into_iter().take(3).map(|x| x.into()).collect();
106 assert!(rationals.len() >= 3);
107 rationals.try_into().unwrap()
108 }
109}
110
111impl TryFrom<Vec<URational>> for LatLng {
112 type Error = crate::Error;
113
114 fn try_from(value: Vec<URational>) -> Result<Self, Self::Error> {
115 if value.len() < 3 {
116 Err("convert to LatLng failed; need at least 3 (u32, u32)".into())
117 } else {
118 Ok(Self(value[0], value[1], value[2]))
119 }
120 }
121}
122
123impl FromIterator<URational> for LatLng {
124 fn from_iter<T: IntoIterator<Item = URational>>(iter: T) -> Self {
125 let mut values = iter.into_iter();
126 Self(
127 values.next().unwrap(),
128 values.next().unwrap(),
129 values.next().unwrap(),
130 )
131 }
132}
133
134impl TryFrom<&Vec<URational>> for LatLng {
135 type Error = crate::Error;
136 fn try_from(value: &Vec<URational>) -> Result<Self, Self::Error> {
137 if value.len() < 3 {
138 Err(crate::Error::ParseFailed("invalid URational data".into()))
139 } else {
140 Ok(Self(value[0], value[1], value[2]))
141 }
142 }
143}
144impl TryFrom<&Vec<IRational>> for LatLng {
145 type Error = crate::Error;
146 fn try_from(value: &Vec<IRational>) -> Result<Self, Self::Error> {
147 if value.len() < 3 {
148 Err(crate::Error::ParseFailed("invalid URational data".into()))
149 } else {
150 Ok(Self(value[0].into(), value[1].into(), value[2].into()))
151 }
152 }
153}
154pub struct InvalidISO6709Coord;
155
156impl FromStr for GPSInfo {
157 type Err = InvalidISO6709Coord;
158 fn from_str(s: &str) -> Result<Self, Self::Err> {
159 let info: Self = iso6709parse::parse(s).map_err(|_| InvalidISO6709Coord)?;
160 Ok(info)
161 }
162}
163
164impl From<ISO6709Coord> for GPSInfo {
165 fn from(v: ISO6709Coord) -> Self {
166 Self {
171 latitude_ref: if v.lat >= 0.0 { 'N' } else { 'S' },
172 latitude: v.lat.abs().into(),
173 longitude_ref: if v.lon >= 0.0 { 'E' } else { 'W' },
174 longitude: v.lon.abs().into(),
175 altitude_ref: v
176 .altitude
177 .map(|x| if x >= 0.0 { 0 } else { 1 })
178 .unwrap_or(0),
179 altitude: v
180 .altitude
181 .map(|x| ((x.abs() * 1000.0).trunc() as u32, 1000).into())
182 .unwrap_or_default(),
183 ..Default::default()
184 }
185 }
186}
187
188impl From<f64> for LatLng {
189 fn from(v: f64) -> Self {
190 let mins = v.fract() * 60.0;
191 [
192 (v.trunc() as u32, 1),
193 (mins.trunc() as u32, 1),
194 ((mins.fract() * 100.0).trunc() as u32, 100),
195 ]
196 .into()
197 }
198}
199
200#[cfg(test)]
216mod tests {
217 use crate::values::Rational;
218
219 use super::*;
220
221 #[test]
222 fn gps_iso6709() {
223 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
224
225 let palace = GPSInfo {
226 latitude_ref: 'N',
227 latitude: LatLng(
228 Rational::<u32>(39, 1),
229 Rational::<u32>(55, 1),
230 Rational::<u32>(0, 1),
231 ),
232 longitude_ref: 'E',
233 longitude: LatLng(
234 Rational::<u32>(116, 1),
235 Rational::<u32>(23, 1),
236 Rational::<u32>(27, 1),
237 ),
238 altitude_ref: 0,
239 altitude: Rational::<u32>(0, 1),
240 ..Default::default()
241 };
242 assert_eq!(palace.format_iso6709(), "+39.91667+116.39083/");
243
244 let liberty = GPSInfo {
245 latitude_ref: 'N',
246 latitude: LatLng(
247 Rational::<u32>(40, 1),
248 Rational::<u32>(41, 1),
249 Rational::<u32>(21, 1),
250 ),
251 longitude_ref: 'W',
252 longitude: LatLng(
253 Rational::<u32>(74, 1),
254 Rational::<u32>(2, 1),
255 Rational::<u32>(40, 1),
256 ),
257 altitude_ref: 0,
258 altitude: Rational::<u32>(0, 1),
259 ..Default::default()
260 };
261 assert_eq!(liberty.format_iso6709(), "+40.68917-074.04444/");
262
263 let above = GPSInfo {
264 latitude_ref: 'N',
265 latitude: LatLng(
266 Rational::<u32>(40, 1),
267 Rational::<u32>(41, 1),
268 Rational::<u32>(21, 1),
269 ),
270 longitude_ref: 'W',
271 longitude: LatLng(
272 Rational::<u32>(74, 1),
273 Rational::<u32>(2, 1),
274 Rational::<u32>(40, 1),
275 ),
276 altitude_ref: 0,
277 altitude: Rational::<u32>(123, 1),
278 ..Default::default()
279 };
280 assert_eq!(above.format_iso6709(), "+40.68917-074.04444+123CRSWGS_84/");
281
282 let below = GPSInfo {
283 latitude_ref: 'N',
284 latitude: LatLng(
285 Rational::<u32>(40, 1),
286 Rational::<u32>(41, 1),
287 Rational::<u32>(21, 1),
288 ),
289 longitude_ref: 'W',
290 longitude: LatLng(
291 Rational::<u32>(74, 1),
292 Rational::<u32>(2, 1),
293 Rational::<u32>(40, 1),
294 ),
295 altitude_ref: 1,
296 altitude: Rational::<u32>(123, 1),
297 ..Default::default()
298 };
299 assert_eq!(below.format_iso6709(), "+40.68917-074.04444-123CRSWGS_84/");
300
301 let below = GPSInfo {
302 latitude_ref: 'N',
303 latitude: LatLng(
304 Rational::<u32>(40, 1),
305 Rational::<u32>(41, 1),
306 Rational::<u32>(21, 1),
307 ),
308 longitude_ref: 'W',
309 longitude: LatLng(
310 Rational::<u32>(74, 1),
311 Rational::<u32>(2, 1),
312 Rational::<u32>(40, 1),
313 ),
314 altitude_ref: 1,
315 altitude: Rational::<u32>(100, 3),
316 ..Default::default()
317 };
318 assert_eq!(
319 below.format_iso6709(),
320 "+40.68917-074.04444-33.333CRSWGS_84/"
321 );
322 }
323
324 #[test]
325 fn gps_iso6709_with_invalid_alt() {
326 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
327
328 let iso: ISO6709Coord = iso6709parse::parse("+26.5322-078.1969+019.099/").unwrap();
329 assert_eq!(iso.lat, 26.5322);
330 assert_eq!(iso.lon, -78.1969);
331 assert_eq!(iso.altitude, None);
332
333 let iso: GPSInfo = iso6709parse::parse("+26.5322-078.1969+019.099/").unwrap();
334 assert_eq!(iso.latitude_ref, 'N');
335 assert_eq!(
336 iso.latitude,
337 LatLng(
338 Rational::<u32>(26, 1),
339 Rational::<u32>(31, 1),
340 Rational::<u32>(93, 100),
341 )
342 );
343
344 assert_eq!(iso.longitude_ref, 'W');
345 assert_eq!(
346 iso.longitude,
347 LatLng(
348 Rational::<u32>(78, 1),
349 Rational::<u32>(11, 1),
350 Rational::<u32>(81, 100),
351 )
352 );
353
354 assert_eq!(iso.altitude_ref, 0);
355 assert_eq!(
356 iso.altitude,
357 URational {
358 ..Default::default()
359 }
360 );
361 }
362}