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 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        // let latitude = self.latitude.0.as_float()
167        //     + self.latitude.1.as_float() / 60.0
168        //     + self.latitude.2.as_float() / 3600.0;
169
170        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// impl<T: AsRef<[(u32, u32)]>> From<T> for LatLng {
201//     fn from(value: T) -> Self {
202//         assert!(value.as_ref().len() >= 3);
203//         value.as_ref().iter().take(3).map(|x| x.into()).collect()
204//     }
205// }
206
207// impl<T: AsRef<[URational]>> From<T> for LatLng {
208//     fn from(value: T) -> Self {
209//         assert!(value.as_ref().len() >= 3);
210//         let s = value.as_ref();
211//         Self(s[0], s[1], s[2])
212//     }
213// }
214
215#[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}