diff --git a/Cargo.lock b/Cargo.lock index 180f62f..78f7cfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1375,6 +1375,7 @@ dependencies = [ "thiserror", "url", "utm", + "wasm-bindgen", ] [[package]] diff --git a/spatial-coordinate-systems/Cargo.toml b/spatial-coordinate-systems/Cargo.toml index ed3c227..8a4d8ee 100644 --- a/spatial-coordinate-systems/Cargo.toml +++ b/spatial-coordinate-systems/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +wasm_bindgen = ["dep:wasm-bindgen"] + [dependencies] # Excluding because rustc-serialize does not work with wasm # dms-coordinates v1.1.0 @@ -23,3 +26,5 @@ pluscodes = "0.5.0" utm = "0.1.6" thiserror = "1.0.38" url = "2.3.1" + +wasm-bindgen={version="0.2",optional=true} diff --git a/spatial-coordinate-systems/src/all.rs b/spatial-coordinate-systems/src/all.rs index a183651..678edc3 100644 --- a/spatial-coordinate-systems/src/all.rs +++ b/spatial-coordinate-systems/src/all.rs @@ -28,9 +28,9 @@ impl TryFrom for Coordinates { fn try_from(latlon: LatLon) -> Result { Ok(Self { latlon, - dd: dd::Coordinate::try_from(latlon)?, - dms: dms::Coordinate::try_from(latlon)?, - dmm: dmm::Coordinate::try_from(latlon)?, + dd: dd::Coordinate::from(latlon), + dms: dms::Coordinate::from(latlon), + dmm: dmm::Coordinate::from(latlon), utm: utm::Coordinate::try_from(latlon)?, plus: plus::Coordinate::try_from(latlon)?, }) diff --git a/spatial-coordinate-systems/src/dd.rs b/spatial-coordinate-systems/src/dd.rs index 107fbe4..3ca3782 100644 --- a/spatial-coordinate-systems/src/dd.rs +++ b/spatial-coordinate-systems/src/dd.rs @@ -92,11 +92,9 @@ impl From<&Coordinate> for LatLon { } } -impl TryFrom for Coordinate { - type Error = Error; - - fn try_from(value: LatLon) -> Result { - Ok(Self(value)) +impl From for Coordinate { + fn from(value: LatLon) -> Self { + Self(value) } } diff --git a/spatial-coordinate-systems/src/dmm.rs b/spatial-coordinate-systems/src/dmm.rs index b6f73a7..3d5576b 100644 --- a/spatial-coordinate-systems/src/dmm.rs +++ b/spatial-coordinate-systems/src/dmm.rs @@ -148,15 +148,13 @@ impl From<&Coordinate> for LatLon { } } -impl TryFrom for Coordinate { - type Error = Error; - - fn try_from(value: LatLon) -> Result { - Ok(Self( - DMM::from_decimal_degrees(value.get_lat(), true), - DMM::from_decimal_degrees(value.get_lon(), false), - value, - )) +impl From for Coordinate { + fn from(latlon: LatLon) -> Self { + Self( + DMM::from_decimal_degrees(latlon.get_lat(), true), + DMM::from_decimal_degrees(latlon.get_lon(), false), + latlon, + ) } } diff --git a/spatial-coordinate-systems/src/dms.rs b/spatial-coordinate-systems/src/dms.rs index 34cca40..1f7c060 100644 --- a/spatial-coordinate-systems/src/dms.rs +++ b/spatial-coordinate-systems/src/dms.rs @@ -164,15 +164,13 @@ impl From<&Coordinate> for LatLon { } } -impl TryFrom for Coordinate { - type Error = Error; - - fn try_from(value: LatLon) -> Result { - Ok(Self( - DMS::from_decimal_degrees(value.get_lat(), true), - DMS::from_decimal_degrees(value.get_lon(), false), - value, - )) +impl From for Coordinate { + fn from(latlon: LatLon) -> Self { + Self( + DMS::from_decimal_degrees(latlon.get_lat(), true), + DMS::from_decimal_degrees(latlon.get_lon(), false), + latlon, + ) } } diff --git a/spatial-coordinate-systems/src/latlon.rs b/spatial-coordinate-systems/src/latlon.rs index 2014644..5a52d10 100644 --- a/spatial-coordinate-systems/src/latlon.rs +++ b/spatial-coordinate-systems/src/latlon.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "wasm_bindgen")] +use wasm_bindgen::prelude::*; use std::fmt; use nom::{ @@ -12,6 +14,7 @@ use crate::{ Error, }; +#[cfg_attr(feature = "wasm_bindgen", wasm_bindgen(getter_with_clone))] #[derive(PartialEq, Debug, Clone, Copy)] pub struct LatLon { lat: f64, diff --git a/spatial-coordinate-systems/src/lib.rs b/spatial-coordinate-systems/src/lib.rs index f6a828c..e53dbbc 100644 --- a/spatial-coordinate-systems/src/lib.rs +++ b/spatial-coordinate-systems/src/lib.rs @@ -89,6 +89,10 @@ impl Coordinate { map(dd::Coordinate::parse, Coordinate::DD), // map(xpin::Coordinate::parse, Cordinate::Xpin), map(plus::Coordinate::parse, Coordinate::Plus), + // Try to parse as a URL last + map(urls::CoordinateUrls::parse, |coordinate_urls| { + Coordinate::DD(dd::Coordinate::from(coordinate_urls.latlon)) + }), )), space0, eof, @@ -101,9 +105,9 @@ impl Coordinate { let lat_lon = LatLon::from(self); Ok(match coordinate_type { - CoordinateType::DD => Self::DD(dd::Coordinate::try_from(lat_lon)?), - CoordinateType::DMS => Self::DMS(dms::Coordinate::try_from(lat_lon)?), - CoordinateType::DMM => Self::DMM(dmm::Coordinate::try_from(lat_lon)?), + CoordinateType::DD => Self::DD(dd::Coordinate::from(lat_lon)), + CoordinateType::DMS => Self::DMS(dms::Coordinate::from(lat_lon)), + CoordinateType::DMM => Self::DMM(dmm::Coordinate::from(lat_lon)), CoordinateType::UTM => Self::UTM(utm::Coordinate::try_from(lat_lon)?), CoordinateType::Plus => Self::Plus(plus::Coordinate::try_from(lat_lon)?), }) diff --git a/spatial-coordinate-systems/src/plus.rs b/spatial-coordinate-systems/src/plus.rs index 22d9f7d..487028d 100644 --- a/spatial-coordinate-systems/src/plus.rs +++ b/spatial-coordinate-systems/src/plus.rs @@ -56,14 +56,14 @@ impl From<&Coordinate> for LatLon { impl TryFrom for Coordinate { type Error = pluscodes::Error; - fn try_from(value: LatLon) -> Result { + fn try_from(latlon: LatLon) -> Result { pluscodes::encode( &pluscodes::Coordinate { - latitude: value.get_lat(), - longitude: value.get_lon(), + latitude: latlon.get_lat(), + longitude: latlon.get_lon(), }, PLUSCODE_LENGTH, ) - .map(|pluscode| Coordinate(pluscode, value)) + .map(|pluscode| Coordinate(pluscode, latlon)) } } diff --git a/spatial-coordinate-systems/src/urls.rs b/spatial-coordinate-systems/src/urls.rs index 19d49e2..b2fca01 100644 --- a/spatial-coordinate-systems/src/urls.rs +++ b/spatial-coordinate-systems/src/urls.rs @@ -1,75 +1,110 @@ -use crate::{Error, LatLon}; +#[cfg(feature = "wasm_bindgen")] +use wasm_bindgen::prelude::*; +use crate::{common::parse_f64, Error, LatLon}; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::digit1, + combinator::{eof, fail, map, map_res, rest}, + error::context, + sequence::tuple, + IResult, +}; use std::str::FromStr; use url::Url; // TODO: Set a reasonable OSM zoom level const OPENSTREETMAP_ZOOM_LEVEL: u8 = 19; +#[cfg_attr(feature = "wasm_bindgen", wasm_bindgen(getter_with_clone))] #[derive(PartialEq, Debug, Clone)] pub struct CoordinateUrls { // TODO: These should be getters only pub google_maps: String, pub openstreetmap: String, + pub waze: String, + pub apple_maps: String, + pub osmand: String, + pub bing_maps: String, pub latlon: LatLon, } impl CoordinateUrls { - pub fn parse(i: &str) -> Result { - let url = Url::parse(i.trim())?; + pub fn parse(i: &str) -> IResult<&str, Self> { + // let (_, url) = Self::parse_url_full(i)?; + // TODO: Do we have to convert to url for each of these? + map( + alt((Self::parse_google_maps, Self::parse_openstreetmap)), + Self::from, + )(i) + } - let pairs = url.query_pairs(); - let mut possible_latlon = vec![]; + fn parse_url_full(i: &str) -> IResult<&str, Url> { + map_res(rest, |i: &str| Url::parse(i.trim()))(i) + } - for (key, value) in pairs { + fn parse_google_maps(i: &str) -> IResult<&str, LatLon> { + let (_, url) = Self::parse_url_full(i)?; + + for (key, value) in url.query_pairs() { if key == "query" { - possible_latlon.push(value.to_string()); - } - } - - if let Some(fragment) = url.fragment() { - if let Some(("map", v)) = fragment.split_once("=") { - if let Some((_, lat_slash_lon)) = v.split_once("/") { - if let Some((lat, lon)) = lat_slash_lon.split_once("/") { - possible_latlon.push(format!("{lat},{lon}")); - } + if let Ok((_str, ret)) = LatLon::parse_full(value.as_ref()) { + return Ok(("", ret)); } } } - // Try to parse a url from google maps - possible_latlon - .iter() - .find_map(|value| { - LatLon::parse_full(value.as_ref()) - .map(|(_, ret)| ret) - .ok() - .map(Self::try_from) - }) - .ok_or_else(|| Error::NoUrlLatLon(i.to_string()))? + context("latlon not found in url", fail)(i) } - // fn parse_openstreetmap(i: Url) ->IResult<(), LatLon> { - // map_res(tuple(( - // tag("map="), - // digit1, - // tag("/"), - // parse_f64 , - // tag("/"), - // parse_f64 , - // eof - // )), |( - // _, - // _, - // _, - // lat , - // _, - // lon , - // _, - // )| { - // LatLon::new(lat,lon) + fn parse_openstreetmap(i: &str) -> IResult<&str, LatLon> { + let (_, url) = Self::parse_url_full(i)?; - // })(i.fragment().ok_or((Err(())))) - // } + // First check if they are querying anything + // https://www.openstreetmap.org/query?lat=12.34&lon=56.78#... + { + let get_float = |k: &str| { + url.query_pairs() + .filter(|(key, _value)| key == k) + .find_map(|(_key, value)| parse_f64(&value).ok().map(|(_, ret)| ret)) + }; + + let lat = get_float("lat"); + let lon = get_float("lat"); + + if let Some((lat, lon)) = lat.zip(lon) { + if let Ok(ret) = LatLon::new(lat, lon) { + return Ok(("", ret)); + } + } + } + + // Now check the fragment + // https://www.openstreetmap.org/query?lat=12.34&lon=56.78#...map=19/12.34/56.78 + { + // TODO: Do not unwrap + if let Some(fragment) = url.fragment() { + if let Ok((_str, ret)) = map_res( + tuple(( + tag("map="), + // Zoom level + digit1, + tag("/"), + parse_f64, + tag("/"), + parse_f64, + eof, + )), + |(_map_equals, _zoom, _, lat, _, lon, _eof)| LatLon::new(lat, lon), + )(fragment) + { + return Ok(("", ret)); + } + } + } + + context("latlon not found in url", fail)(i) + } } impl From<&CoordinateUrls> for LatLon { @@ -78,29 +113,76 @@ impl From<&CoordinateUrls> for LatLon { } } -impl TryFrom for CoordinateUrls { - type Error = Error; - - fn try_from(latlon: LatLon) -> Result { +impl From for CoordinateUrls { + fn from(latlon: LatLon) -> Self { let (lat, lon) = (latlon.get_lat(), latlon.get_lon()); - let latlon_str = format!("{lat},{lon}"); + let (lat_str, lon_str) = (format!("{lat}"), format!("{lon}")); + let ll = format!("{lat},{lon}"); + + let google_maps = String::from( + Url::parse_with_params( + "https://www.google.com/maps/search/", + &[("api", "1"), ("query", ll.as_ref())], + ) + .unwrap(), + ); let openstreetmap = { - let mut ret = Url::parse("https://www.openstreetmap.org/")?; + let mut ret = Url::parse_with_params( + "https://www.openstreetmap.org/query", + &[("lat", lat_str.as_str()), ("lon", lon_str.as_str())], + ) + .unwrap(); ret.set_fragment(Some(&format!("map={OPENSTREETMAP_ZOOM_LEVEL}/{lat}/{lon}"))); String::from(ret) }; - Ok(Self { - google_maps: String::from(Url::parse_with_params( - "https://www.google.com/maps/search/", - &[("api", "1"), ("query", &latlon_str)], - )?), + // https://developers.google.com/waze/deeplinks + let waze = String::from( + Url::parse_with_params( + "https://waze.com/ul", + // TODO: Figure out what nagivate=yes does + &[("ll", ll.as_ref()), ("navigate", "yes")], + ) + .unwrap(), + ); + + // https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/MapLinks/MapLinks.html + // http://maps.apple.com/?ll=50.894967,4.341626 + let apple_maps = String::from( + Url::parse_with_params("https://maps.apple.com/", &[("ll", ll.as_str())]).unwrap(), + ); + + let osmand = String::from( + Url::parse_with_params( + "https://osmand.net/go", + // TODO: Find a good zoom level + &[ + ("lat", lat_str.as_ref()), + ("lon", lon_str.as_ref()), + ("z", "17"), + ], + ) + .unwrap(), + ); + + // https://learn.microsoft.com/en-us/bingmaps/articles/create-a-custom-map-url + let bing_maps = String::from( + Url::parse_with_params("https://bing.com/maps/default.aspx", &[("cp", ll.as_str())]) + .unwrap(), + ); + + Self { + google_maps, openstreetmap, latlon, - }) + waze, + apple_maps, + osmand, + bing_maps, + } } } @@ -108,7 +190,7 @@ impl FromStr for CoordinateUrls { type Err = Error; fn from_str(i: &str) -> Result { - Self::parse(i).map_err(Into::into) + Self::parse(i).map(|(_, ret)| ret).map_err(Into::into) } } diff --git a/xpin-wasm/Cargo.toml b/xpin-wasm/Cargo.toml index f51b579..c882db8 100644 --- a/xpin-wasm/Cargo.toml +++ b/xpin-wasm/Cargo.toml @@ -11,5 +11,5 @@ crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" xpin = {path=".."} -spatial-coordinate-systems = {path="../spatial-coordinate-systems/"} +spatial-coordinate-systems = {path="../spatial-coordinate-systems/",features=["wasm_bindgen"]} console_error_panic_hook = "0.1.7" diff --git a/xpin-wasm/src/lib.rs b/xpin-wasm/src/lib.rs index 5dd793e..4305bcd 100644 --- a/xpin-wasm/src/lib.rs +++ b/xpin-wasm/src/lib.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use spatial_coordinate_systems::{Coordinate, LatLon}; +use spatial_coordinate_systems::{urls::CoordinateUrls, Coordinate, LatLon}; use wasm_bindgen::prelude::*; use xpin::Address; @@ -177,33 +177,31 @@ impl TryFrom<&'_ Address<'_>> for EncodedAddress { address: addr.to_string(), lat_lon: Box::new([lat, lon]), src_coords, - coordinate_urls: spatial_coordinate_systems::urls::CoordinateUrls::try_from( - all_coordinates.latlon, - ) - .map_err(|e| e.to_string())? - .into(), + coordinate_urls: CoordinateUrls::try_from(all_coordinates.latlon) + .map_err(|e| e.to_string())? + .into(), decimal_degrees: format!("{}, {}", lat, lon), all_coordinates: Coordinates::from(&all_coordinates), }) } } -#[wasm_bindgen(getter_with_clone)] -#[derive(Debug, Clone)] -pub struct CoordinateUrls { - // TODO: These should be getters only - pub google_maps: String, - pub openstreetmap: String, -} +// #[wasm_bindgen(getter_with_clone)] +// #[derive(Debug, Clone)] +// pub struct CoordinateUrls { +// // TODO: These should be getters only +// pub google_maps: String, +// pub openstreetmap: String, +// } -impl From for CoordinateUrls { - fn from(value: spatial_coordinate_systems::urls::CoordinateUrls) -> Self { - Self { - google_maps: value.google_maps, - openstreetmap: value.openstreetmap, - } - } -} +// impl From for CoordinateUrls { +// fn from(value: spatial_coordinate_systems::urls::CoordinateUrls) -> Self { +// Self { +// google_maps: value.google_maps, +// openstreetmap: value.openstreetmap, +// } +// } +// } #[cfg(test)] mod tests {