Skip to main content

nom_exif/exif/
gps.rs

1use std::str::FromStr;
2
3use iso6709parse::ISO6709Coord;
4
5use crate::values::{IRational, URational};
6
7/// Represents gps information stored in [`GPSInfo`](crate::ExifTag::GPSInfo)
8/// subIFD.
9#[derive(Debug, Default, Clone, PartialEq, Eq)]
10pub struct GPSInfo {
11    /// N, S
12    pub latitude_ref: char,
13    /// degree, minute, second,
14    pub latitude: LatLng,
15
16    /// E, W
17    pub longitude_ref: char,
18    /// degree, minute, second,
19    pub longitude: LatLng,
20
21    /// 0: Above Sea Level
22    /// 1: Below Sea Level
23    pub altitude_ref: u8,
24    /// meters
25    pub altitude: URational,
26
27    /// Speed unit
28    /// - K: kilometers per hour
29    /// - M: miles per hour
30    /// - N: knots
31    pub speed_ref: Option<char>,
32    pub speed: Option<URational>,
33}
34
35/// degree, minute, second,
36#[derive(Debug, Clone, PartialEq, Eq, Default)]
37pub struct LatLng(pub URational, pub URational, pub URational);
38
39impl GPSInfo {
40    /// Returns an ISO 6709 geographic point location string such as
41    /// `+48.8577+002.295/`.
42    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    /// Returns an ISO 6709 geographic point location string such as
75    /// `+48.8577+002.295/`.
76    #[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        // value
89        //     .into_iter()
90        //     .map(|x| x.into())
91        //     .collect::<Vec<URational>>()
92        //     .try_into()
93        //     .unwrap()
94    }
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        // `iso6709parse` only decodes the altitude field when a `CRS` tag
161        // follows it, so Apple's default `±lat±lon±alt/` form (no CRS) loses
162        // the altitude. Recover it ourselves. See #66.
163        if coord.altitude.is_none() {
164            coord.altitude = parse_iso6709_altitude(s);
165        }
166        Ok(coord.into())
167    }
168}
169
170/// Extract the altitude (third signed numeric field) from an ISO 6709 string
171/// representation such as `+47.7199-117.4931+522.171/`. Each field is a `+`/`-`
172/// sign followed by a run of digits and dots, so a trailing `CRS…` suffix and
173/// the closing `/` are skipped naturally. Returns `None` when there is no third
174/// field (a plain `±lat±lon/` pair), so a two-field string never mistakes the
175/// longitude for an altitude.
176fn 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        // let latitude = self.latitude.0.as_float()
201        //     + self.latitude.1.as_float() / 60.0
202        //     + self.latitude.2.as_float() / 3600.0;
203
204        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            // The leftover fraction is a fraction *of a minute*, so it must be
229            // scaled by 60 to reach seconds before storing hundredths. See #66.
230            ((mins.fract() * 60.0 * 100.0).round() as u32, 100),
231        ]
232        .into()
233    }
234}
235
236// impl<T: AsRef<[(u32, u32)]>> From<T> for LatLng {
237//     fn from(value: T) -> Self {
238//         assert!(value.as_ref().len() >= 3);
239//         value.as_ref().iter().take(3).map(|x| x.into()).collect()
240//     }
241// }
242
243// impl<T: AsRef<[URational]>> From<T> for LatLng {
244//     fn from(value: T) -> Self {
245//         assert!(value.as_ref().len() >= 3);
246//         let s = value.as_ref();
247//         Self(s[0], s[1], s[2])
248//     }
249// }
250
251#[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        // `iso6709parse` itself drops the altitude when no `CRS` tag follows...
365        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        // ...but GPSInfo::from_str recovers it, and the minute-fraction is now
371        // scaled into the seconds slot correctly. See #66.
372        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        // Apple's default QuickTime location form: signed altitude, no CRS
402        // suffix. 2.8.0 had two defects here — the minute-fraction landed
403        // unscaled in the seconds slot, and the altitude was dropped. See #66.
404        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        // Negative altitude, still no CRS.
427        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        // No altitude field at all -> the trailing longitude must not be
432        // mistaken for an altitude.
433        let gps: GPSInfo = "+47.7199-117.4931/".parse().ok().unwrap();
434        assert_eq!(gps.altitude, URational::default());
435    }
436}