205 lines
5.3 KiB
Rust
205 lines
5.3 KiB
Rust
use crate::{
|
|
common::{optional_separator, parse_direction, parse_f64},
|
|
Direction, LatLon,
|
|
};
|
|
use nom::{
|
|
branch::alt,
|
|
character::complete::{self, space0},
|
|
combinator::{map, map_opt},
|
|
sequence::tuple,
|
|
IResult,
|
|
};
|
|
use std::{fmt, str::FromStr};
|
|
|
|
#[derive(PartialEq, Debug, Clone)]
|
|
pub struct Coordinate(pub DMM, pub DMM);
|
|
|
|
impl Coordinate {
|
|
pub fn parse(i: &str) -> IResult<&str, Self> {
|
|
map_opt(
|
|
tuple((DMM::parse, optional_separator(','), space0, DMM::parse)),
|
|
|(lat, _, _, lon)| {
|
|
// Ensure this is a north/south then east/west direction
|
|
if lat.direction.is_lat() && lon.direction.is_lon() {
|
|
Some(Coordinate(lat, lon))
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
)(i)
|
|
}
|
|
}
|
|
|
|
impl FromStr for Coordinate {
|
|
type Err = ();
|
|
|
|
fn from_str(i: &str) -> Result<Self, Self::Err> {
|
|
Self::parse(i).map_err(|_| ()).map(|(_, ret)| ret)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Debug, Clone)]
|
|
pub struct DMM {
|
|
pub degrees: i16,
|
|
pub minutes: f64,
|
|
pub direction: Direction,
|
|
}
|
|
|
|
/// Parse only the numeric portion of the coordinate
|
|
fn parse_dmm_numeric(i: &str) -> IResult<&str, (i16, f64)> {
|
|
map(
|
|
tuple((
|
|
// Degrees
|
|
complete::i16,
|
|
optional_separator('°'),
|
|
space0,
|
|
// Minutes
|
|
parse_f64,
|
|
optional_separator('\''),
|
|
)),
|
|
|(degrees, _, _, minutes, _)| (degrees, minutes),
|
|
)(i)
|
|
}
|
|
|
|
impl DMM {
|
|
pub fn parse(i: &str) -> IResult<&str, DMM> {
|
|
map(
|
|
alt((
|
|
tuple((
|
|
// Degrees/Minutes
|
|
parse_dmm_numeric,
|
|
space0,
|
|
// Direction
|
|
parse_direction,
|
|
)),
|
|
map(
|
|
tuple((
|
|
// Direction
|
|
parse_direction,
|
|
space0,
|
|
// Degrees/Minutes
|
|
parse_dmm_numeric,
|
|
)),
|
|
|(direction, space, dmm)| (dmm, space, direction),
|
|
),
|
|
)),
|
|
|((degrees, minutes), _, direction)| {
|
|
let negate = direction == Direction::West || direction == Direction::South;
|
|
|
|
Self {
|
|
degrees: degrees * if negate { -1 } else { 1 },
|
|
minutes,
|
|
direction: if direction.is_lat() {
|
|
Direction::North
|
|
} else {
|
|
Direction::East
|
|
},
|
|
}
|
|
},
|
|
)(i)
|
|
}
|
|
|
|
pub fn try_from_decimal_degrees(d: f64, is_latitude: bool) -> Result<Self, ()> {
|
|
let bounds = if is_latitude {
|
|
-90_f64..=90_f64
|
|
} else {
|
|
-180_f64..=180_f64
|
|
};
|
|
|
|
if !bounds.contains(&d) {
|
|
return Err(());
|
|
}
|
|
|
|
let degrees = d as i16;
|
|
let minutes = d.fract() * 60_f64;
|
|
|
|
Ok(Self {
|
|
degrees,
|
|
minutes,
|
|
direction: if is_latitude {
|
|
Direction::North
|
|
} else {
|
|
Direction::East
|
|
},
|
|
})
|
|
}
|
|
|
|
pub fn to_decimal_degrees(&self) -> f64 {
|
|
self.degrees as f64 + self.minutes / 60_f64
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for DMM {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}° {}' {}", self.degrees, self.minutes, self.direction)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Coordinate {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}, {}", self.0, self.1)
|
|
}
|
|
}
|
|
|
|
impl TryInto<LatLon> for Coordinate {
|
|
type Error = ();
|
|
|
|
fn try_into(self) -> Result<LatLon, Self::Error> {
|
|
LatLon::from(self.0.to_decimal_degrees(), self.1.to_decimal_degrees()).ok_or(())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<LatLon> for Coordinate {
|
|
type Error = ();
|
|
|
|
fn try_from(value: LatLon) -> Result<Self, Self::Error> {
|
|
Ok(Self(
|
|
DMM::try_from_decimal_degrees(value.lat, true)?,
|
|
DMM::try_from_decimal_degrees(value.lon, false)?,
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_general() {
|
|
macro_rules! p {
|
|
($tt:tt) => {{
|
|
let cvt = Coordinate::from_str($tt);
|
|
eprintln!("Testing: {} => {:?}", $tt, cvt);
|
|
assert!(cvt.is_ok());
|
|
}};
|
|
($tt:tt, DMM) => {{
|
|
let cvt = DMM::parse($tt);
|
|
eprintln!("Testing: {} => {:?}", $tt, cvt);
|
|
assert!(cvt.is_ok());
|
|
}};
|
|
}
|
|
// p!(r#"0° 0' 0" N 100° 30' 1" W"#);
|
|
// p!(r#"0 0 0 N 100 30 1 W"#);
|
|
|
|
p!("0° 0' N", DMM);
|
|
p!("0° 0'N", DMM);
|
|
p!("N 0° 0'", DMM);
|
|
|
|
p!("0° 0' N", DMM);
|
|
p!("0° 0'N", DMM);
|
|
p!("N 0° 0' 0", DMM);
|
|
|
|
p!(r#"E 100° 30'"#, DMM);
|
|
p!(r#"N 0° 0' E 100° 30'"#);
|
|
|
|
// parse_dmm_numeric(r#"38 53.2148425"#).unwrap();
|
|
|
|
// p!(r#"38 53.2148425"#, DMM);
|
|
// p!(r#"38 53.2148425'"#, DMM);
|
|
// p!(r#"38° 53.2148425"#, DMM);
|
|
// p!(r#"38° 53.2148425'"#, DMM);
|
|
// p!(r#"-77° -1.7611312866219464'"#, DMM);
|
|
// p!(r#"38° 53.2148425', -77° -1.7611312866219464'"#);
|
|
}
|
|
}
|