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 mut coord: ISO6709Coord = iso6709parse::parse(s).map_err(|_| InvalidISO6709Coord)?;
160 if coord.altitude.is_none() {
164 coord.altitude = parse_iso6709_altitude(s);
165 }
166 Ok(coord.into())
167 }
168}
169
170fn parse_iso6709_altitude(s: &str) -> Option<f64> {
177 let bytes = s.as_bytes();
178 let mut fields = 0;
179 let mut i = 0;
180 while i < bytes.len() {
181 if bytes[i] == b'+' || bytes[i] == b'-' {
182 let start = i;
183 i += 1;
184 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
185 i += 1;
186 }
187 fields += 1;
188 if fields == 3 {
189 return s[start..i].parse::<f64>().ok();
190 }
191 } else {
192 i += 1;
193 }
194 }
195 None
196}
197
198impl From<ISO6709Coord> for GPSInfo {
199 fn from(v: ISO6709Coord) -> Self {
200 Self {
205 latitude_ref: if v.lat >= 0.0 { 'N' } else { 'S' },
206 latitude: v.lat.abs().into(),
207 longitude_ref: if v.lon >= 0.0 { 'E' } else { 'W' },
208 longitude: v.lon.abs().into(),
209 altitude_ref: v
210 .altitude
211 .map(|x| if x >= 0.0 { 0 } else { 1 })
212 .unwrap_or(0),
213 altitude: v
214 .altitude
215 .map(|x| ((x.abs() * 1000.0).trunc() as u32, 1000).into())
216 .unwrap_or_default(),
217 ..Default::default()
218 }
219 }
220}
221
222impl From<f64> for LatLng {
223 fn from(v: f64) -> Self {
224 let mins = v.fract() * 60.0;
225 [
226 (v.trunc() as u32, 1),
227 (mins.trunc() as u32, 1),
228 ((mins.fract() * 60.0 * 100.0).round() as u32, 100),
231 ]
232 .into()
233 }
234}
235
236#[cfg(test)]
252mod tests {
253 use crate::values::Rational;
254
255 use super::*;
256
257 #[test]
258 fn gps_iso6709() {
259 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
260
261 let palace = GPSInfo {
262 latitude_ref: 'N',
263 latitude: LatLng(
264 Rational::<u32>(39, 1),
265 Rational::<u32>(55, 1),
266 Rational::<u32>(0, 1),
267 ),
268 longitude_ref: 'E',
269 longitude: LatLng(
270 Rational::<u32>(116, 1),
271 Rational::<u32>(23, 1),
272 Rational::<u32>(27, 1),
273 ),
274 altitude_ref: 0,
275 altitude: Rational::<u32>(0, 1),
276 ..Default::default()
277 };
278 assert_eq!(palace.format_iso6709(), "+39.91667+116.39083/");
279
280 let liberty = GPSInfo {
281 latitude_ref: 'N',
282 latitude: LatLng(
283 Rational::<u32>(40, 1),
284 Rational::<u32>(41, 1),
285 Rational::<u32>(21, 1),
286 ),
287 longitude_ref: 'W',
288 longitude: LatLng(
289 Rational::<u32>(74, 1),
290 Rational::<u32>(2, 1),
291 Rational::<u32>(40, 1),
292 ),
293 altitude_ref: 0,
294 altitude: Rational::<u32>(0, 1),
295 ..Default::default()
296 };
297 assert_eq!(liberty.format_iso6709(), "+40.68917-074.04444/");
298
299 let above = GPSInfo {
300 latitude_ref: 'N',
301 latitude: LatLng(
302 Rational::<u32>(40, 1),
303 Rational::<u32>(41, 1),
304 Rational::<u32>(21, 1),
305 ),
306 longitude_ref: 'W',
307 longitude: LatLng(
308 Rational::<u32>(74, 1),
309 Rational::<u32>(2, 1),
310 Rational::<u32>(40, 1),
311 ),
312 altitude_ref: 0,
313 altitude: Rational::<u32>(123, 1),
314 ..Default::default()
315 };
316 assert_eq!(above.format_iso6709(), "+40.68917-074.04444+123CRSWGS_84/");
317
318 let below = GPSInfo {
319 latitude_ref: 'N',
320 latitude: LatLng(
321 Rational::<u32>(40, 1),
322 Rational::<u32>(41, 1),
323 Rational::<u32>(21, 1),
324 ),
325 longitude_ref: 'W',
326 longitude: LatLng(
327 Rational::<u32>(74, 1),
328 Rational::<u32>(2, 1),
329 Rational::<u32>(40, 1),
330 ),
331 altitude_ref: 1,
332 altitude: Rational::<u32>(123, 1),
333 ..Default::default()
334 };
335 assert_eq!(below.format_iso6709(), "+40.68917-074.04444-123CRSWGS_84/");
336
337 let below = GPSInfo {
338 latitude_ref: 'N',
339 latitude: LatLng(
340 Rational::<u32>(40, 1),
341 Rational::<u32>(41, 1),
342 Rational::<u32>(21, 1),
343 ),
344 longitude_ref: 'W',
345 longitude: LatLng(
346 Rational::<u32>(74, 1),
347 Rational::<u32>(2, 1),
348 Rational::<u32>(40, 1),
349 ),
350 altitude_ref: 1,
351 altitude: Rational::<u32>(100, 3),
352 ..Default::default()
353 };
354 assert_eq!(
355 below.format_iso6709(),
356 "+40.68917-074.04444-33.333CRSWGS_84/"
357 );
358 }
359
360 #[test]
361 fn gps_iso6709_altitude_without_crs() {
362 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
363
364 let iso: ISO6709Coord = iso6709parse::parse("+26.5322-078.1969+019.099/").unwrap();
366 assert_eq!(iso.lat, 26.5322);
367 assert_eq!(iso.lon, -78.1969);
368 assert_eq!(iso.altitude, None);
369
370 let iso: GPSInfo = "+26.5322-078.1969+019.099/".parse().ok().unwrap();
373 assert_eq!(iso.latitude_ref, 'N');
374 assert_eq!(
375 iso.latitude,
376 LatLng(
377 Rational::<u32>(26, 1),
378 Rational::<u32>(31, 1),
379 Rational::<u32>(5592, 100),
380 )
381 );
382
383 assert_eq!(iso.longitude_ref, 'W');
384 assert_eq!(
385 iso.longitude,
386 LatLng(
387 Rational::<u32>(78, 1),
388 Rational::<u32>(11, 1),
389 Rational::<u32>(4884, 100),
390 )
391 );
392
393 assert_eq!(iso.altitude_ref, 0);
394 assert_eq!(iso.altitude, Rational::<u32>(19099, 1000));
395 }
396
397 #[test]
398 fn gps_iso6709_apple_altitude_without_crs_issue_66() {
399 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
400
401 let gps: GPSInfo = "+47.7199-117.4931+522.171/".parse().ok().unwrap();
405 assert_eq!(gps.latitude_ref, 'N');
406 assert_eq!(
407 gps.latitude,
408 LatLng(
409 Rational::<u32>(47, 1),
410 Rational::<u32>(43, 1),
411 Rational::<u32>(1164, 100),
412 )
413 );
414 assert_eq!(gps.longitude_ref, 'W');
415 assert_eq!(
416 gps.longitude,
417 LatLng(
418 Rational::<u32>(117, 1),
419 Rational::<u32>(29, 1),
420 Rational::<u32>(3516, 100),
421 )
422 );
423 assert_eq!(gps.altitude_ref, 0);
424 assert_eq!(gps.altitude, Rational::<u32>(522171, 1000));
425
426 let gps: GPSInfo = "+47.7199-117.4931-12.5/".parse().ok().unwrap();
428 assert_eq!(gps.altitude_ref, 1);
429 assert_eq!(gps.altitude, Rational::<u32>(12500, 1000));
430
431 let gps: GPSInfo = "+47.7199-117.4931/".parse().ok().unwrap();
434 assert_eq!(gps.altitude, URational::default());
435 }
436}