From 795a33362444a2a11f71ef025b669719e32e7416 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Mon, 20 Mar 2023 21:09:14 -0400 Subject: [PATCH] First version of multiple coordinate formats --- spatial-coordinate-systems/src/common.rs | 8 +- spatial-coordinate-systems/src/dd.rs | 16 +- spatial-coordinate-systems/src/dmm.rs | 199 +++++++++++++------- spatial-coordinate-systems/src/dms.rs | 175 +++++++++++++---- spatial-coordinate-systems/src/lib.rs | 134 +++++++++---- spatial-coordinate-systems/src/plus.rs | 2 +- spatial-coordinate-systems/src/utm.rs | 2 +- web-frontend/src/lib/CoordinateInput.svelte | 36 ++++ web-frontend/src/lib/Map.svelte | 2 +- web-frontend/src/lib/common.js | 9 +- web-frontend/src/routes/app/+page.svelte | 101 +++++++--- xpin-wasm/src/lib.rs | 94 ++++++++- 12 files changed, 599 insertions(+), 179 deletions(-) create mode 100644 web-frontend/src/lib/CoordinateInput.svelte diff --git a/spatial-coordinate-systems/src/common.rs b/spatial-coordinate-systems/src/common.rs index f113fc1..20b4946 100644 --- a/spatial-coordinate-systems/src/common.rs +++ b/spatial-coordinate-systems/src/common.rs @@ -1,4 +1,3 @@ - use nom::{ branch::alt, bytes::complete::{tag_no_case, take_while1}, @@ -6,7 +5,7 @@ use nom::{ complete::{space0, space1}, *, }, - combinator::{map, map_opt, opt}, + combinator::{eof, map, map_opt, opt, peek}, sequence::tuple, IResult, }; @@ -16,8 +15,9 @@ use crate::Direction; pub fn optional_separator<'a>(sep: char) -> impl FnMut(&'a str) -> IResult<&'a str, ()> { map( alt(( - map(tuple((space0, complete::char(sep), space0)), std::mem::drop), - map(space1, std::mem::drop), + map(tuple((space0, complete::char(sep))), std::mem::drop), + map(peek(space1), std::mem::drop), + map(eof, std::mem::drop), )), std::mem::drop, ) diff --git a/spatial-coordinate-systems/src/dd.rs b/spatial-coordinate-systems/src/dd.rs index 9b291c7..fccfaa7 100644 --- a/spatial-coordinate-systems/src/dd.rs +++ b/spatial-coordinate-systems/src/dd.rs @@ -1,8 +1,8 @@ -use crate::common::optional_separator; -use nom::{combinator::map_opt, number::complete::double, sequence::tuple, IResult}; +use crate::common::{optional_separator, parse_f64}; +use nom::{character::complete::space0, combinator::map_opt, sequence::tuple, IResult}; use std::{fmt, str::FromStr}; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub struct Coordinate { pub lat: f64, pub lon: f64, @@ -10,13 +10,15 @@ pub struct Coordinate { impl Coordinate { pub fn parse(i: &str) -> IResult<&str, Self> { - map_opt( - tuple((double, optional_separator(','), double)), - |(lat, _, lon)| { + let ret = map_opt( + tuple((parse_f64, optional_separator(','), space0, parse_f64)), + |(lat, _, _, lon)| { // Ensure this is a north/south then east/west direction Self::from(lat, lon) }, - )(i) + )(i); + eprintln!("{:?}", ret); + ret } pub fn from(lat: f64, lon: f64) -> Option { diff --git a/spatial-coordinate-systems/src/dmm.rs b/spatial-coordinate-systems/src/dmm.rs index 9a745c2..e58bc96 100644 --- a/spatial-coordinate-systems/src/dmm.rs +++ b/spatial-coordinate-systems/src/dmm.rs @@ -2,26 +2,26 @@ use crate::{ common::{optional_separator, parse_direction, parse_f64}, Direction, LatLon, }; -use dms_coordinates::{Bearing, DMS}; use nom::{ - character::complete::{self}, + branch::alt, + character::complete::{self, space0}, combinator::{map, map_opt}, sequence::tuple, 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); impl Coordinate { pub fn parse(i: &str) -> IResult<&str, Self> { map_opt( - tuple((DMM::parse, optional_separator(','), DMM::parse)), - |(ns, _, ew)| { + tuple((DMM::parse, optional_separator(','), space0, DMM::parse)), + |(lat, _, _, lon)| { // Ensure this is a north/south then east/west direction - if ns.direction.is_lat() && ew.direction.is_lon() { - Some(Coordinate(ns, ew)) + if lat.direction.is_lat() && lon.direction.is_lon() { + Some(Coordinate(lat, lon)) } else { None } @@ -38,38 +38,100 @@ impl FromStr for Coordinate { } } -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub struct DMM { - pub degrees: i32, + 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( - tuple(( - // Degrees - complete::i32, - optional_separator('°'), - // Minutes - parse_f64, - optional_separator('\''), - // Direction - parse_direction, + 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)| DMM { - degrees, - minutes, - 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 { + 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) + write!(f, "{}° {}' {}", self.degrees, self.minutes, self.direction) } } @@ -83,55 +145,60 @@ impl TryInto for Coordinate { type Error = (); fn try_into(self) -> Result { - LatLon::from( - 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(()) + LatLon::from(self.0.to_decimal_degrees(), self.1.to_decimal_degrees()).ok_or(()) } } impl TryFrom for Coordinate { - type Error = Infallible; + type Error = (); fn try_from(value: LatLon) -> Result { - let lat = DMS::from_decimal_degrees(value.lat, true); - let lon = DMS::from_decimal_degrees(value.lon, false); - Ok(Self( - DMM { - degrees: lat.degrees, - 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, - }, + 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'"#); + } +} diff --git a/spatial-coordinate-systems/src/dms.rs b/spatial-coordinate-systems/src/dms.rs index 7722c8b..ee82811 100644 --- a/spatial-coordinate-systems/src/dms.rs +++ b/spatial-coordinate-systems/src/dms.rs @@ -2,30 +2,26 @@ use crate::{ common::{optional_separator, parse_direction, parse_f64}, Direction, LatLon, }; -use dms_coordinates::Bearing; -pub use dms_coordinates::DMS; use nom::{ - character::complete::{self}, + branch::alt, + character::complete::{self, space0}, combinator::{map, map_opt}, sequence::tuple, IResult, }; use std::{fmt, str::FromStr}; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub struct Coordinate(pub DMS, pub DMS); impl Coordinate { pub fn parse(i: &str) -> IResult<&str, Self> { map_opt( - tuple((parse_dms, optional_separator(','), parse_dms)), - |(ns, _, ew)| { + tuple((DMS::parse, optional_separator(','), space0, DMS::parse)), + |(lat, _, _, lon)| { // Ensure this is a north/south then east/west direction - if ns.bearing.is_northern() - || ns.bearing.is_southern() && ew.bearing.is_eastern() - || ew.bearing.is_western() - { - Some(Coordinate(ns, ew)) + if lat.direction.is_lat() && lon.direction.is_lon() { + Some(Coordinate(lat, lon)) } else { None } @@ -34,37 +30,108 @@ 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( tuple(( // Degrees - complete::i32, + complete::i16, optional_separator('°'), + space0, // Minutes - complete::i32, + complete::i16, optional_separator('\''), + space0, // Seconds parse_f64, optional_separator('"'), - // Direction - parse_direction, )), - |(degrees, (), minutes, (), seconds, (), direction)| { - DMS::new( - degrees, - minutes, - seconds, - match direction { - Direction::North => Bearing::North, - Direction::South => Bearing::South, - Direction::East => Bearing::East, - Direction::West => Bearing::West, - }, - ) - }, + |(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 + parse_direction, + )), + // N 0° 0' 0 E 100° 30' 1 + 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 { + 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, + minutes, + seconds, + direction: if is_latitude { + Direction::North + } else { + Direction::East + }, + }) + } + + 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 { type Err = (); @@ -77,14 +144,14 @@ impl fmt::Display for Coordinate { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( 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, - "{}° {}' {}\"", - self.1.degrees, self.1.minutes, self.1.seconds + "{}° {}' {}\" {}", + self.1.degrees, self.1.minutes, self.1.seconds, self.1.direction )?; Ok(()) } @@ -103,8 +170,46 @@ impl TryFrom for Coordinate { fn try_from(value: LatLon) -> Result { Ok(Self( - DMS::from_decimal_degrees(value.lat, true), - DMS::from_decimal_degrees(value.lon, false), + DMS::try_from_decimal_degrees(value.lat, true)?, + 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::::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"#); + } +} diff --git a/spatial-coordinate-systems/src/lib.rs b/spatial-coordinate-systems/src/lib.rs index 25486f1..3343c70 100644 --- a/spatial-coordinate-systems/src/lib.rs +++ b/spatial-coordinate-systems/src/lib.rs @@ -1,10 +1,11 @@ use std::{fmt, str::FromStr}; mod common; pub mod dd; -// pub mod dmm; -// pub mod dms; +pub mod dmm; +pub mod dms; pub mod plus; pub mod utm; +// pub mod xpin; use nom::{ branch::alt, @@ -24,6 +25,17 @@ pub enum Direction { 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 { /// True if this is north/south pub fn is_lat(&self) -> bool { @@ -35,25 +47,42 @@ impl Direction { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Coordinate { DD(dd::Coordinate), - // DMS(dms::Coordinate), - // DMM(dmm::Coordinate), + DMS(dms::Coordinate), + DMM(dmm::Coordinate), UTM(utm::Coordinate), + // Xpin(xpin::Xpin), Plus(plus::Coordinate), } +#[derive(Debug, Copy, Clone)] +pub enum CoordinateType { + DD, + DMS, + DMM, + UTM, + // Xpin, + Plus, +} + impl Coordinate { pub fn parse(i: &str) -> IResult<&str, Self> { map( tuple(( space0, + // Order matters here! + // The parser will match on the first successful one alt(( - map(dd::Coordinate::parse, Coordinate::DD), - // map(dms::Coordinate::parse, Coordinate::DMS), - // map(dmm::Coordinate::parse, Coordinate::DMM), + map(dms::Coordinate::parse, Coordinate::DMS), + map(dmm::Coordinate::parse, Coordinate::DMM), 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), )), space0, @@ -62,6 +91,30 @@ impl Coordinate { |(_, coordinate, _, _)| coordinate, )(i) } + + // TODO: Make this `&self` + pub fn as_type(self, coordinate_type: &CoordinateType) -> Result { + 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 for LatLon { @@ -70,8 +123,8 @@ impl TryFrom for LatLon { fn try_from(value: Coordinate) -> Result { match value { Coordinate::DD(dd) => dd.try_into().or(Err(())), - // Coordinate::DMM(dmm) => dmm.try_into(), - // Coordinate::DMS(dms) => dms.try_into(), + Coordinate::DMM(dmm) => dmm.try_into(), + Coordinate::DMS(dms) => dms.try_into(), Coordinate::UTM(utm) => utm.try_into(), Coordinate::Plus(plus) => plus.try_into(), } @@ -83,16 +136,16 @@ impl From for Coordinate { Self::DD(c) } } -// impl From for Coordinate { -// fn from(c: dms::Coordinate) -> Self { -// Self::DMS(c) -// } -// } -// impl From for Coordinate { -// fn from(c: dmm::Coordinate) -> Self { -// Self::DMM(c) -// } -// } +impl From for Coordinate { + fn from(c: dms::Coordinate) -> Self { + Self::DMS(c) + } +} +impl From for Coordinate { + fn from(c: dmm::Coordinate) -> Self { + Self::DMM(c) + } +} impl From for Coordinate { fn from(c: utm::Coordinate) -> Self { Self::UTM(c) @@ -104,18 +157,18 @@ impl From for Coordinate { } } -// impl TryFrom for dms::Coordinate { -// type Error = (); -// fn try_from(c: Coordinate) -> Result { -// dms::Coordinate::try_from(LatLon::try_from(c)?) -// } -// } -// impl TryFrom for dmm::Coordinate { -// type Error = (); -// fn try_from(c: Coordinate) -> Result { -// dmm::Coordinate::try_from(LatLon::try_from(c)?).or(Err(())) -// } -// } +impl TryFrom for dms::Coordinate { + type Error = (); + fn try_from(c: Coordinate) -> Result { + dms::Coordinate::try_from(LatLon::try_from(c)?) + } +} +impl TryFrom for dmm::Coordinate { + type Error = (); + fn try_from(c: Coordinate) -> Result { + dmm::Coordinate::try_from(LatLon::try_from(c)?).or(Err(())) + } +} impl TryFrom for utm::Coordinate { type Error = (); fn try_from(c: Coordinate) -> Result { @@ -133,8 +186,8 @@ impl fmt::Display for Coordinate { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Coordinate::DD(dd) => write!(f, "{}", dd), - // Coordinate::DMM(dmm) => write!(f, "{}", dmm), - // Coordinate::DMS(dms) => write!(f, "{}", dms), + Coordinate::DMM(dmm) => write!(f, "{}", dmm), + Coordinate::DMS(dms) => write!(f, "{}", dms), Coordinate::UTM(utm) => write!(f, "{}", utm), Coordinate::Plus(plus) => write!(f, "{}", plus), } @@ -154,6 +207,13 @@ mod tests { #[test] fn test_general() { + macro_rules! p { + ($tt:tt) => {{ + let cvt = Coordinate::from_str($tt); + eprintln!("Testing: {} => {:?}", $tt, cvt); + assert!(cvt.is_ok()); + }}; + } assert_eq!( Coordinate::from_str("0.000000 100.500278"), 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!( // dms::Coordinate::try_from(Coordinate::from_str("0.000000 100.500278").unwrap()), // dms::Coordinate::try_from(Coordinate::from_str("0.000000 100.500278").unwrap()) diff --git a/spatial-coordinate-systems/src/plus.rs b/spatial-coordinate-systems/src/plus.rs index 220c349..b0e0002 100644 --- a/spatial-coordinate-systems/src/plus.rs +++ b/spatial-coordinate-systems/src/plus.rs @@ -9,7 +9,7 @@ const PLUSCODE_CHARS: [char; 23] = [ 'R', 'V', 'W', 'X', ]; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub struct Coordinate(pub String); impl Coordinate { diff --git a/spatial-coordinate-systems/src/utm.rs b/spatial-coordinate-systems/src/utm.rs index cbc6a2d..7ed21f7 100644 --- a/spatial-coordinate-systems/src/utm.rs +++ b/spatial-coordinate-systems/src/utm.rs @@ -11,7 +11,7 @@ use nom::{ }; use std::{fmt, str::FromStr}; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub struct Coordinate { pub zone_num: u8, pub zone_letter: char, diff --git a/web-frontend/src/lib/CoordinateInput.svelte b/web-frontend/src/lib/CoordinateInput.svelte new file mode 100644 index 0000000..2937b1b --- /dev/null +++ b/web-frontend/src/lib/CoordinateInput.svelte @@ -0,0 +1,36 @@ + + + + +
+ + +
diff --git a/web-frontend/src/lib/Map.svelte b/web-frontend/src/lib/Map.svelte index 1d4b948..f58f511 100644 --- a/web-frontend/src/lib/Map.svelte +++ b/web-frontend/src/lib/Map.svelte @@ -49,7 +49,7 @@ }); map.on('click', onMapClick); map.on('locationfound', (e) => { - dispatch('locationfound', e); + dispatch('locationfound', e.detail); }); // Add the location control diff --git a/web-frontend/src/lib/common.js b/web-frontend/src/lib/common.js index 6c4e619..646135a 100644 --- a/web-frontend/src/lib/common.js +++ b/web-frontend/src/lib/common.js @@ -4,17 +4,22 @@ import * as xpinWasm from 'xpin-wasm'; export const emptyxpin = { address: '', latLon: [0.0, 0.0], - decimalDegrees: '' + decimalDegrees: '', + srcCoordsRepr: '0.0, 0.0' }; export function getxpin(xpin) { if (!xpin) { return; } + return { address: xpin.get_address(), 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 }; } diff --git a/web-frontend/src/routes/app/+page.svelte b/web-frontend/src/routes/app/+page.svelte index 696ddaa..ddd75b8 100644 --- a/web-frontend/src/routes/app/+page.svelte +++ b/web-frontend/src/routes/app/+page.svelte @@ -3,14 +3,18 @@ import { WasmStatus } from '$lib/common.js'; import { onMount, onDestroy } from 'svelte'; import AddressInput from '$lib/AddressInput.svelte'; + import CoordinateInput from '$lib/CoordinateInput.svelte'; import { browser } from '$app/environment'; import Error from './Error.svelte'; import { emptyxpin, getxpin } from '$lib/common.js'; + let coordinateTypes = []; + let selectedCoordinateType; let leaflet; import Map from '$lib/Map.svelte'; let map; + let coordinateInputValue = '0, 0'; let addr = emptyxpin; let addrInputValue = ''; @@ -26,6 +30,7 @@ try { addr = getxpin(wasm.call.EncodedAddress.from_lat_lon(latlng.lat, latlng.lng)); addrInputValue = addr.address; + coordinateInputValue = addr.decimalDegrees; popup .setLatLng({ lat: addr.latLon[0], @@ -36,22 +41,41 @@ } catch (err) { console.error(err); addrInputValue = ''; + coordinateInputValue = '0, 0'; popup.setLatLng(latlng).setContent(`You clicked at ${latlng}`).openOn(map); } }; const locationfound = (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) => { - updateEditedAddress(e.latlng); + updateAddr( + getxpin(wasm.call.EncodedAddress.from_coordinate(`${e.latlng.lat}, ${e.latlng.lng}`)), + false + ); }; let init = async () => { 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 () => { leaflet = await import('leaflet'); }) @@ -62,10 +86,6 @@ }); }; - // onMount(async () => { - // leaflet = await import('leaflet'); - // }); - onDestroy(async () => { if (map) { console.log('Unloading Leaflet map.'); @@ -74,10 +94,29 @@ }); let outputValue = ' '; - const input = () => { + const selectedCoordinateTypeChange = () => { + console.log('New type:', selectedCoordinateType); + coordinateInputValue = addr.xpin.get_coords_repr_as( + coordinateTypes.indexOf(selectedCoordinateType) + ); + }; + + const coordinateInput = () => { 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) { - addr = getxpin(wasm.call.EncodedAddress.from_address(addrInputValue)); console.log('New addr', addr); let latlng = new leaflet.LatLng(addr.latLon[0], addr.latLon[1]); map.panTo(latlng, 20); @@ -85,12 +124,35 @@ map.setView(latlng); 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) { + console.error(e); + outputValue = `${e}`; + } + };

