use std::str::FromStr;
use iso6709parse::ISO6709Coord;
use crate::values::{IRational, URational};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GPSInfo {
pub latitude_ref: LatRef,
pub latitude: LatLng,
pub longitude_ref: LonRef,
pub longitude: LatLng,
pub altitude: Altitude,
pub speed: Option<Speed>,
}
impl Default for GPSInfo {
fn default() -> Self {
Self {
latitude_ref: LatRef::North,
latitude: LatLng::default(),
longitude_ref: LonRef::East,
longitude: LatLng::default(),
altitude: Altitude::Unknown,
speed: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LatLng {
pub degrees: URational,
pub minutes: URational,
pub seconds: URational,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LatRef {
North,
South,
}
impl LatRef {
pub fn from_char(c: char) -> Option<Self> {
match c {
'N' | 'n' => Some(Self::North),
'S' | 's' => Some(Self::South),
_ => None,
}
}
pub fn as_char(self) -> char {
match self {
Self::North => 'N',
Self::South => 'S',
}
}
pub fn sign(self) -> f64 {
match self {
Self::North => 1.0,
Self::South => -1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LonRef {
East,
West,
}
impl LonRef {
pub fn from_char(c: char) -> Option<Self> {
match c {
'E' | 'e' => Some(Self::East),
'W' | 'w' => Some(Self::West),
_ => None,
}
}
pub fn as_char(self) -> char {
match self {
Self::East => 'E',
Self::West => 'W',
}
}
pub fn sign(self) -> f64 {
match self {
Self::East => 1.0,
Self::West => -1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Altitude {
#[default]
Unknown,
AboveSeaLevel(URational),
BelowSeaLevel(URational),
}
impl Altitude {
pub fn meters(&self) -> Option<f64> {
match self {
Altitude::Unknown => None,
Altitude::AboveSeaLevel(r) => r.to_f64(),
Altitude::BelowSeaLevel(r) => r.to_f64().map(|m| -m),
}
}
pub fn magnitude(&self) -> Option<URational> {
match self {
Altitude::Unknown => None,
Altitude::AboveSeaLevel(r) | Altitude::BelowSeaLevel(r) => Some(*r),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpeedUnit {
KmPerHour,
MilesPerHour,
Knots,
}
impl SpeedUnit {
pub fn from_char(c: char) -> Option<Self> {
match c {
'K' | 'k' => Some(Self::KmPerHour),
'M' | 'm' => Some(Self::MilesPerHour),
'N' | 'n' => Some(Self::Knots),
_ => None,
}
}
pub fn as_char(self) -> char {
match self {
Self::KmPerHour => 'K',
Self::MilesPerHour => 'M',
Self::Knots => 'N',
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Speed {
pub unit: SpeedUnit,
pub value: URational,
}
impl LatLng {
pub const fn new(degrees: URational, minutes: URational, seconds: URational) -> Self {
Self {
degrees,
minutes,
seconds,
}
}
pub fn to_decimal_degrees(&self) -> Option<f64> {
let d = self.degrees.to_f64()?;
let m = self.minutes.to_f64()?;
let s = self.seconds.to_f64()?;
Some(d + m / 60.0 + s / 3600.0)
}
pub fn try_from_decimal_degrees(degrees: f64) -> Result<Self, crate::ConvertError> {
if !degrees.is_finite() || degrees.abs() > 180.0 {
return Err(crate::ConvertError::InvalidDecimalDegrees(degrees));
}
let abs = degrees.abs();
let d = abs.trunc() as u32;
let mins_total = (abs - d as f64) * 60.0;
let m = mins_total.trunc() as u32;
let secs_hundredths = ((mins_total - m as f64) * 60.0 * 100.0).round() as u32;
Ok(Self::new(
URational::new(d, 1),
URational::new(m, 1),
URational::new(secs_hundredths, 100),
))
}
}
impl GPSInfo {
pub fn latitude_decimal(&self) -> Option<f64> {
Some(self.latitude.to_decimal_degrees()? * self.latitude_ref.sign())
}
pub fn longitude_decimal(&self) -> Option<f64> {
Some(self.longitude.to_decimal_degrees()? * self.longitude_ref.sign())
}
pub fn altitude_meters(&self) -> Option<f64> {
self.altitude.meters()
}
pub fn to_iso6709(&self) -> String {
let latitude = self.latitude.to_decimal_degrees().unwrap_or(0.0);
let longitude = self.longitude.to_decimal_degrees().unwrap_or(0.0);
let altitude_meters = self.altitude.meters();
format!(
"{}{latitude:08.5}{}{longitude:09.5}{}/",
match self.latitude_ref {
LatRef::North => '+',
LatRef::South => '-',
},
match self.longitude_ref {
LonRef::East => '+',
LonRef::West => '-',
},
match altitude_meters {
None | Some(0.0) => String::new(),
Some(m) => format!(
"{}{}CRSWGS_84",
if m >= 0.0 { "+" } else { "-" },
Self::format_float(m.abs())
),
}
)
}
fn format_float(f: f64) -> String {
if f.fract() == 0.0 {
f.to_string()
} else {
format!("{f:.3}")
}
}
}
impl TryFrom<&[URational]> for LatLng {
type Error = crate::Error;
fn try_from(value: &[URational]) -> Result<Self, Self::Error> {
if value.len() < 3 {
return Err(crate::Error::Malformed {
kind: crate::error::MalformedKind::IfdEntry,
message: "need at least 3 URational components for LatLng".into(),
});
}
Ok(Self {
degrees: value[0],
minutes: value[1],
seconds: value[2],
})
}
}
impl TryFrom<&[IRational]> for LatLng {
type Error = crate::Error;
fn try_from(value: &[IRational]) -> Result<Self, Self::Error> {
if value.len() < 3 {
return Err(crate::Error::Malformed {
kind: crate::error::MalformedKind::IfdEntry,
message: "need at least 3 IRational components for LatLng".into(),
});
}
let map_negative = |_| crate::Error::Malformed {
kind: crate::error::MalformedKind::IfdEntry,
message: "negative LatLng component".into(),
};
Ok(Self {
degrees: URational::try_from(value[0]).map_err(map_negative)?,
minutes: URational::try_from(value[1]).map_err(map_negative)?,
seconds: URational::try_from(value[2]).map_err(map_negative)?,
})
}
}
impl TryFrom<&Vec<URational>> for LatLng {
type Error = crate::Error;
fn try_from(value: &Vec<URational>) -> Result<Self, Self::Error> {
Self::try_from(value.as_slice())
}
}
impl TryFrom<&Vec<IRational>> for LatLng {
type Error = crate::Error;
fn try_from(value: &Vec<IRational>) -> Result<Self, Self::Error> {
Self::try_from(value.as_slice())
}
}
impl FromStr for GPSInfo {
type Err = crate::ConvertError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
iso6709parse::parse::<ISO6709Coord>(s)
.map(GPSInfo::from_iso6709_coord)
.map_err(|_| crate::ConvertError::InvalidIso6709(s.to_string()))
}
}
impl GPSInfo {
pub(crate) fn from_iso6709_coord(v: ISO6709Coord) -> Self {
let latitude_ref = if v.lat >= 0.0 {
LatRef::North
} else {
LatRef::South
};
let longitude_ref = if v.lon >= 0.0 {
LonRef::East
} else {
LonRef::West
};
let latitude = LatLng::try_from_decimal_degrees(v.lat.abs()).unwrap_or_default();
let longitude = LatLng::try_from_decimal_degrees(v.lon.abs()).unwrap_or_default();
let altitude = match v.altitude {
None => Altitude::Unknown,
Some(x) => {
let mag = URational::new((x.abs() * 1000.0).trunc() as u32, 1000);
if x >= 0.0 {
Altitude::AboveSeaLevel(mag)
} else {
Altitude::BelowSeaLevel(mag)
}
}
};
Self {
latitude_ref,
latitude,
longitude_ref,
longitude,
altitude,
speed: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gps_iso6709() {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let palace = GPSInfo {
latitude_ref: LatRef::North,
latitude: LatLng::new(
URational::new(39, 1),
URational::new(55, 1),
URational::new(0, 1),
),
longitude_ref: LonRef::East,
longitude: LatLng::new(
URational::new(116, 1),
URational::new(23, 1),
URational::new(27, 1),
),
altitude: Altitude::AboveSeaLevel(URational::new(0, 1)),
speed: None,
};
assert_eq!(palace.to_iso6709(), "+39.91667+116.39083/");
let liberty = GPSInfo {
latitude_ref: LatRef::North,
latitude: LatLng::new(
URational::new(40, 1),
URational::new(41, 1),
URational::new(21, 1),
),
longitude_ref: LonRef::West,
longitude: LatLng::new(
URational::new(74, 1),
URational::new(2, 1),
URational::new(40, 1),
),
altitude: Altitude::AboveSeaLevel(URational::new(0, 1)),
speed: None,
};
assert_eq!(liberty.to_iso6709(), "+40.68917-074.04444/");
let above = GPSInfo {
latitude_ref: LatRef::North,
latitude: LatLng::new(
URational::new(40, 1),
URational::new(41, 1),
URational::new(21, 1),
),
longitude_ref: LonRef::West,
longitude: LatLng::new(
URational::new(74, 1),
URational::new(2, 1),
URational::new(40, 1),
),
altitude: Altitude::AboveSeaLevel(URational::new(123, 1)),
speed: None,
};
assert_eq!(above.to_iso6709(), "+40.68917-074.04444+123CRSWGS_84/");
let below = GPSInfo {
latitude_ref: LatRef::North,
latitude: LatLng::new(
URational::new(40, 1),
URational::new(41, 1),
URational::new(21, 1),
),
longitude_ref: LonRef::West,
longitude: LatLng::new(
URational::new(74, 1),
URational::new(2, 1),
URational::new(40, 1),
),
altitude: Altitude::BelowSeaLevel(URational::new(123, 1)),
speed: None,
};
assert_eq!(below.to_iso6709(), "+40.68917-074.04444-123CRSWGS_84/");
let below = GPSInfo {
latitude_ref: LatRef::North,
latitude: LatLng::new(
URational::new(40, 1),
URational::new(41, 1),
URational::new(21, 1),
),
longitude_ref: LonRef::West,
longitude: LatLng::new(
URational::new(74, 1),
URational::new(2, 1),
URational::new(40, 1),
),
altitude: Altitude::BelowSeaLevel(URational::new(100, 3)),
speed: None,
};
assert_eq!(below.to_iso6709(), "+40.68917-074.04444-33.333CRSWGS_84/");
}
#[test]
fn gps_iso6709_with_invalid_alt() {
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().unwrap();
assert_eq!(iso.latitude_ref, LatRef::North);
assert_eq!(
iso.latitude,
LatLng::new(
URational::new(26, 1),
URational::new(31, 1),
URational::new(5592, 100),
)
);
assert_eq!(iso.longitude_ref, LonRef::West);
assert_eq!(
iso.longitude,
LatLng::new(
URational::new(78, 1),
URational::new(11, 1),
URational::new(4884, 100),
)
);
assert_eq!(iso.altitude, Altitude::Unknown);
}
#[test]
fn latlng_to_decimal_degrees() {
let p = LatLng::new(
URational::new(40, 1),
URational::new(41, 1),
URational::new(21, 1),
);
let d = p.to_decimal_degrees().unwrap();
assert!((d - 40.689_167).abs() < 1e-5);
}
#[test]
fn latlng_to_decimal_degrees_zero_denominator() {
let p = LatLng::new(
URational::new(40, 0),
URational::new(41, 1),
URational::new(21, 1),
);
assert_eq!(p.to_decimal_degrees(), None);
}
#[test]
fn latlng_try_from_decimal_degrees_ok() {
let p = LatLng::try_from_decimal_degrees(43.5).unwrap();
let back = p.to_decimal_degrees().unwrap();
assert!((back - 43.5).abs() < 1e-3);
}
#[test]
fn latlng_try_from_decimal_degrees_rejects_nan_inf_oob() {
use crate::ConvertError;
assert!(matches!(
LatLng::try_from_decimal_degrees(f64::NAN),
Err(ConvertError::InvalidDecimalDegrees(_))
));
assert!(matches!(
LatLng::try_from_decimal_degrees(f64::INFINITY),
Err(ConvertError::InvalidDecimalDegrees(_))
));
assert!(matches!(
LatLng::try_from_decimal_degrees(181.0),
Err(ConvertError::InvalidDecimalDegrees(_))
));
}
#[test]
fn lat_lon_ref_round_trip() {
for c in ['N', 'S', 'n', 's'] {
assert!(LatRef::from_char(c).is_some());
}
for c in ['E', 'W', 'e', 'w'] {
assert!(LonRef::from_char(c).is_some());
}
assert_eq!(LatRef::North.as_char(), 'N');
assert_eq!(LonRef::West.as_char(), 'W');
assert_eq!(LatRef::South.sign(), -1.0);
assert_eq!(LonRef::East.sign(), 1.0);
assert_eq!(LatRef::from_char('X'), None);
}
#[test]
fn altitude_meters_signed() {
let above = Altitude::AboveSeaLevel(URational::new(123, 1));
let below = Altitude::BelowSeaLevel(URational::new(123, 1));
assert_eq!(above.meters(), Some(123.0));
assert_eq!(below.meters(), Some(-123.0));
assert_eq!(Altitude::Unknown.meters(), None);
assert_eq!(Altitude::AboveSeaLevel(URational::new(1, 0)).meters(), None);
}
#[test]
fn speed_unit_round_trip() {
assert_eq!(SpeedUnit::from_char('K'), Some(SpeedUnit::KmPerHour));
assert_eq!(SpeedUnit::from_char('M'), Some(SpeedUnit::MilesPerHour));
assert_eq!(SpeedUnit::from_char('N'), Some(SpeedUnit::Knots));
assert_eq!(SpeedUnit::from_char('X'), None);
assert_eq!(SpeedUnit::Knots.as_char(), 'N');
}
#[test]
fn gps_info_decimal_accessors() {
let liberty = GPSInfo {
latitude_ref: LatRef::North,
latitude: LatLng::new(
URational::new(40, 1),
URational::new(41, 1),
URational::new(21, 1),
),
longitude_ref: LonRef::West,
longitude: LatLng::new(
URational::new(74, 1),
URational::new(2, 1),
URational::new(40, 1),
),
altitude: Altitude::AboveSeaLevel(URational::new(123, 1)),
speed: None,
};
let lat = liberty.latitude_decimal().unwrap();
let lon = liberty.longitude_decimal().unwrap();
assert!((lat - 40.689_167).abs() < 1e-5);
assert!((lon - (-74.044_444)).abs() < 1e-5);
assert_eq!(liberty.altitude_meters(), Some(123.0));
}
#[test]
fn gps_info_from_str_uses_convert_error() {
use crate::ConvertError;
let err = "garbage".parse::<GPSInfo>().unwrap_err();
assert!(matches!(err, ConvertError::InvalidIso6709(_)));
}
}