1use std::str::FromStr;
2
3use iso6709parse::{parse_string_representation, 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<'a> FromIterator<&'a URational> for LatLng {
135 fn from_iter<T: IntoIterator<Item = &'a URational>>(iter: T) -> Self {
136 let mut values = iter.into_iter();
137 Self(
138 *values.next().unwrap(),
139 *values.next().unwrap(),
140 *values.next().unwrap(),
141 )
142 }
143}
144
145impl<'a> FromIterator<&'a IRational> for LatLng {
146 fn from_iter<T: IntoIterator<Item = &'a IRational>>(iter: T) -> Self {
147 let mut values = iter.into_iter();
148 Self(
149 (*values.next().unwrap()).into(),
150 (*values.next().unwrap()).into(),
151 (*values.next().unwrap()).into(),
152 )
153 }
154}
155
156pub struct InvalidISO6709Coord;
157
158impl FromStr for GPSInfo {
159 type Err = InvalidISO6709Coord;
160 fn from_str(s: &str) -> Result<Self, Self::Err> {
161 let info: Self = parse_string_representation(s).map_err(|_| InvalidISO6709Coord)?;
162 Ok(info)
163 }
164}
165
166impl From<ISO6709Coord> for GPSInfo {
167 fn from(v: ISO6709Coord) -> Self {
168 Self {
173 latitude_ref: if v.lat >= 0.0 { 'N' } else { 'S' },
174 latitude: v.lat.into(),
175 longitude_ref: if v.lon >= 0.0 { 'E' } else { 'W' },
176 longitude: v.lon.into(),
177 altitude_ref: v
178 .altitude
179 .map(|x| if x >= 0.0 { 0 } else { 1 })
180 .unwrap_or(0),
181 altitude: v
182 .altitude
183 .map(|x| ((x * 1000.0).trunc() as u32, 1000).into())
184 .unwrap_or_default(),
185 ..Default::default()
186 }
187 }
188}
189
190impl From<f64> for LatLng {
191 fn from(v: f64) -> Self {
192 let mins = v.fract() * 60.0;
193 [
194 (v.trunc() as u32, 1),
195 (mins.trunc() as u32, 1),
196 ((mins.fract() * 100.0).trunc() as u32, 100),
197 ]
198 .into()
199 }
200}
201
202#[cfg(test)]
218mod tests {
219 use crate::values::Rational;
220
221 use super::*;
222
223 #[test]
224 fn gps_iso6709() {
225 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
226
227 let palace = GPSInfo {
228 latitude_ref: 'N',
229 latitude: LatLng(
230 Rational::<u32>(39, 1),
231 Rational::<u32>(55, 1),
232 Rational::<u32>(0, 1),
233 ),
234 longitude_ref: 'E',
235 longitude: LatLng(
236 Rational::<u32>(116, 1),
237 Rational::<u32>(23, 1),
238 Rational::<u32>(27, 1),
239 ),
240 altitude_ref: 0,
241 altitude: Rational::<u32>(0, 1),
242 ..Default::default()
243 };
244 assert_eq!(palace.format_iso6709(), "+39.91667+116.39083/");
245
246 let liberty = GPSInfo {
247 latitude_ref: 'N',
248 latitude: LatLng(
249 Rational::<u32>(40, 1),
250 Rational::<u32>(41, 1),
251 Rational::<u32>(21, 1),
252 ),
253 longitude_ref: 'W',
254 longitude: LatLng(
255 Rational::<u32>(74, 1),
256 Rational::<u32>(2, 1),
257 Rational::<u32>(40, 1),
258 ),
259 altitude_ref: 0,
260 altitude: Rational::<u32>(0, 1),
261 ..Default::default()
262 };
263 assert_eq!(liberty.format_iso6709(), "+40.68917-074.04444/");
264
265 let above = GPSInfo {
266 latitude_ref: 'N',
267 latitude: LatLng(
268 Rational::<u32>(40, 1),
269 Rational::<u32>(41, 1),
270 Rational::<u32>(21, 1),
271 ),
272 longitude_ref: 'W',
273 longitude: LatLng(
274 Rational::<u32>(74, 1),
275 Rational::<u32>(2, 1),
276 Rational::<u32>(40, 1),
277 ),
278 altitude_ref: 0,
279 altitude: Rational::<u32>(123, 1),
280 ..Default::default()
281 };
282 assert_eq!(above.format_iso6709(), "+40.68917-074.04444+123CRSWGS_84/");
283
284 let below = GPSInfo {
285 latitude_ref: 'N',
286 latitude: LatLng(
287 Rational::<u32>(40, 1),
288 Rational::<u32>(41, 1),
289 Rational::<u32>(21, 1),
290 ),
291 longitude_ref: 'W',
292 longitude: LatLng(
293 Rational::<u32>(74, 1),
294 Rational::<u32>(2, 1),
295 Rational::<u32>(40, 1),
296 ),
297 altitude_ref: 1,
298 altitude: Rational::<u32>(123, 1),
299 ..Default::default()
300 };
301 assert_eq!(below.format_iso6709(), "+40.68917-074.04444-123CRSWGS_84/");
302
303 let below = GPSInfo {
304 latitude_ref: 'N',
305 latitude: LatLng(
306 Rational::<u32>(40, 1),
307 Rational::<u32>(41, 1),
308 Rational::<u32>(21, 1),
309 ),
310 longitude_ref: 'W',
311 longitude: LatLng(
312 Rational::<u32>(74, 1),
313 Rational::<u32>(2, 1),
314 Rational::<u32>(40, 1),
315 ),
316 altitude_ref: 1,
317 altitude: Rational::<u32>(100, 3),
318 ..Default::default()
319 };
320 assert_eq!(
321 below.format_iso6709(),
322 "+40.68917-074.04444-33.333CRSWGS_84/"
323 );
324 }
325}