use std::str::FromStr;
use iso6709parse::ISO6709Coord;
use crate::values::{IRational, URational};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct GPSInfo {
pub latitude_ref: char,
pub latitude: LatLng,
pub longitude_ref: char,
pub longitude: LatLng,
pub altitude_ref: u8,
pub altitude: URational,
pub speed_ref: Option<char>,
pub speed: Option<URational>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LatLng(pub URational, pub URational, pub URational);
impl GPSInfo {
pub fn format_iso6709(&self) -> String {
let latitude = self.latitude.0.as_float()
+ self.latitude.1.as_float() / 60.0
+ self.latitude.2.as_float() / 3600.0;
let longitude = self.longitude.0.as_float()
+ self.longitude.1.as_float() / 60.0
+ self.longitude.2.as_float() / 3600.0;
let altitude = self.altitude.as_float();
format!(
"{}{latitude:08.5}{}{longitude:09.5}{}/",
if self.latitude_ref == 'N' { '+' } else { '-' },
if self.longitude_ref == 'E' { '+' } else { '-' },
if self.altitude.0 == 0 {
"".to_string()
} else {
format!(
"{}{}CRSWGS_84",
if self.altitude_ref == 0 { "+" } else { "-" },
Self::format_float(altitude)
)
}
)
}
fn format_float(f: f64) -> String {
if f.fract() == 0.0 {
f.to_string()
} else {
format!("{f:.3}")
}
}
#[deprecated(since = "1.2.3", note = "please use `format_iso6709` instead")]
#[allow(clippy::wrong_self_convention)]
pub fn to_iso6709(&self) -> String {
self.format_iso6709()
}
}
impl From<[(u32, u32); 3]> for LatLng {
fn from(value: [(u32, u32); 3]) -> Self {
let res: [URational; 3] = value.map(|x| x.into());
res.into()
}
}
impl From<[URational; 3]> for LatLng {
fn from(value: [URational; 3]) -> Self {
Self(value[0], value[1], value[2])
}
}
impl FromIterator<(u32, u32)> for LatLng {
fn from_iter<T: IntoIterator<Item = (u32, u32)>>(iter: T) -> Self {
let rationals: Vec<URational> = iter.into_iter().take(3).map(|x| x.into()).collect();
assert!(rationals.len() >= 3);
rationals.try_into().unwrap()
}
}
impl TryFrom<Vec<URational>> for LatLng {
type Error = crate::Error;
fn try_from(value: Vec<URational>) -> Result<Self, Self::Error> {
if value.len() < 3 {
Err("convert to LatLng failed; need at least 3 (u32, u32)".into())
} else {
Ok(Self(value[0], value[1], value[2]))
}
}
}
impl FromIterator<URational> for LatLng {
fn from_iter<T: IntoIterator<Item = URational>>(iter: T) -> Self {
let mut values = iter.into_iter();
Self(
values.next().unwrap(),
values.next().unwrap(),
values.next().unwrap(),
)
}
}
impl TryFrom<&Vec<URational>> for LatLng {
type Error = crate::Error;
fn try_from(value: &Vec<URational>) -> Result<Self, Self::Error> {
if value.len() < 3 {
Err(crate::Error::ParseFailed("invalid URational data".into()))
} else {
Ok(Self(value[0], value[1], value[2]))
}
}
}
impl TryFrom<&Vec<IRational>> for LatLng {
type Error = crate::Error;
fn try_from(value: &Vec<IRational>) -> Result<Self, Self::Error> {
if value.len() < 3 {
Err(crate::Error::ParseFailed("invalid URational data".into()))
} else {
Ok(Self(value[0].into(), value[1].into(), value[2].into()))
}
}
}
pub struct InvalidISO6709Coord;
impl FromStr for GPSInfo {
type Err = InvalidISO6709Coord;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut coord: ISO6709Coord = iso6709parse::parse(s).map_err(|_| InvalidISO6709Coord)?;
if coord.altitude.is_none() {
coord.altitude = parse_iso6709_altitude(s);
}
Ok(coord.into())
}
}
fn parse_iso6709_altitude(s: &str) -> Option<f64> {
let bytes = s.as_bytes();
let mut fields = 0;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'+' || bytes[i] == b'-' {
let start = i;
i += 1;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
fields += 1;
if fields == 3 {
return s[start..i].parse::<f64>().ok();
}
} else {
i += 1;
}
}
None
}
impl From<ISO6709Coord> for GPSInfo {
fn from(v: ISO6709Coord) -> Self {
Self {
latitude_ref: if v.lat >= 0.0 { 'N' } else { 'S' },
latitude: v.lat.abs().into(),
longitude_ref: if v.lon >= 0.0 { 'E' } else { 'W' },
longitude: v.lon.abs().into(),
altitude_ref: v
.altitude
.map(|x| if x >= 0.0 { 0 } else { 1 })
.unwrap_or(0),
altitude: v
.altitude
.map(|x| ((x.abs() * 1000.0).trunc() as u32, 1000).into())
.unwrap_or_default(),
..Default::default()
}
}
}
impl From<f64> for LatLng {
fn from(v: f64) -> Self {
let mins = v.fract() * 60.0;
[
(v.trunc() as u32, 1),
(mins.trunc() as u32, 1),
((mins.fract() * 60.0 * 100.0).round() as u32, 100),
]
.into()
}
}
#[cfg(test)]
mod tests {
use crate::values::Rational;
use super::*;
#[test]
fn gps_iso6709() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let palace = GPSInfo {
latitude_ref: 'N',
latitude: LatLng(
Rational::<u32>(39, 1),
Rational::<u32>(55, 1),
Rational::<u32>(0, 1),
),
longitude_ref: 'E',
longitude: LatLng(
Rational::<u32>(116, 1),
Rational::<u32>(23, 1),
Rational::<u32>(27, 1),
),
altitude_ref: 0,
altitude: Rational::<u32>(0, 1),
..Default::default()
};
assert_eq!(palace.format_iso6709(), "+39.91667+116.39083/");
let liberty = GPSInfo {
latitude_ref: 'N',
latitude: LatLng(
Rational::<u32>(40, 1),
Rational::<u32>(41, 1),
Rational::<u32>(21, 1),
),
longitude_ref: 'W',
longitude: LatLng(
Rational::<u32>(74, 1),
Rational::<u32>(2, 1),
Rational::<u32>(40, 1),
),
altitude_ref: 0,
altitude: Rational::<u32>(0, 1),
..Default::default()
};
assert_eq!(liberty.format_iso6709(), "+40.68917-074.04444/");
let above = GPSInfo {
latitude_ref: 'N',
latitude: LatLng(
Rational::<u32>(40, 1),
Rational::<u32>(41, 1),
Rational::<u32>(21, 1),
),
longitude_ref: 'W',
longitude: LatLng(
Rational::<u32>(74, 1),
Rational::<u32>(2, 1),
Rational::<u32>(40, 1),
),
altitude_ref: 0,
altitude: Rational::<u32>(123, 1),
..Default::default()
};
assert_eq!(above.format_iso6709(), "+40.68917-074.04444+123CRSWGS_84/");
let below = GPSInfo {
latitude_ref: 'N',
latitude: LatLng(
Rational::<u32>(40, 1),
Rational::<u32>(41, 1),
Rational::<u32>(21, 1),
),
longitude_ref: 'W',
longitude: LatLng(
Rational::<u32>(74, 1),
Rational::<u32>(2, 1),
Rational::<u32>(40, 1),
),
altitude_ref: 1,
altitude: Rational::<u32>(123, 1),
..Default::default()
};
assert_eq!(below.format_iso6709(), "+40.68917-074.04444-123CRSWGS_84/");
let below = GPSInfo {
latitude_ref: 'N',
latitude: LatLng(
Rational::<u32>(40, 1),
Rational::<u32>(41, 1),
Rational::<u32>(21, 1),
),
longitude_ref: 'W',
longitude: LatLng(
Rational::<u32>(74, 1),
Rational::<u32>(2, 1),
Rational::<u32>(40, 1),
),
altitude_ref: 1,
altitude: Rational::<u32>(100, 3),
..Default::default()
};
assert_eq!(
below.format_iso6709(),
"+40.68917-074.04444-33.333CRSWGS_84/"
);
}
#[test]
fn gps_iso6709_altitude_without_crs() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let iso: ISO6709Coord = iso6709parse::parse("+26.5322-078.1969+019.099/").unwrap();
assert_eq!(iso.lat, 26.5322);
assert_eq!(iso.lon, -78.1969);
assert_eq!(iso.altitude, None);
let iso: GPSInfo = "+26.5322-078.1969+019.099/".parse().ok().unwrap();
assert_eq!(iso.latitude_ref, 'N');
assert_eq!(
iso.latitude,
LatLng(
Rational::<u32>(26, 1),
Rational::<u32>(31, 1),
Rational::<u32>(5592, 100),
)
);
assert_eq!(iso.longitude_ref, 'W');
assert_eq!(
iso.longitude,
LatLng(
Rational::<u32>(78, 1),
Rational::<u32>(11, 1),
Rational::<u32>(4884, 100),
)
);
assert_eq!(iso.altitude_ref, 0);
assert_eq!(iso.altitude, Rational::<u32>(19099, 1000));
}
#[test]
fn gps_iso6709_apple_altitude_without_crs_issue_66() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let gps: GPSInfo = "+47.7199-117.4931+522.171/".parse().ok().unwrap();
assert_eq!(gps.latitude_ref, 'N');
assert_eq!(
gps.latitude,
LatLng(
Rational::<u32>(47, 1),
Rational::<u32>(43, 1),
Rational::<u32>(1164, 100),
)
);
assert_eq!(gps.longitude_ref, 'W');
assert_eq!(
gps.longitude,
LatLng(
Rational::<u32>(117, 1),
Rational::<u32>(29, 1),
Rational::<u32>(3516, 100),
)
);
assert_eq!(gps.altitude_ref, 0);
assert_eq!(gps.altitude, Rational::<u32>(522171, 1000));
let gps: GPSInfo = "+47.7199-117.4931-12.5/".parse().ok().unwrap();
assert_eq!(gps.altitude_ref, 1);
assert_eq!(gps.altitude, Rational::<u32>(12500, 1000));
let gps: GPSInfo = "+47.7199-117.4931/".parse().ok().unwrap();
assert_eq!(gps.altitude, URational::default());
}
}