1use std::str::FromStr;
2
3use iso6709parse::ISO6709Coord;
4
5use crate::values::{IRational, URational};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct GPSInfo {
10 pub latitude_ref: LatRef,
11 pub latitude: LatLng,
12 pub longitude_ref: LonRef,
13 pub longitude: LatLng,
14 pub altitude: Altitude,
15 pub speed: Option<Speed>,
16}
17
18impl Default for GPSInfo {
19 fn default() -> Self {
20 Self {
21 latitude_ref: LatRef::North,
22 latitude: LatLng::default(),
23 longitude_ref: LonRef::East,
24 longitude: LatLng::default(),
25 altitude: Altitude::Unknown,
26 speed: None,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub struct LatLng {
34 pub degrees: URational,
35 pub minutes: URational,
36 pub seconds: URational,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum LatRef {
42 North,
43 South,
44}
45
46impl LatRef {
47 pub fn from_char(c: char) -> Option<Self> {
49 match c {
50 'N' | 'n' => Some(Self::North),
51 'S' | 's' => Some(Self::South),
52 _ => None,
53 }
54 }
55
56 pub fn as_char(self) -> char {
57 match self {
58 Self::North => 'N',
59 Self::South => 'S',
60 }
61 }
62
63 pub fn sign(self) -> f64 {
65 match self {
66 Self::North => 1.0,
67 Self::South => -1.0,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum LonRef {
75 East,
76 West,
77}
78
79impl LonRef {
80 pub fn from_char(c: char) -> Option<Self> {
81 match c {
82 'E' | 'e' => Some(Self::East),
83 'W' | 'w' => Some(Self::West),
84 _ => None,
85 }
86 }
87
88 pub fn as_char(self) -> char {
89 match self {
90 Self::East => 'E',
91 Self::West => 'W',
92 }
93 }
94
95 pub fn sign(self) -> f64 {
96 match self {
97 Self::East => 1.0,
98 Self::West => -1.0,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
108pub enum Altitude {
109 #[default]
111 Unknown,
112 AboveSeaLevel(URational),
113 BelowSeaLevel(URational),
114}
115
116impl Altitude {
117 pub fn meters(&self) -> Option<f64> {
119 match self {
120 Altitude::Unknown => None,
121 Altitude::AboveSeaLevel(r) => r.to_f64(),
122 Altitude::BelowSeaLevel(r) => r.to_f64().map(|m| -m),
123 }
124 }
125
126 pub fn magnitude(&self) -> Option<URational> {
128 match self {
129 Altitude::Unknown => None,
130 Altitude::AboveSeaLevel(r) | Altitude::BelowSeaLevel(r) => Some(*r),
131 }
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum SpeedUnit {
138 KmPerHour,
139 MilesPerHour,
140 Knots,
141}
142
143impl SpeedUnit {
144 pub fn from_char(c: char) -> Option<Self> {
145 match c {
146 'K' | 'k' => Some(Self::KmPerHour),
147 'M' | 'm' => Some(Self::MilesPerHour),
148 'N' | 'n' => Some(Self::Knots),
149 _ => None,
150 }
151 }
152
153 pub fn as_char(self) -> char {
154 match self {
155 Self::KmPerHour => 'K',
156 Self::MilesPerHour => 'M',
157 Self::Knots => 'N',
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub struct Speed {
165 pub unit: SpeedUnit,
166 pub value: URational,
167}
168
169impl LatLng {
170 pub const fn new(degrees: URational, minutes: URational, seconds: URational) -> Self {
171 Self {
172 degrees,
173 minutes,
174 seconds,
175 }
176 }
177
178 pub fn to_decimal_degrees(&self) -> Option<f64> {
181 let d = self.degrees.to_f64()?;
182 let m = self.minutes.to_f64()?;
183 let s = self.seconds.to_f64()?;
184 Some(d + m / 60.0 + s / 3600.0)
185 }
186
187 pub fn try_from_decimal_degrees(degrees: f64) -> Result<Self, crate::ConvertError> {
190 if !degrees.is_finite() || degrees.abs() > 180.0 {
191 return Err(crate::ConvertError::InvalidDecimalDegrees(degrees));
192 }
193 let abs = degrees.abs();
194 let d = abs.trunc() as u32;
195 let mins_total = (abs - d as f64) * 60.0;
196 let m = mins_total.trunc() as u32;
197 let secs_hundredths = ((mins_total - m as f64) * 60.0 * 100.0).round() as u32;
198 Ok(Self::new(
199 URational::new(d, 1),
200 URational::new(m, 1),
201 URational::new(secs_hundredths, 100),
202 ))
203 }
204}
205
206impl GPSInfo {
207 pub fn latitude_decimal(&self) -> Option<f64> {
209 Some(self.latitude.to_decimal_degrees()? * self.latitude_ref.sign())
210 }
211
212 pub fn longitude_decimal(&self) -> Option<f64> {
214 Some(self.longitude.to_decimal_degrees()? * self.longitude_ref.sign())
215 }
216
217 pub fn altitude_meters(&self) -> Option<f64> {
219 self.altitude.meters()
220 }
221
222 pub fn to_iso6709(&self) -> String {
225 let latitude = self.latitude.to_decimal_degrees().unwrap_or(0.0);
226 let longitude = self.longitude.to_decimal_degrees().unwrap_or(0.0);
227 let altitude_meters = self.altitude.meters();
228 format!(
229 "{}{latitude:08.5}{}{longitude:09.5}{}/",
230 match self.latitude_ref {
231 LatRef::North => '+',
232 LatRef::South => '-',
233 },
234 match self.longitude_ref {
235 LonRef::East => '+',
236 LonRef::West => '-',
237 },
238 match altitude_meters {
239 None | Some(0.0) => String::new(),
240 Some(m) => format!(
241 "{}{}CRSWGS_84",
242 if m >= 0.0 { "+" } else { "-" },
243 Self::format_float(m.abs())
244 ),
245 }
246 )
247 }
248
249 fn format_float(f: f64) -> String {
250 if f.fract() == 0.0 {
251 f.to_string()
252 } else {
253 format!("{f:.3}")
254 }
255 }
256}
257
258impl TryFrom<&[URational]> for LatLng {
259 type Error = crate::Error;
260 fn try_from(value: &[URational]) -> Result<Self, Self::Error> {
261 if value.len() < 3 {
262 return Err(crate::Error::Malformed {
263 kind: crate::error::MalformedKind::IfdEntry,
264 message: "need at least 3 URational components for LatLng".into(),
265 });
266 }
267 Ok(Self {
268 degrees: value[0],
269 minutes: value[1],
270 seconds: value[2],
271 })
272 }
273}
274
275impl TryFrom<&[IRational]> for LatLng {
276 type Error = crate::Error;
277 fn try_from(value: &[IRational]) -> Result<Self, Self::Error> {
278 if value.len() < 3 {
279 return Err(crate::Error::Malformed {
280 kind: crate::error::MalformedKind::IfdEntry,
281 message: "need at least 3 IRational components for LatLng".into(),
282 });
283 }
284 let map_negative = |_| crate::Error::Malformed {
285 kind: crate::error::MalformedKind::IfdEntry,
286 message: "negative LatLng component".into(),
287 };
288 Ok(Self {
289 degrees: URational::try_from(value[0]).map_err(map_negative)?,
290 minutes: URational::try_from(value[1]).map_err(map_negative)?,
291 seconds: URational::try_from(value[2]).map_err(map_negative)?,
292 })
293 }
294}
295
296impl TryFrom<&Vec<URational>> for LatLng {
297 type Error = crate::Error;
298 fn try_from(value: &Vec<URational>) -> Result<Self, Self::Error> {
299 Self::try_from(value.as_slice())
300 }
301}
302
303impl TryFrom<&Vec<IRational>> for LatLng {
304 type Error = crate::Error;
305 fn try_from(value: &Vec<IRational>) -> Result<Self, Self::Error> {
306 Self::try_from(value.as_slice())
307 }
308}
309
310impl FromStr for GPSInfo {
311 type Err = crate::ConvertError;
312 fn from_str(s: &str) -> Result<Self, Self::Err> {
313 iso6709parse::parse::<ISO6709Coord>(s)
314 .map(|mut coord| {
315 if coord.altitude.is_none() {
319 coord.altitude = parse_iso6709_altitude(s);
320 }
321 GPSInfo::from_iso6709_coord(coord)
322 })
323 .map_err(|_| crate::ConvertError::InvalidIso6709(s.to_string()))
324 }
325}
326
327fn parse_iso6709_altitude(s: &str) -> Option<f64> {
334 let bytes = s.as_bytes();
335 let mut fields = 0;
336 let mut i = 0;
337 while i < bytes.len() {
338 if bytes[i] == b'+' || bytes[i] == b'-' {
339 let start = i;
340 i += 1;
341 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
342 i += 1;
343 }
344 fields += 1;
345 if fields == 3 {
346 return s[start..i].parse::<f64>().ok();
347 }
348 } else {
349 i += 1;
350 }
351 }
352 None
353}
354
355impl GPSInfo {
356 pub(crate) fn from_iso6709_coord(v: ISO6709Coord) -> Self {
361 let latitude_ref = if v.lat >= 0.0 {
362 LatRef::North
363 } else {
364 LatRef::South
365 };
366 let longitude_ref = if v.lon >= 0.0 {
367 LonRef::East
368 } else {
369 LonRef::West
370 };
371 let latitude = LatLng::try_from_decimal_degrees(v.lat.abs()).unwrap_or_default();
372 let longitude = LatLng::try_from_decimal_degrees(v.lon.abs()).unwrap_or_default();
373 let altitude = match v.altitude {
374 None => Altitude::Unknown,
375 Some(x) => {
376 let mag = URational::new((x.abs() * 1000.0).trunc() as u32, 1000);
377 if x >= 0.0 {
378 Altitude::AboveSeaLevel(mag)
379 } else {
380 Altitude::BelowSeaLevel(mag)
381 }
382 }
383 };
384 Self {
385 latitude_ref,
386 latitude,
387 longitude_ref,
388 longitude,
389 altitude,
390 speed: None,
391 }
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn gps_iso6709() {
401 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
402
403 let palace = GPSInfo {
404 latitude_ref: LatRef::North,
405 latitude: LatLng::new(
406 URational::new(39, 1),
407 URational::new(55, 1),
408 URational::new(0, 1),
409 ),
410 longitude_ref: LonRef::East,
411 longitude: LatLng::new(
412 URational::new(116, 1),
413 URational::new(23, 1),
414 URational::new(27, 1),
415 ),
416 altitude: Altitude::AboveSeaLevel(URational::new(0, 1)),
417 speed: None,
418 };
419 assert_eq!(palace.to_iso6709(), "+39.91667+116.39083/");
420
421 let liberty = GPSInfo {
422 latitude_ref: LatRef::North,
423 latitude: LatLng::new(
424 URational::new(40, 1),
425 URational::new(41, 1),
426 URational::new(21, 1),
427 ),
428 longitude_ref: LonRef::West,
429 longitude: LatLng::new(
430 URational::new(74, 1),
431 URational::new(2, 1),
432 URational::new(40, 1),
433 ),
434 altitude: Altitude::AboveSeaLevel(URational::new(0, 1)),
435 speed: None,
436 };
437 assert_eq!(liberty.to_iso6709(), "+40.68917-074.04444/");
438
439 let above = GPSInfo {
440 latitude_ref: LatRef::North,
441 latitude: LatLng::new(
442 URational::new(40, 1),
443 URational::new(41, 1),
444 URational::new(21, 1),
445 ),
446 longitude_ref: LonRef::West,
447 longitude: LatLng::new(
448 URational::new(74, 1),
449 URational::new(2, 1),
450 URational::new(40, 1),
451 ),
452 altitude: Altitude::AboveSeaLevel(URational::new(123, 1)),
453 speed: None,
454 };
455 assert_eq!(above.to_iso6709(), "+40.68917-074.04444+123CRSWGS_84/");
456
457 let below = GPSInfo {
458 latitude_ref: LatRef::North,
459 latitude: LatLng::new(
460 URational::new(40, 1),
461 URational::new(41, 1),
462 URational::new(21, 1),
463 ),
464 longitude_ref: LonRef::West,
465 longitude: LatLng::new(
466 URational::new(74, 1),
467 URational::new(2, 1),
468 URational::new(40, 1),
469 ),
470 altitude: Altitude::BelowSeaLevel(URational::new(123, 1)),
471 speed: None,
472 };
473 assert_eq!(below.to_iso6709(), "+40.68917-074.04444-123CRSWGS_84/");
474
475 let below = GPSInfo {
476 latitude_ref: LatRef::North,
477 latitude: LatLng::new(
478 URational::new(40, 1),
479 URational::new(41, 1),
480 URational::new(21, 1),
481 ),
482 longitude_ref: LonRef::West,
483 longitude: LatLng::new(
484 URational::new(74, 1),
485 URational::new(2, 1),
486 URational::new(40, 1),
487 ),
488 altitude: Altitude::BelowSeaLevel(URational::new(100, 3)),
489 speed: None,
490 };
491 assert_eq!(below.to_iso6709(), "+40.68917-074.04444-33.333CRSWGS_84/");
492 }
493
494 #[test]
495 fn gps_iso6709_altitude_without_crs() {
496 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
497
498 let iso: ISO6709Coord = iso6709parse::parse("+26.5322-078.1969+019.099/").unwrap();
500 assert_eq!(iso.lat, 26.5322);
501 assert_eq!(iso.lon, -78.1969);
502 assert_eq!(iso.altitude, None);
503
504 let iso: GPSInfo = "+26.5322-078.1969+019.099/".parse().unwrap();
506 assert_eq!(iso.latitude_ref, LatRef::North);
507 assert_eq!(
508 iso.latitude,
509 LatLng::new(
510 URational::new(26, 1),
511 URational::new(31, 1),
512 URational::new(5592, 100),
513 )
514 );
515
516 assert_eq!(iso.longitude_ref, LonRef::West);
517 assert_eq!(
518 iso.longitude,
519 LatLng::new(
520 URational::new(78, 1),
521 URational::new(11, 1),
522 URational::new(4884, 100),
523 )
524 );
525
526 assert_eq!(
527 iso.altitude,
528 Altitude::AboveSeaLevel(URational::new(19099, 1000))
529 );
530 }
531
532 #[test]
533 fn gps_iso6709_apple_altitude_without_crs_issue_66() {
534 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
540
541 let gps: GPSInfo = "+47.7199-117.4931+522.171/".parse().unwrap();
542 assert_eq!(
543 gps.altitude,
544 Altitude::AboveSeaLevel(URational::new(522171, 1000))
545 );
546 assert_eq!(gps.altitude_meters(), Some(522.171));
547
548 let gps: GPSInfo = "+47.7199-117.4931-12.5/".parse().unwrap();
550 assert_eq!(gps.altitude_meters(), Some(-12.5));
551
552 let gps: GPSInfo = "+47.7199-117.4931/".parse().unwrap();
555 assert_eq!(gps.altitude, Altitude::Unknown);
556
557 let gps: GPSInfo = "+47.7199-117.4931+522.171CRSWGS_84/".parse().unwrap();
559 assert_eq!(gps.altitude_meters(), Some(522.171));
560 }
561
562 #[test]
563 fn latlng_to_decimal_degrees() {
564 let p = LatLng::new(
565 URational::new(40, 1),
566 URational::new(41, 1),
567 URational::new(21, 1),
568 );
569 let d = p.to_decimal_degrees().unwrap();
570 assert!((d - 40.689_167).abs() < 1e-5);
571 }
572
573 #[test]
574 fn latlng_to_decimal_degrees_zero_denominator() {
575 let p = LatLng::new(
576 URational::new(40, 0),
577 URational::new(41, 1),
578 URational::new(21, 1),
579 );
580 assert_eq!(p.to_decimal_degrees(), None);
581 }
582
583 #[test]
584 fn latlng_try_from_decimal_degrees_ok() {
585 let p = LatLng::try_from_decimal_degrees(43.5).unwrap();
586 let back = p.to_decimal_degrees().unwrap();
587 assert!((back - 43.5).abs() < 1e-3);
588 }
589
590 #[test]
591 fn latlng_try_from_decimal_degrees_rejects_nan_inf_oob() {
592 use crate::ConvertError;
593 assert!(matches!(
594 LatLng::try_from_decimal_degrees(f64::NAN),
595 Err(ConvertError::InvalidDecimalDegrees(_))
596 ));
597 assert!(matches!(
598 LatLng::try_from_decimal_degrees(f64::INFINITY),
599 Err(ConvertError::InvalidDecimalDegrees(_))
600 ));
601 assert!(matches!(
602 LatLng::try_from_decimal_degrees(181.0),
603 Err(ConvertError::InvalidDecimalDegrees(_))
604 ));
605 }
606
607 #[test]
608 fn lat_lon_ref_round_trip() {
609 for c in ['N', 'S', 'n', 's'] {
610 assert!(LatRef::from_char(c).is_some());
611 }
612 for c in ['E', 'W', 'e', 'w'] {
613 assert!(LonRef::from_char(c).is_some());
614 }
615 assert_eq!(LatRef::North.as_char(), 'N');
616 assert_eq!(LonRef::West.as_char(), 'W');
617 assert_eq!(LatRef::South.sign(), -1.0);
618 assert_eq!(LonRef::East.sign(), 1.0);
619 assert_eq!(LatRef::from_char('X'), None);
620 }
621
622 #[test]
623 fn altitude_meters_signed() {
624 let above = Altitude::AboveSeaLevel(URational::new(123, 1));
625 let below = Altitude::BelowSeaLevel(URational::new(123, 1));
626 assert_eq!(above.meters(), Some(123.0));
627 assert_eq!(below.meters(), Some(-123.0));
628 assert_eq!(Altitude::Unknown.meters(), None);
629 assert_eq!(Altitude::AboveSeaLevel(URational::new(1, 0)).meters(), None);
630 }
631
632 #[test]
633 fn speed_unit_round_trip() {
634 assert_eq!(SpeedUnit::from_char('K'), Some(SpeedUnit::KmPerHour));
635 assert_eq!(SpeedUnit::from_char('M'), Some(SpeedUnit::MilesPerHour));
636 assert_eq!(SpeedUnit::from_char('N'), Some(SpeedUnit::Knots));
637 assert_eq!(SpeedUnit::from_char('X'), None);
638 assert_eq!(SpeedUnit::Knots.as_char(), 'N');
639 }
640
641 #[test]
642 fn gps_info_decimal_accessors() {
643 let liberty = GPSInfo {
644 latitude_ref: LatRef::North,
645 latitude: LatLng::new(
646 URational::new(40, 1),
647 URational::new(41, 1),
648 URational::new(21, 1),
649 ),
650 longitude_ref: LonRef::West,
651 longitude: LatLng::new(
652 URational::new(74, 1),
653 URational::new(2, 1),
654 URational::new(40, 1),
655 ),
656 altitude: Altitude::AboveSeaLevel(URational::new(123, 1)),
657 speed: None,
658 };
659 let lat = liberty.latitude_decimal().unwrap();
660 let lon = liberty.longitude_decimal().unwrap();
661 assert!((lat - 40.689_167).abs() < 1e-5);
662 assert!((lon - (-74.044_444)).abs() < 1e-5);
663 assert_eq!(liberty.altitude_meters(), Some(123.0));
664 }
665
666 #[test]
667 fn gps_info_from_str_uses_convert_error() {
668 use crate::ConvertError;
669 let err = "garbage".parse::<GPSInfo>().unwrap_err();
670 assert!(matches!(err, ConvertError::InvalidIso6709(_)));
671 }
672}