Skip to main content

nom_exif/exif/
gps.rs

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