First version of multiple coordinate formats

This commit is contained in:
Austen Adler 2023-03-20 21:09:14 -04:00
parent 97814e390a
commit 795a333624
12 changed files with 599 additions and 179 deletions

View File

@ -1,4 +1,3 @@
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::complete::{tag_no_case, take_while1}, bytes::complete::{tag_no_case, take_while1},
@ -6,7 +5,7 @@ use nom::{
complete::{space0, space1}, complete::{space0, space1},
*, *,
}, },
combinator::{map, map_opt, opt}, combinator::{eof, map, map_opt, opt, peek},
sequence::tuple, sequence::tuple,
IResult, IResult,
}; };
@ -16,8 +15,9 @@ use crate::Direction;
pub fn optional_separator<'a>(sep: char) -> impl FnMut(&'a str) -> IResult<&'a str, ()> { pub fn optional_separator<'a>(sep: char) -> impl FnMut(&'a str) -> IResult<&'a str, ()> {
map( map(
alt(( alt((
map(tuple((space0, complete::char(sep), space0)), std::mem::drop), map(tuple((space0, complete::char(sep))), std::mem::drop),
map(space1, std::mem::drop), map(peek(space1), std::mem::drop),
map(eof, std::mem::drop),
)), )),
std::mem::drop, std::mem::drop,
) )

View File

@ -1,8 +1,8 @@
use crate::common::optional_separator; use crate::common::{optional_separator, parse_f64};
use nom::{combinator::map_opt, number::complete::double, sequence::tuple, IResult}; use nom::{character::complete::space0, combinator::map_opt, sequence::tuple, IResult};
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Clone)]
pub struct Coordinate { pub struct Coordinate {
pub lat: f64, pub lat: f64,
pub lon: f64, pub lon: f64,
@ -10,13 +10,15 @@ pub struct Coordinate {
impl Coordinate { impl Coordinate {
pub fn parse(i: &str) -> IResult<&str, Self> { pub fn parse(i: &str) -> IResult<&str, Self> {
map_opt( let ret = map_opt(
tuple((double, optional_separator(','), double)), tuple((parse_f64, optional_separator(','), space0, parse_f64)),
|(lat, _, lon)| { |(lat, _, _, lon)| {
// Ensure this is a north/south then east/west direction // Ensure this is a north/south then east/west direction
Self::from(lat, lon) Self::from(lat, lon)
}, },
)(i) )(i);
eprintln!("{:?}", ret);
ret
} }
pub fn from(lat: f64, lon: f64) -> Option<Self> { pub fn from(lat: f64, lon: f64) -> Option<Self> {

View File

@ -2,26 +2,26 @@ use crate::{
common::{optional_separator, parse_direction, parse_f64}, common::{optional_separator, parse_direction, parse_f64},
Direction, LatLon, Direction, LatLon,
}; };
use dms_coordinates::{Bearing, DMS};
use nom::{ use nom::{
character::complete::{self}, branch::alt,
character::complete::{self, space0},
combinator::{map, map_opt}, combinator::{map, map_opt},
sequence::tuple, sequence::tuple,
IResult, IResult,
}; };
use std::{convert::Infallible, fmt, str::FromStr}; use std::{fmt, str::FromStr};
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Clone)]
pub struct Coordinate(pub DMM, pub DMM); pub struct Coordinate(pub DMM, pub DMM);
impl Coordinate { impl Coordinate {
pub fn parse(i: &str) -> IResult<&str, Self> { pub fn parse(i: &str) -> IResult<&str, Self> {
map_opt( map_opt(
tuple((DMM::parse, optional_separator(','), DMM::parse)), tuple((DMM::parse, optional_separator(','), space0, DMM::parse)),
|(ns, _, ew)| { |(lat, _, _, lon)| {
// Ensure this is a north/south then east/west direction // Ensure this is a north/south then east/west direction
if ns.direction.is_lat() && ew.direction.is_lon() { if lat.direction.is_lat() && lon.direction.is_lon() {
Some(Coordinate(ns, ew)) Some(Coordinate(lat, lon))
} else { } else {
None None
} }
@ -38,38 +38,100 @@ impl FromStr for Coordinate {
} }
} }
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Clone)]
pub struct DMM { pub struct DMM {
pub degrees: i32, pub degrees: i16,
pub minutes: f64, pub minutes: f64,
pub direction: Direction, 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 { impl DMM {
pub fn parse(i: &str) -> IResult<&str, DMM> { pub fn parse(i: &str) -> IResult<&str, DMM> {
map( map(
alt((
tuple(( tuple((
// Degrees // Degrees/Minutes
complete::i32, parse_dmm_numeric,
optional_separator('°'), space0,
// Minutes
parse_f64,
optional_separator('\''),
// Direction // Direction
parse_direction, parse_direction,
)), )),
|(degrees, (), minutes, (), direction)| DMM { map(
degrees, 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, minutes,
direction, direction: if direction.is_lat() {
Direction::North
} else {
Direction::East
},
}
}, },
)(i) )(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 { impl fmt::Display for DMM {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}° {}'", self.degrees, self.minutes) write!(f, "{}° {}' {}", self.degrees, self.minutes, self.direction)
} }
} }
@ -83,55 +145,60 @@ impl TryInto<LatLon> for Coordinate {
type Error = (); type Error = ();
fn try_into(self) -> Result<LatLon, Self::Error> { fn try_into(self) -> Result<LatLon, Self::Error> {
LatLon::from( LatLon::from(self.0.to_decimal_degrees(), self.1.to_decimal_degrees()).ok_or(())
DMS::new(
self.0.degrees,
self.0.minutes as i32,
self.0.minutes.fract() * 60_f64,
match self.0.direction {
Direction::North => Bearing::North,
Direction::South => Bearing::South,
Direction::East => Bearing::East,
Direction::West => Bearing::West,
},
)
.to_decimal_degrees(),
DMS::new(
self.1.degrees,
self.1.minutes as i32,
self.1.minutes.fract() * 60_f64,
match self.1.direction {
Direction::North => Bearing::North,
Direction::South => Bearing::South,
Direction::East => Bearing::East,
Direction::West => Bearing::West,
},
)
.to_decimal_degrees(),
)
.ok_or(())
} }
} }
impl TryFrom<LatLon> for Coordinate { impl TryFrom<LatLon> for Coordinate {
type Error = Infallible; type Error = ();
fn try_from(value: LatLon) -> Result<Self, Self::Error> { fn try_from(value: LatLon) -> Result<Self, Self::Error> {
let lat = DMS::from_decimal_degrees(value.lat, true);
let lon = DMS::from_decimal_degrees(value.lon, false);
Ok(Self( Ok(Self(
DMM { DMM::try_from_decimal_degrees(value.lat, true)?,
degrees: lat.degrees, DMM::try_from_decimal_degrees(value.lon, false)?,
minutes: lat.minutes as f64 + lat.seconds / 60_f64,
// TODO: Fix the bearing information for these
direction: Direction::North,
},
DMM {
degrees: lon.degrees,
minutes: lon.minutes as f64 + lon.seconds / 60_f64,
direction: Direction::North,
},
)) ))
} }
} }
#[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'"#);
}
}

View File

@ -2,30 +2,26 @@ use crate::{
common::{optional_separator, parse_direction, parse_f64}, common::{optional_separator, parse_direction, parse_f64},
Direction, LatLon, Direction, LatLon,
}; };
use dms_coordinates::Bearing;
pub use dms_coordinates::DMS;
use nom::{ use nom::{
character::complete::{self}, branch::alt,
character::complete::{self, space0},
combinator::{map, map_opt}, combinator::{map, map_opt},
sequence::tuple, sequence::tuple,
IResult, IResult,
}; };
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Clone)]
pub struct Coordinate(pub DMS, pub DMS); pub struct Coordinate(pub DMS, pub DMS);
impl Coordinate { impl Coordinate {
pub fn parse(i: &str) -> IResult<&str, Self> { pub fn parse(i: &str) -> IResult<&str, Self> {
map_opt( map_opt(
tuple((parse_dms, optional_separator(','), parse_dms)), tuple((DMS::parse, optional_separator(','), space0, DMS::parse)),
|(ns, _, ew)| { |(lat, _, _, lon)| {
// Ensure this is a north/south then east/west direction // Ensure this is a north/south then east/west direction
if ns.bearing.is_northern() if lat.direction.is_lat() && lon.direction.is_lon() {
|| ns.bearing.is_southern() && ew.bearing.is_eastern() Some(Coordinate(lat, lon))
|| ew.bearing.is_western()
{
Some(Coordinate(ns, ew))
} else { } else {
None None
} }
@ -34,35 +30,106 @@ impl Coordinate {
} }
} }
fn parse_dms(i: &str) -> IResult<&str, DMS> { /// Parse only the numeric portion of the coordinate
fn parse_dms_numeric(i: &str) -> IResult<&str, (i16, i16, f64)> {
map( map(
tuple(( tuple((
// Degrees // Degrees
complete::i32, complete::i16,
optional_separator('°'), optional_separator('°'),
space0,
// Minutes // Minutes
complete::i32, complete::i16,
optional_separator('\''), optional_separator('\''),
space0,
// Seconds // Seconds
parse_f64, parse_f64,
optional_separator('"'), optional_separator('"'),
)),
|(degrees, _, _, minutes, _, _, seconds, _)| (degrees, minutes, seconds),
)(i)
}
#[derive(PartialEq, Debug, Clone)]
pub struct DMS {
pub degrees: i16,
pub minutes: i16,
pub seconds: f64,
pub direction: Direction,
}
impl DMS {
pub fn parse(i: &str) -> IResult<&str, DMS> {
map(
alt((
// 0° 0' 0 N 100° 30' 1 E
tuple((
// Degrees/Minutes/Seconds
parse_dms_numeric,
space0,
// Direction // Direction
parse_direction, parse_direction,
)), )),
|(degrees, (), minutes, (), seconds, (), direction)| { // N 0° 0' 0 E 100° 30' 1
DMS::new( map(
tuple((
// Direction
parse_direction,
space0,
// Degrees/Minutes/Seconds
parse_dms_numeric,
)),
|(direction, space, dms)| (dms, space, direction),
),
)),
|((degrees, minutes, seconds), _, direction)| {
let negate = direction == Direction::West || direction == Direction::South;
Self {
degrees: degrees * if negate { -1 } else { 1 },
minutes,
seconds,
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;
let seconds = minutes.fract() * 60_f64;
let minutes = minutes as i16;
Ok(Self {
degrees, degrees,
minutes, minutes,
seconds, seconds,
match direction { direction: if is_latitude {
Direction::North => Bearing::North, Direction::North
Direction::South => Bearing::South, } else {
Direction::East => Bearing::East, Direction::East
Direction::West => Bearing::West,
}, },
) })
}, }
)(i)
pub fn to_decimal_degrees(&self) -> f64 {
self.degrees as f64 + self.minutes as f64 / 60_f64 + self.seconds / 3_600_f64
}
} }
impl FromStr for Coordinate { impl FromStr for Coordinate {
@ -77,14 +144,14 @@ impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
"{}° {}' {}\"", "{}° {}' {}\" {}",
self.0.degrees, self.0.minutes, self.0.seconds self.0.degrees, self.0.minutes, self.0.seconds, self.0.direction
)?; )?;
write!(f, ", ")?; write!(f, ", ")?;
write!( write!(
f, f,
"{}° {}' {}\"", "{}° {}' {}\" {}",
self.1.degrees, self.1.minutes, self.1.seconds self.1.degrees, self.1.minutes, self.1.seconds, self.1.direction
)?; )?;
Ok(()) Ok(())
} }
@ -103,8 +170,46 @@ impl TryFrom<LatLon> for Coordinate {
fn try_from(value: LatLon) -> Result<Self, Self::Error> { fn try_from(value: LatLon) -> Result<Self, Self::Error> {
Ok(Self( Ok(Self(
DMS::from_decimal_degrees(value.lat, true), DMS::try_from_decimal_degrees(value.lat, true)?,
DMS::from_decimal_degrees(value.lon, false), DMS::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());
eprintln!("Now converting to latlon");
assert!(dbg!(TryInto::<LatLon>::try_into(cvt.unwrap())).is_ok());
}};
($tt:tt, DMS) => {{
let cvt = DMS::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' 0\" N", DMS);
p!("0° 0' 0\"N", DMS);
p!("N 0° 0' 0\"", DMS);
p!("0° 0' 0\" N", DMS);
p!("0° 0' 0\"N", DMS);
p!("N 0° 0' 0", DMS);
p!(r#"E 100° 30' 1""#, DMS);
p!(r#"N 0° 0' 0" E 100° 30' 1""#);
p!(r#"N 51° 38' 55.3308 E 10° 34' 47.4024"#);
}
}

View File

@ -1,10 +1,11 @@
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
mod common; mod common;
pub mod dd; pub mod dd;
// pub mod dmm; pub mod dmm;
// pub mod dms; pub mod dms;
pub mod plus; pub mod plus;
pub mod utm; pub mod utm;
// pub mod xpin;
use nom::{ use nom::{
branch::alt, branch::alt,
@ -24,6 +25,17 @@ pub enum Direction {
West, West,
} }
impl fmt::Display for Direction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::North => write!(f, "N"),
Self::South => write!(f, "S"),
Self::East => write!(f, "E"),
Self::West => write!(f, "W"),
}
}
}
impl Direction { impl Direction {
/// True if this is north/south /// True if this is north/south
pub fn is_lat(&self) -> bool { pub fn is_lat(&self) -> bool {
@ -35,25 +47,42 @@ impl Direction {
} }
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub enum Coordinate { pub enum Coordinate {
DD(dd::Coordinate), DD(dd::Coordinate),
// DMS(dms::Coordinate), DMS(dms::Coordinate),
// DMM(dmm::Coordinate), DMM(dmm::Coordinate),
UTM(utm::Coordinate), UTM(utm::Coordinate),
// Xpin(xpin::Xpin),
Plus(plus::Coordinate), Plus(plus::Coordinate),
} }
#[derive(Debug, Copy, Clone)]
pub enum CoordinateType {
DD,
DMS,
DMM,
UTM,
// Xpin,
Plus,
}
impl Coordinate { impl Coordinate {
pub fn parse(i: &str) -> IResult<&str, Self> { pub fn parse(i: &str) -> IResult<&str, Self> {
map( map(
tuple(( tuple((
space0, space0,
// Order matters here!
// The parser will match on the first successful one
alt(( alt((
map(dd::Coordinate::parse, Coordinate::DD), map(dms::Coordinate::parse, Coordinate::DMS),
// map(dms::Coordinate::parse, Coordinate::DMS), map(dmm::Coordinate::parse, Coordinate::DMM),
// map(dmm::Coordinate::parse, Coordinate::DMM),
map(utm::Coordinate::parse, Coordinate::UTM), map(utm::Coordinate::parse, Coordinate::UTM),
// DD needs to be after DMS because the first two digits of
// 0 0 0 N 100 30 1 W
// can be parsed as a DD
map(dd::Coordinate::parse, Coordinate::DD),
// map(xpin::Coordinate::parse, Cordinate::Xpin),
map(plus::Coordinate::parse, Coordinate::Plus), map(plus::Coordinate::parse, Coordinate::Plus),
)), )),
space0, space0,
@ -62,6 +91,30 @@ impl Coordinate {
|(_, coordinate, _, _)| coordinate, |(_, coordinate, _, _)| coordinate,
)(i) )(i)
} }
// TODO: Make this `&self`
pub fn as_type(self, coordinate_type: &CoordinateType) -> Result<Self, ()> {
let lat_lon = LatLon::try_from(self)?;
Ok(match coordinate_type {
CoordinateType::DD => Self::DD(dd::Coordinate::try_from(lat_lon).or(Err(()))?),
CoordinateType::DMS => Self::DMS(dms::Coordinate::try_from(lat_lon)?),
CoordinateType::DMM => Self::DMM(dmm::Coordinate::try_from(lat_lon)?),
CoordinateType::UTM => Self::UTM(utm::Coordinate::try_from(lat_lon)?),
CoordinateType::Plus => Self::Plus(plus::Coordinate::try_from(lat_lon).or(Err(()))?),
})
}
pub fn get_type(&self) -> CoordinateType {
match self {
Self::DD(_) => CoordinateType::DD,
Self::DMS(_) => CoordinateType::DMS,
Self::DMM(_) => CoordinateType::DMM,
Self::UTM(_) => CoordinateType::UTM,
// Self::Xpin(_) => CoordinateType::Xpin,
Self::Plus(_) => CoordinateType::Plus,
}
}
} }
impl TryFrom<Coordinate> for LatLon { impl TryFrom<Coordinate> for LatLon {
@ -70,8 +123,8 @@ impl TryFrom<Coordinate> for LatLon {
fn try_from(value: Coordinate) -> Result<LatLon, Self::Error> { fn try_from(value: Coordinate) -> Result<LatLon, Self::Error> {
match value { match value {
Coordinate::DD(dd) => dd.try_into().or(Err(())), Coordinate::DD(dd) => dd.try_into().or(Err(())),
// Coordinate::DMM(dmm) => dmm.try_into(), Coordinate::DMM(dmm) => dmm.try_into(),
// Coordinate::DMS(dms) => dms.try_into(), Coordinate::DMS(dms) => dms.try_into(),
Coordinate::UTM(utm) => utm.try_into(), Coordinate::UTM(utm) => utm.try_into(),
Coordinate::Plus(plus) => plus.try_into(), Coordinate::Plus(plus) => plus.try_into(),
} }
@ -83,16 +136,16 @@ impl From<dd::Coordinate> for Coordinate {
Self::DD(c) Self::DD(c)
} }
} }
// impl From<dms::Coordinate> for Coordinate { impl From<dms::Coordinate> for Coordinate {
// fn from(c: dms::Coordinate) -> Self { fn from(c: dms::Coordinate) -> Self {
// Self::DMS(c) Self::DMS(c)
// } }
// } }
// impl From<dmm::Coordinate> for Coordinate { impl From<dmm::Coordinate> for Coordinate {
// fn from(c: dmm::Coordinate) -> Self { fn from(c: dmm::Coordinate) -> Self {
// Self::DMM(c) Self::DMM(c)
// } }
// } }
impl From<utm::Coordinate> for Coordinate { impl From<utm::Coordinate> for Coordinate {
fn from(c: utm::Coordinate) -> Self { fn from(c: utm::Coordinate) -> Self {
Self::UTM(c) Self::UTM(c)
@ -104,18 +157,18 @@ impl From<plus::Coordinate> for Coordinate {
} }
} }
// impl TryFrom<Coordinate> for dms::Coordinate { impl TryFrom<Coordinate> for dms::Coordinate {
// type Error = (); type Error = ();
// fn try_from(c: Coordinate) -> Result<Self, Self::Error> { fn try_from(c: Coordinate) -> Result<Self, Self::Error> {
// dms::Coordinate::try_from(LatLon::try_from(c)?) dms::Coordinate::try_from(LatLon::try_from(c)?)
// } }
// } }
// impl TryFrom<Coordinate> for dmm::Coordinate { impl TryFrom<Coordinate> for dmm::Coordinate {
// type Error = (); type Error = ();
// fn try_from(c: Coordinate) -> Result<Self, Self::Error> { fn try_from(c: Coordinate) -> Result<Self, Self::Error> {
// dmm::Coordinate::try_from(LatLon::try_from(c)?).or(Err(())) dmm::Coordinate::try_from(LatLon::try_from(c)?).or(Err(()))
// } }
// } }
impl TryFrom<Coordinate> for utm::Coordinate { impl TryFrom<Coordinate> for utm::Coordinate {
type Error = (); type Error = ();
fn try_from(c: Coordinate) -> Result<Self, Self::Error> { fn try_from(c: Coordinate) -> Result<Self, Self::Error> {
@ -133,8 +186,8 @@ impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Coordinate::DD(dd) => write!(f, "{}", dd), Coordinate::DD(dd) => write!(f, "{}", dd),
// Coordinate::DMM(dmm) => write!(f, "{}", dmm), Coordinate::DMM(dmm) => write!(f, "{}", dmm),
// Coordinate::DMS(dms) => write!(f, "{}", dms), Coordinate::DMS(dms) => write!(f, "{}", dms),
Coordinate::UTM(utm) => write!(f, "{}", utm), Coordinate::UTM(utm) => write!(f, "{}", utm),
Coordinate::Plus(plus) => write!(f, "{}", plus), Coordinate::Plus(plus) => write!(f, "{}", plus),
} }
@ -154,6 +207,13 @@ mod tests {
#[test] #[test]
fn test_general() { fn test_general() {
macro_rules! p {
($tt:tt) => {{
let cvt = Coordinate::from_str($tt);
eprintln!("Testing: {} => {:?}", $tt, cvt);
assert!(cvt.is_ok());
}};
}
assert_eq!( assert_eq!(
Coordinate::from_str("0.000000 100.500278"), Coordinate::from_str("0.000000 100.500278"),
Ok(Coordinate::DD(dd::Coordinate { Ok(Coordinate::DD(dd::Coordinate {
@ -172,6 +232,12 @@ mod tests {
})) }))
); );
p!("1 2");
p!(r#"0° 0' 0" N 100° 30' 1" W"#);
p!(r#"0 0 0 N 100 30 1 W"#);
// TODO: Handle no bearings
// p!(r#"0° 0' 0" 100° 30' 1""#);
// assert_eq!( // assert_eq!(
// dms::Coordinate::try_from(Coordinate::from_str("0.000000 100.500278").unwrap()), // dms::Coordinate::try_from(Coordinate::from_str("0.000000 100.500278").unwrap()),
// dms::Coordinate::try_from(Coordinate::from_str("0.000000 100.500278").unwrap()) // dms::Coordinate::try_from(Coordinate::from_str("0.000000 100.500278").unwrap())

View File

@ -9,7 +9,7 @@ const PLUSCODE_CHARS: [char; 23] = [
'R', 'V', 'W', 'X', 'R', 'V', 'W', 'X',
]; ];
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Clone)]
pub struct Coordinate(pub String); pub struct Coordinate(pub String);
impl Coordinate { impl Coordinate {

View File

@ -11,7 +11,7 @@ use nom::{
}; };
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Clone)]
pub struct Coordinate { pub struct Coordinate {
pub zone_num: u8, pub zone_num: u8,
pub zone_letter: char, pub zone_letter: char,

View File

@ -0,0 +1,36 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const selectedCoordinateTypeChange = (event) => {
dispatch('type-change', event.detail);
};
const change = (event) => {
dispatch('type-change', event.detail);
};
export let selectedCoordinateType;
export let coordinateTypes;
export let value;
</script>
<select bind:value={selectedCoordinateType} on:change={selectedCoordinateTypeChange}>
{#each coordinateTypes as t}
<option value={t}>{t}</option>
{/each}
</select>
<div class="mb-4">
<label class="text-gray-700 text-sm font-bold mb-2" for="username">{selectedCoordinateType}</label
>
<input
class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-full"
type="text"
bind:value
placeholder="0.00"
on:change
/>
</div>

View File

@ -49,7 +49,7 @@
}); });
map.on('click', onMapClick); map.on('click', onMapClick);
map.on('locationfound', (e) => { map.on('locationfound', (e) => {
dispatch('locationfound', e); dispatch('locationfound', e.detail);
}); });
// Add the location control // Add the location control

View File

@ -4,17 +4,22 @@ import * as xpinWasm from 'xpin-wasm';
export const emptyxpin = { export const emptyxpin = {
address: '', address: '',
latLon: [0.0, 0.0], latLon: [0.0, 0.0],
decimalDegrees: '' decimalDegrees: '',
srcCoordsRepr: '0.0, 0.0'
}; };
export function getxpin(xpin) { export function getxpin(xpin) {
if (!xpin) { if (!xpin) {
return; return;
} }
return { return {
address: xpin.get_address(), address: xpin.get_address(),
latLon: xpin.get_lat_lon(), latLon: xpin.get_lat_lon(),
decimalDegrees: xpin.get_decimal_degrees() decimalDegrees: xpin.get_decimal_degrees(),
srcCoordsType: xpin.get_coords_repr_type(),
srcCoordsRepr: xpin.get_coords_repr(),
xpin: xpin
}; };
} }

View File

@ -3,14 +3,18 @@
import { WasmStatus } from '$lib/common.js'; import { WasmStatus } from '$lib/common.js';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import AddressInput from '$lib/AddressInput.svelte'; import AddressInput from '$lib/AddressInput.svelte';
import CoordinateInput from '$lib/CoordinateInput.svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import Error from './Error.svelte'; import Error from './Error.svelte';
import { emptyxpin, getxpin } from '$lib/common.js'; import { emptyxpin, getxpin } from '$lib/common.js';
let coordinateTypes = [];
let selectedCoordinateType;
let leaflet; let leaflet;
import Map from '$lib/Map.svelte'; import Map from '$lib/Map.svelte';
let map; let map;
let coordinateInputValue = '0, 0';
let addr = emptyxpin; let addr = emptyxpin;
let addrInputValue = ''; let addrInputValue = '';
@ -26,6 +30,7 @@
try { try {
addr = getxpin(wasm.call.EncodedAddress.from_lat_lon(latlng.lat, latlng.lng)); addr = getxpin(wasm.call.EncodedAddress.from_lat_lon(latlng.lat, latlng.lng));
addrInputValue = addr.address; addrInputValue = addr.address;
coordinateInputValue = addr.decimalDegrees;
popup popup
.setLatLng({ .setLatLng({
lat: addr.latLon[0], lat: addr.latLon[0],
@ -36,22 +41,41 @@
} catch (err) { } catch (err) {
console.error(err); console.error(err);
addrInputValue = ''; addrInputValue = '';
coordinateInputValue = '0, 0';
popup.setLatLng(latlng).setContent(`You clicked at ${latlng}`).openOn(map); popup.setLatLng(latlng).setContent(`You clicked at ${latlng}`).openOn(map);
} }
}; };
const locationfound = (e) => { const locationfound = (e) => {
console.log('Updating current location event', e); console.log('Updating current location event', e);
updateEditedAddress(e.detail.latlng); // kupdateEditedAddress(e.detail.latlng);
updateAddr(
getxpin(wasm.call.EncodedAddress.from_coordinate(`${e.latlng.lat}, ${e.latlng.lng}`)),
false
);
}; };
const onMapClick = (e) => { const onMapClick = (e) => {
updateEditedAddress(e.latlng); updateAddr(
getxpin(wasm.call.EncodedAddress.from_coordinate(`${e.latlng.lat}, ${e.latlng.lng}`)),
false
);
}; };
let init = async () => { let init = async () => {
await getWasm() await getWasm()
.then((w) => (wasm = w)) .then((w) => {
wasm = w;
for (let k in wasm.call.CoordinateType) {
// Add the string coordinate types
if (!isNaN(k * 1)) {
continue;
}
coordinateTypes.push(k);
}
selectedCoordinateType = coordinateTypes[0];
})
.then(async () => { .then(async () => {
leaflet = await import('leaflet'); leaflet = await import('leaflet');
}) })
@ -62,10 +86,6 @@
}); });
}; };
// onMount(async () => {
// leaflet = await import('leaflet');
// });
onDestroy(async () => { onDestroy(async () => {
if (map) { if (map) {
console.log('Unloading Leaflet map.'); console.log('Unloading Leaflet map.');
@ -74,10 +94,29 @@
}); });
let outputValue = ' '; let outputValue = ' ';
const input = () => { const selectedCoordinateTypeChange = () => {
console.log('New type:', selectedCoordinateType);
coordinateInputValue = addr.xpin.get_coords_repr_as(
coordinateTypes.indexOf(selectedCoordinateType)
);
};
const coordinateInput = () => {
try { try {
let parsed = wasm.call.EncodedAddress.from_coordinate(coordinateInputValue);
updateAddr(getxpin(parsed), true);
console.log('parsed:', parsed);
} catch (e) {
console.error('Could not parse coordinate input:', e);
}
};
const updateAddr = (newAddr, fromTextInput) => {
try {
addr = newAddr;
if (browser) { if (browser) {
addr = getxpin(wasm.call.EncodedAddress.from_address(addrInputValue));
console.log('New addr', addr); console.log('New addr', addr);
let latlng = new leaflet.LatLng(addr.latLon[0], addr.latLon[1]); let latlng = new leaflet.LatLng(addr.latLon[0], addr.latLon[1]);
map.panTo(latlng, 20); map.panTo(latlng, 20);
@ -85,7 +124,30 @@
map.setView(latlng); map.setView(latlng);
outputValue = ' '; outputValue = ' ';
addrInputValue = addr.address;
// Update coordinate display
if (fromTextInput) {
selectedCoordinateType = coordinateTypes[addr.srcCoordsType] || selectedCoordinateType;
coordinateInputValue = addr.srcCoordsRepr || coordinateInputValue;
console.log('hi');
} else {
console.log('Getting it in the format of', selectedCoordinateType);
coordinateInputValue = addr.xpin.get_coords_repr_as(
coordinateTypes.indexOf(selectedCoordinateType)
);
console.log('Result:', coordinateInputValue);
} }
}
} catch (e) {
console.error(e);
outputValue = `${e}`;
}
};
const addressInput = () => {
try {
updateAddr(getxpin(wasm.call.EncodedAddress.from_address(addrInputValue)), false);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
outputValue = `${e}`; outputValue = `${e}`;
@ -110,20 +172,17 @@
<p class="text-lg js-only">Loading WebAssembly module...</p> <p class="text-lg js-only">Loading WebAssembly module...</p>
{:then} {:then}
<p>Address: <span class="font-bold">{addr.address}</span></p> <p>Address: <span class="font-bold">{addr.address}</span></p>
<p class="text-sm">({addr.latLon}) => ({addr.decimalDegrees})</p> <p class="text-sm">({addr.latLon}) => ({addr.latLon})</p>
<AddressInput bind:value={addrInputValue} on:input={input} /> <AddressInput bind:value={addrInputValue} on:input={addressInput} />
<div class="mb-4"> <CoordinateInput
<label class="text-gray-700 text-sm font-bold mb-2" for="username">Lat/Lon</label> bind:value={coordinateInputValue}
<input on:type-change={selectedCoordinateTypeChange}
class="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-full" on:change={coordinateInput}
disabled bind:coordinateTypes
type="text" bind:selectedCoordinateType
bind:value={addr.decimalDegrees}
placeholder="0.00"
/> />
</div>
<Map bind:map {onMapClick} on:locationfound={locationfound} /> <Map bind:map {onMapClick} on:locationfound={locationfound} />
{:catch message} {:catch message}

View File

@ -1,11 +1,52 @@
use std::str::FromStr; use std::str::FromStr;
use spatial_coordinate_systems::{Coordinate, LatLon};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use xpin::Address; use xpin::Address;
#[wasm_bindgen]
#[derive(Debug, Copy, Clone)]
pub enum CoordinateType {
DD,
DMS,
DMM,
UTM,
// Xpin,
Plus,
}
impl Into<spatial_coordinate_systems::CoordinateType> for CoordinateType {
fn into(self) -> spatial_coordinate_systems::CoordinateType {
match self {
Self::DD => spatial_coordinate_systems::CoordinateType::DD,
Self::DMS => spatial_coordinate_systems::CoordinateType::DMS,
Self::DMM => spatial_coordinate_systems::CoordinateType::DMM,
Self::UTM => spatial_coordinate_systems::CoordinateType::UTM,
// Self::Xpin => spatial_coordinate_systems::CoordinateType::Xpin,
Self::Plus => spatial_coordinate_systems::CoordinateType::Plus,
}
}
}
impl From<spatial_coordinate_systems::CoordinateType> for CoordinateType {
fn from(value: spatial_coordinate_systems::CoordinateType) -> CoordinateType {
match value {
spatial_coordinate_systems::CoordinateType::DD => Self::DD,
spatial_coordinate_systems::CoordinateType::DMS => Self::DMS,
spatial_coordinate_systems::CoordinateType::DMM => Self::DMM,
spatial_coordinate_systems::CoordinateType::UTM => Self::UTM,
// spatial_coordinate_systems::CoordinateType::Xpin => Self::Xpin ,
spatial_coordinate_systems::CoordinateType::Plus => Self::Plus,
}
}
}
#[wasm_bindgen] #[wasm_bindgen]
pub struct EncodedAddress { pub struct EncodedAddress {
address: String, address: String,
/// The coordinates used to encode this address
src_coords: Option<Coordinate>,
// coords_repr: Option<String>,
pub lat: f64, pub lat: f64,
pub lon: f64, pub lon: f64,
} }
@ -18,6 +59,31 @@ impl EncodedAddress {
format!("{}, {}", self.lat, self.lon) format!("{}, {}", self.lat, self.lon)
} }
pub fn get_coords_repr_type(&self) -> Option<CoordinateType> {
self.src_coords
.as_ref()
.map(|c| c.get_type())
.map(From::from)
}
/// Get the string representation of the encoded value
#[wasm_bindgen]
pub fn get_coords_repr(&self) -> Option<String> {
self.src_coords.as_ref().map(|s| s.to_string())
}
/// Get the string representation of the encoded value
#[wasm_bindgen]
pub fn get_coords_repr_as(&self, coordinate_type: CoordinateType) -> Option<String> {
self.src_coords
.clone()
.as_ref()
// TODO: Remove the clone here
.map(|c| c.clone().as_type(&coordinate_type.into()).ok())
.flatten()
.map(|s| s.to_string())
}
/// Get an encoded address from a latitude/longitude /// Get an encoded address from a latitude/longitude
#[wasm_bindgen] #[wasm_bindgen]
pub fn from_lat_lon(lat: f64, lon: f64) -> Result<EncodedAddress, String> { pub fn from_lat_lon(lat: f64, lon: f64) -> Result<EncodedAddress, String> {
@ -27,6 +93,26 @@ impl EncodedAddress {
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
/// Get an encoded address from a latitude/longitude
#[wasm_bindgen]
pub fn from_coordinate(i: &str) -> Result<EncodedAddress, String> {
let src_coords = Coordinate::from_str(i)
.map_err(|()| format!("Could not parse str as a coordinate {i:?}"))?;
// TODO: Remove the clone here
let latlon = LatLon::try_from(src_coords.clone())
.map_err(|_| format!("Could not convert coordinate back to latlon"))?;
let mut ret = xpin::Address::from_lat_lon(latlon.lat, latlon.lon)
.as_ref()
.map(EncodedAddress::from)
.map_err(|e| e.to_string())?;
ret.src_coords = Some(src_coords);
Ok(ret)
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn from_address(addr_str: &str) -> Result<EncodedAddress, String> { pub fn from_address(addr_str: &str) -> Result<EncodedAddress, String> {
xpin::Address::from_str(addr_str) xpin::Address::from_str(addr_str)
@ -46,18 +132,12 @@ impl EncodedAddress {
} }
} }
#[wasm_bindgen]
pub fn parse(i: &str) -> Result<String, String> {
spatial_coordinate_systems::Coordinate::from_str(i)
.map(|c| format!("{c:?}"))
.map_err(|()| format!("Could not parse str as a coordinate {i:?}"))
}
impl From<&'_ Address<'_>> for EncodedAddress { impl From<&'_ Address<'_>> for EncodedAddress {
fn from(addr: &Address) -> Self { fn from(addr: &Address) -> Self {
let (lat, lon) = addr.as_lat_lon(); let (lat, lon) = addr.as_lat_lon();
Self { Self {
address: addr.to_string(), address: addr.to_string(),
src_coords: None,
lat, lat,
lon, lon,
} }