diff --git a/Cargo.lock b/Cargo.lock index d3c5116..180f62f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,6 +350,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.27" @@ -577,6 +586,16 @@ dependencies = [ "want", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -1354,6 +1373,7 @@ dependencies = [ "nom", "pluscodes", "thiserror", + "url", "utm", ] @@ -1512,6 +1532,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.26.0" @@ -1702,12 +1737,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.10.1" @@ -1726,6 +1776,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utm" version = "0.1.6" diff --git a/spatial-coordinate-systems/Cargo.toml b/spatial-coordinate-systems/Cargo.toml index 444ffbf..ed3c227 100644 --- a/spatial-coordinate-systems/Cargo.toml +++ b/spatial-coordinate-systems/Cargo.toml @@ -22,3 +22,4 @@ nom = "7.1.3" pluscodes = "0.5.0" utm = "0.1.6" thiserror = "1.0.38" +url = "2.3.1" diff --git a/spatial-coordinate-systems/src/error.rs b/spatial-coordinate-systems/src/error.rs index 1810f54..995bff5 100644 --- a/spatial-coordinate-systems/src/error.rs +++ b/spatial-coordinate-systems/src/error.rs @@ -21,6 +21,13 @@ pub enum Error { NomErr(String), #[error("No UTM zone letter")] NoUtmZoneLetter, + /// The url could not be parsed at all + #[error("Could not parse URL: {0}")] + UrlParseError(url::ParseError), + + /// A latitude/longitude could not be extracted from the url + #[error("Could not find lat/lon in URL: {0}")] + NoUrlLatLon(String), } impl From for Error { @@ -35,6 +42,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: url::ParseError) -> Self { + Self::UrlParseError(e) + } +} + impl From> for Error { fn from(e: nom::Err) -> Self { Self::NomErr(format!("{e:?}")) diff --git a/spatial-coordinate-systems/src/latlon.rs b/spatial-coordinate-systems/src/latlon.rs index 66d18f0..2014644 100644 --- a/spatial-coordinate-systems/src/latlon.rs +++ b/spatial-coordinate-systems/src/latlon.rs @@ -1,6 +1,16 @@ use std::fmt; -use crate::Error; +use nom::{ + character::complete::space0, + combinator::{eof, map_res}, + sequence::tuple, + IResult, +}; + +use crate::{ + common::{optional_separator, parse_f64}, + Error, +}; #[derive(PartialEq, Debug, Clone, Copy)] pub struct LatLon { @@ -26,6 +36,23 @@ impl LatLon { pub fn get_lon(&self) -> f64 { self.lon } + + /// Parse a latitude and longitude as two floating point numbers separated by a comma and/or whitespace only (no bearings) + /// Parse only the entire string + pub fn parse_full(i: &str) -> IResult<&str, Self> { + map_res( + tuple(( + space0, + parse_f64, + optional_separator(','), + space0, + parse_f64, + space0, + eof, + )), + |(_, lat, _, _, lon, _, _)| Self::new(lat, lon), + )(i) + } } impl fmt::Display for LatLon { diff --git a/spatial-coordinate-systems/src/lib.rs b/spatial-coordinate-systems/src/lib.rs index a6341e1..f6a828c 100644 --- a/spatial-coordinate-systems/src/lib.rs +++ b/spatial-coordinate-systems/src/lib.rs @@ -6,6 +6,7 @@ pub mod dmm; pub mod dms; pub mod latlon; pub mod plus; +pub mod urls; pub mod utm; pub use error::Error; // pub mod xpin; diff --git a/spatial-coordinate-systems/src/urls.rs b/spatial-coordinate-systems/src/urls.rs new file mode 100644 index 0000000..dbfab59 --- /dev/null +++ b/spatial-coordinate-systems/src/urls.rs @@ -0,0 +1,72 @@ +use crate::{Error, LatLon}; +use std::str::FromStr; +use url::Url; + +#[derive(PartialEq, Debug, Clone)] +pub struct CoordinateUrls { + google_maps: String, + latlon: LatLon, +} + +impl CoordinateUrls { + pub fn parse(i: &str) -> Result { + let url = Url::parse(i.trim())?; + + // Try to parse a url from google maps + url.query_pairs() + .find_map(|(key, value)| { + if key != "query" { + return None; + } + + LatLon::parse_full(value.as_ref()) + .map(|(_, ret)| ret) + .ok() + .map(Self::try_from) + }) + .ok_or_else(|| Error::NoUrlLatLon(i.to_string()))? + } +} + +impl From<&CoordinateUrls> for LatLon { + fn from(coordinate: &CoordinateUrls) -> LatLon { + coordinate.latlon + } +} + +impl TryFrom for CoordinateUrls { + type Error = Error; + + fn try_from(latlon: LatLon) -> Result { + let latlon_str = format!("{},{}", latlon.get_lat(), latlon.get_lon()); + + Ok(Self { + google_maps: String::from(Url::parse_with_params( + "https://www.google.com/maps/search/", + &[("api", "1"), ("query", &latlon_str)], + )?), + latlon, + }) + } +} + +impl FromStr for CoordinateUrls { + type Err = Error; + + fn from_str(i: &str) -> Result { + Self::parse(i).map_err(Into::into) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_general() { + assert!(dbg!(CoordinateUrls::parse( + "https://www.google.com/maps/search/?query=27,23" + )) + .is_ok()); + } +}