use crate::number::NmeaNumber;
#[cfg_attr(feature = "c", repr(C))]
#[derive(Debug, Clone, Copy, Default)]
pub struct NmeaCoordinate {
pub degrees: u8,
pub minutes: NmeaNumber,
pub negative: bool,
}
impl NmeaCoordinate {
fn parse(raw: &str, dir: &str, max_degrees: u8) -> Option<Self> {
if raw.is_empty() {
return None;
}
let dot = raw.find('.')?;
let deg_end = dot - 2;
let degrees: u8 = raw[..deg_end].parse().ok()?;
if degrees > max_degrees {
return None;
}
let minutes = NmeaNumber::parse(&raw[deg_end..])?;
let negative = matches!(dir, "S" | "W");
Some(Self {
degrees,
minutes,
negative,
})
}
}
#[cfg_attr(feature = "c", repr(C))]
#[derive(Debug, Clone, Copy, Default)]
pub struct NmeaCoordinates {
pub latitude: NmeaCoordinate,
pub longitude: NmeaCoordinate,
}
impl NmeaCoordinates {
#[must_use]
pub fn parse(lat: &str, lat_dir: &str, lon: &str, lon_dir: &str) -> Option<Self> {
if lat.is_empty() || lon.is_empty() {
return None;
}
Some(Self {
latitude: NmeaCoordinate::parse(lat, lat_dir, 90)?,
longitude: NmeaCoordinate::parse(lon, lon_dir, 180)?,
})
}
}
#[cfg(feature = "float")]
impl From<NmeaCoordinate> for f32 {
fn from(value: NmeaCoordinate) -> f32 {
let decimal = f32::from(value.degrees) + Into::<f32>::into(value.minutes) / 60.0;
if value.negative { -decimal } else { decimal }
}
}
#[cfg(feature = "float")]
impl From<NmeaCoordinate> for f64 {
fn from(value: NmeaCoordinate) -> f64 {
let decimal = f64::from(value.degrees) + Into::<f64>::into(value.minutes) / 60.0;
if value.negative { -decimal } else { decimal }
}
}
#[cfg(feature = "geo")]
impl From<NmeaCoordinates> for geo_types::Point<f32> {
fn from(value: NmeaCoordinates) -> Self {
geo_types::Point::new(f32::from(value.longitude), f32::from(value.latitude))
}
}
#[cfg(feature = "geo")]
impl From<NmeaCoordinates> for geo_types::Coord<f32> {
fn from(value: NmeaCoordinates) -> Self {
geo_types::Coord {
x: f32::from(value.longitude),
y: f32::from(value.latitude),
}
}
}
#[cfg(feature = "geo")]
impl From<NmeaCoordinates> for geo_types::Point<f64> {
fn from(value: NmeaCoordinates) -> Self {
geo_types::Point::new(f64::from(value.longitude), f64::from(value.latitude))
}
}
#[cfg(feature = "geo")]
impl From<NmeaCoordinates> for geo_types::Coord<f64> {
fn from(value: NmeaCoordinates) -> Self {
geo_types::Coord {
x: f64::from(value.longitude),
y: f64::from(value.latitude),
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "geo")]
mod geo_tests {
use super::super::*;
fn coords() -> NmeaCoordinates {
NmeaCoordinates {
latitude: NmeaCoordinate {
degrees: 55,
minutes: NmeaNumber {
value: 400,
scale: 1,
}, negative: false,
},
longitude: NmeaCoordinate {
degrees: 12,
minutes: NmeaNumber {
value: 300,
scale: 1,
}, negative: false,
},
}
}
fn coords_negative() -> NmeaCoordinates {
NmeaCoordinates {
latitude: NmeaCoordinate {
degrees: 33,
minutes: NmeaNumber {
value: 450,
scale: 1,
},
negative: true,
},
longitude: NmeaCoordinate {
degrees: 70,
minutes: NmeaNumber {
value: 250,
scale: 1,
},
negative: true,
},
}
}
#[test]
fn point_from_coords() {
let p = geo_types::Point::<f64>::from(coords());
assert!(p.x() > 12.0 && p.x() < 13.0);
assert!(p.y() > 55.0 && p.y() < 56.0);
}
#[test]
fn coord_from_coords() {
let c = geo_types::Coord::<f64>::from(coords());
assert!(c.x > 12.0 && c.x < 13.0);
assert!(c.y > 55.0 && c.y < 56.0);
}
#[test]
fn point_negative_coords() {
let p = geo_types::Point::<f64>::from(coords_negative());
assert!(p.x() < 0.0);
assert!(p.y() < 0.0);
}
#[test]
fn coord_negative_coords() {
let c = geo_types::Coord::<f64>::from(coords_negative());
assert!(c.x < 0.0);
assert!(c.y < 0.0);
}
#[test]
fn point_and_coord_agree() {
let p = geo_types::Point::<f64>::from(coords());
let c = geo_types::Coord::<f64>::from(coords());
assert_eq!(p.x(), c.x);
assert_eq!(p.y(), c.y);
}
#[test]
fn longitude_is_x_latitude_is_y() {
let c = NmeaCoordinates {
latitude: NmeaCoordinate {
degrees: 10,
minutes: NmeaNumber { value: 0, scale: 1 },
negative: false,
},
longitude: NmeaCoordinate {
degrees: 20,
minutes: NmeaNumber { value: 0, scale: 1 },
negative: false,
},
};
let p = geo_types::Point::<f64>::from(c);
assert_eq!(p.x(), 20.0); assert_eq!(p.y(), 10.0); }
#[test]
fn point_from_coords_f32() {
let p = geo_types::Point::<f32>::from(coords());
assert!(p.x() > 12.0 && p.x() < 13.0);
assert!(p.y() > 55.0 && p.y() < 56.0);
}
#[test]
fn coord_from_coords_f32() {
let c = geo_types::Coord::<f32>::from(coords());
assert!(c.x > 12.0 && c.x < 13.0);
assert!(c.y > 55.0 && c.y < 56.0);
}
#[test]
fn point_negative_coords_f32() {
let p = geo_types::Point::<f32>::from(coords_negative());
assert!(p.x() < 0.0);
assert!(p.y() < 0.0);
}
#[test]
fn f32_and_f64_points_agree() {
let p32 = geo_types::Point::<f32>::from(coords());
let p64 = geo_types::Point::<f64>::from(coords());
assert!((p32.x() as f64 - p64.x()).abs() < 1e-5);
assert!((p32.y() as f64 - p64.y()).abs() < 1e-5);
}
}
}