App

@@ -110,20 +172,17 @@

Loading WebAssembly module...

{:then}

Address: {addr.address}

-

({addr.latLon}) => ({addr.decimalDegrees})

+

({addr.latLon}) => ({addr.latLon})

- + -
- - -
+ {:catch message} diff --git a/xpin-wasm/src/lib.rs b/xpin-wasm/src/lib.rs index a2b54ae..8a594db 100644 --- a/xpin-wasm/src/lib.rs +++ b/xpin-wasm/src/lib.rs @@ -1,11 +1,52 @@ use std::str::FromStr; +use spatial_coordinate_systems::{Coordinate, LatLon}; use wasm_bindgen::prelude::*; use xpin::Address; +#[wasm_bindgen] +#[derive(Debug, Copy, Clone)] +pub enum CoordinateType { + DD, + DMS, + DMM, + UTM, + // Xpin, + Plus, +} + +impl Into 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 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] pub struct EncodedAddress { address: String, + /// The coordinates used to encode this address + src_coords: Option, + // coords_repr: Option, pub lat: f64, pub lon: f64, } @@ -18,6 +59,31 @@ impl EncodedAddress { format!("{}, {}", self.lat, self.lon) } + pub fn get_coords_repr_type(&self) -> Option { + 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 { + 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 { + 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 #[wasm_bindgen] pub fn from_lat_lon(lat: f64, lon: f64) -> Result { @@ -27,6 +93,26 @@ impl EncodedAddress { .map_err(|e| e.to_string()) } + /// Get an encoded address from a latitude/longitude + #[wasm_bindgen] + pub fn from_coordinate(i: &str) -> Result { + 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] pub fn from_address(addr_str: &str) -> Result { xpin::Address::from_str(addr_str) @@ -46,18 +132,12 @@ impl EncodedAddress { } } -#[wasm_bindgen] -pub fn parse(i: &str) -> Result { - 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 { fn from(addr: &Address) -> Self { let (lat, lon) = addr.as_lat_lon(); Self { address: addr.to_string(), + src_coords: None, lat, lon, }