From 218fa83c10b78bbfd06a7b6ef95757a110e1ba98 Mon Sep 17 00:00:00 2001 From: David Bottiau Date: Mon, 16 Sep 2024 00:45:24 +0200 Subject: [PATCH] Add `geo::is::valid()` function (#4711) Co-authored-by: Tobie Morgan Hitchcock --- core/src/fnc/args.rs | 8 +- core/src/fnc/geo.rs | 79 ++++++++++--------- core/src/fnc/mod.rs | 2 + .../script/modules/surrealdb/functions/geo.rs | 4 +- .../modules/surrealdb/functions/geo/is.rs | 11 +++ core/src/sql/geometry.rs | 44 ++++++++++- core/src/syn/parser/builtin.rs | 1 + sdk/fuzz/fuzz_targets/fuzz_executor.dict | 1 + sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict | 1 + sdk/tests/function.rs | 34 ++++++++ 10 files changed, 143 insertions(+), 42 deletions(-) create mode 100644 core/src/fnc/script/modules/surrealdb/functions/geo/is.rs diff --git a/core/src/fnc/args.rs b/core/src/fnc/args.rs index a9704b50..9878048a 100644 --- a/core/src/fnc/args.rs +++ b/core/src/fnc/args.rs @@ -1,7 +1,7 @@ use crate::err::Error; use crate::sql::value::Value; use crate::sql::{ - Array, Bytes, Closure, Datetime, Duration, Kind, Number, Object, Regex, Strand, Thing, + Array, Bytes, Closure, Datetime, Duration, Geometry, Kind, Number, Object, Regex, Strand, Thing, }; use std::vec::IntoIter; @@ -58,6 +58,12 @@ impl FromArg for Duration { } } +impl FromArg for Geometry { + fn from_arg(arg: Value) -> Result { + arg.coerce_to_geometry() + } +} + impl FromArg for Thing { fn from_arg(arg: Value) -> Result { arg.coerce_to_record() diff --git a/core/src/fnc/geo.rs b/core/src/fnc/geo.rs index bd978a96..2a7ca790 100644 --- a/core/src/fnc/geo.rs +++ b/core/src/fnc/geo.rs @@ -6,55 +6,45 @@ use geo::algorithm::centroid::Centroid; use geo::algorithm::chamberlain_duquette_area::ChamberlainDuquetteArea; use geo::algorithm::haversine_distance::HaversineDistance; -pub fn area((arg,): (Value,)) -> Result { +pub fn area((arg,): (Geometry,)) -> Result { match arg { - Value::Geometry(v) => match v { - Geometry::Point(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), - Geometry::Line(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), - Geometry::Polygon(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), - Geometry::MultiPoint(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), - Geometry::MultiLine(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), - Geometry::MultiPolygon(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), - Geometry::Collection(v) => Ok(v - .into_iter() - .collect::>() - .chamberlain_duquette_unsigned_area() - .into()), - }, - _ => Ok(Value::None), + Geometry::Point(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), + Geometry::Line(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), + Geometry::Polygon(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), + Geometry::MultiPoint(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), + Geometry::MultiLine(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), + Geometry::MultiPolygon(v) => Ok(v.chamberlain_duquette_unsigned_area().into()), + Geometry::Collection(v) => Ok(v + .into_iter() + .collect::>() + .chamberlain_duquette_unsigned_area() + .into()), } } -pub fn bearing(points: (Value, Value)) -> Result { - Ok(match points { - (Value::Geometry(Geometry::Point(v)), Value::Geometry(Geometry::Point(w))) => { - v.haversine_bearing(w).into() - } +pub fn bearing((v, w): (Geometry, Geometry)) -> Result { + Ok(match (v, w) { + (Geometry::Point(v), Geometry::Point(w)) => v.haversine_bearing(w).into(), _ => Value::None, }) } -pub fn centroid((arg,): (Value,)) -> Result { +pub fn centroid((arg,): (Geometry,)) -> Result { let centroid = match arg { - Value::Geometry(v) => match v { - Geometry::Point(v) => Some(v.centroid()), - Geometry::Line(v) => v.centroid(), - Geometry::Polygon(v) => v.centroid(), - Geometry::MultiPoint(v) => v.centroid(), - Geometry::MultiLine(v) => v.centroid(), - Geometry::MultiPolygon(v) => v.centroid(), - Geometry::Collection(v) => v.into_iter().collect::>().centroid(), - }, - _ => None, + Geometry::Point(v) => Some(v.centroid()), + Geometry::Line(v) => v.centroid(), + Geometry::Polygon(v) => v.centroid(), + Geometry::MultiPoint(v) => v.centroid(), + Geometry::MultiLine(v) => v.centroid(), + Geometry::MultiPolygon(v) => v.centroid(), + Geometry::Collection(v) => v.into_iter().collect::>().centroid(), }; Ok(centroid.map(Into::into).unwrap_or(Value::None)) } -pub fn distance(points: (Value, Value)) -> Result { - Ok(match points { - (Value::Geometry(Geometry::Point(v)), Value::Geometry(Geometry::Point(w))) => { - v.haversine_distance(&w).into() - } +pub fn distance((v, w): (Geometry, Geometry)) -> Result { + Ok(match (v, w) { + (Geometry::Point(v), Geometry::Point(w)) => v.haversine_distance(&w).into(), _ => Value::None, }) } @@ -66,7 +56,7 @@ pub mod hash { use crate::sql::geometry::Geometry; use crate::sql::value::Value; - pub fn encode((point, len): (Value, Option)) -> Result { + pub fn encode((arg, len): (Geometry, Option)) -> Result { let len = match len { Some(len) if (1..=12).contains(&len) => len, None => 12, @@ -76,8 +66,8 @@ pub mod hash { }) }; - Ok(match point { - Value::Geometry(Geometry::Point(v)) => geo::encode(v, len).into(), + Ok(match arg { + Geometry::Point(v) => geo::encode(v, len).into(), _ => Value::None, }) } @@ -89,3 +79,14 @@ pub mod hash { } } } + +pub mod is { + + use crate::err::Error; + use crate::sql::geometry::Geometry; + use crate::sql::value::Value; + + pub fn valid((arg,): (Geometry,)) -> Result { + Ok(arg.is_valid().into()) + } +} diff --git a/core/src/fnc/mod.rs b/core/src/fnc/mod.rs index 46a8443c..d84e3289 100644 --- a/core/src/fnc/mod.rs +++ b/core/src/fnc/mod.rs @@ -194,6 +194,7 @@ pub fn synchronous( "geo::distance" => geo::distance, "geo::hash::decode" => geo::hash::decode, "geo::hash::encode" => geo::hash::encode, + "geo::is::valid" => geo::is::valid, // "math::abs" => math::abs, "math::acos" => math::acos, @@ -639,6 +640,7 @@ pub async fn idiom( "distance" => geo::distance, "hash_decode" => geo::hash::decode, "hash_encode" => geo::hash::encode, + "is_valid" => geo::is::valid, ) } Value::Thing(_) => { diff --git a/core/src/fnc/script/modules/surrealdb/functions/geo.rs b/core/src/fnc/script/modules/surrealdb/functions/geo.rs index ee0b05b0..e8f14587 100644 --- a/core/src/fnc/script/modules/surrealdb/functions/geo.rs +++ b/core/src/fnc/script/modules/surrealdb/functions/geo.rs @@ -2,6 +2,7 @@ use super::run; use crate::fnc::script::modules::impl_module_def; mod hash; +mod is; #[non_exhaustive] pub struct Package; @@ -13,5 +14,6 @@ impl_module_def!( "bearing" => run, "centroid" => run, "distance" => run, - "hash" => (hash::Package) + "hash" => (hash::Package), + "is" => (is::Package) ); diff --git a/core/src/fnc/script/modules/surrealdb/functions/geo/is.rs b/core/src/fnc/script/modules/surrealdb/functions/geo/is.rs new file mode 100644 index 00000000..7bbfb4b9 --- /dev/null +++ b/core/src/fnc/script/modules/surrealdb/functions/geo/is.rs @@ -0,0 +1,11 @@ +use super::super::run; +use crate::fnc::script::modules::impl_module_def; + +#[non_exhaustive] +pub struct Package; + +impl_module_def!( + Package, + "geo::is", + "valid" => run +); diff --git a/core/src/sql/geometry.rs b/core/src/sql/geometry.rs index 5ea04d03..b375771d 100644 --- a/core/src/sql/geometry.rs +++ b/core/src/sql/geometry.rs @@ -5,7 +5,7 @@ use crate::sql::fmt::Fmt; use crate::sql::value::Value; use geo::algorithm::contains::Contains; use geo::algorithm::intersects::Intersects; -use geo::{Coord, LineString, Point, Polygon}; +use geo::{Coord, LineString, LinesIter, Point, Polygon}; use geo_types::{MultiLineString, MultiPoint, MultiPolygon}; use revision::revisioned; use serde::{Deserialize, Serialize}; @@ -67,6 +67,48 @@ impl Geometry { pub fn is_collection(&self) -> bool { matches!(self, Self::Collection(_)) } + /// Check if this has valid latitude and longitude points: + /// * -90 <= lat <= 90 + /// * -180 <= lng <= 180 + pub fn is_valid(&self) -> bool { + match self { + Geometry::Point(p) => { + (-90.0..=90.0).contains(&p.0.y) && (-180.0..=180.0).contains(&p.0.x) + } + Geometry::MultiPoint(v) => v + .iter() + .all(|p| (-90.0..=90.0).contains(&p.0.y) && (-180.0..=180.0).contains(&p.0.x)), + Geometry::Line(v) => v.lines_iter().all(|l| { + (-90.0..=90.0).contains(&l.start.y) + && (-180.0..=180.0).contains(&l.start.x) + && (-90.0..=90.0).contains(&l.end.y) + && (-180.0..=180.0).contains(&l.end.x) + }), + Geometry::Polygon(v) => v.lines_iter().all(|l| { + (-90.0..=90.0).contains(&l.start.y) + && (-180.0..=180.0).contains(&l.start.x) + && (-90.0..=90.0).contains(&l.end.y) + && (-180.0..=180.0).contains(&l.end.x) + }), + Geometry::MultiLine(v) => v.iter().all(|l| { + l.lines_iter().all(|l| { + (-90.0..=90.0).contains(&l.start.y) + && (-180.0..=180.0).contains(&l.start.x) + && (-90.0..=90.0).contains(&l.end.y) + && (-180.0..=180.0).contains(&l.end.x) + }) + }), + Geometry::MultiPolygon(v) => v.iter().all(|p| { + p.lines_iter().all(|l| { + (-90.0..=90.0).contains(&l.start.y) + && (-180.0..=180.0).contains(&l.start.x) + && (-90.0..=90.0).contains(&l.end.y) + && (-180.0..=180.0).contains(&l.end.x) + }) + }), + Geometry::Collection(v) => v.iter().all(Geometry::is_valid), + } + } /// Get the type of this Geometry as text pub fn as_type(&self) -> &'static str { match self { diff --git a/core/src/syn/parser/builtin.rs b/core/src/syn/parser/builtin.rs index bc31fe63..c2f0c63c 100644 --- a/core/src/syn/parser/builtin.rs +++ b/core/src/syn/parser/builtin.rs @@ -182,6 +182,7 @@ pub(crate) static PATHS: phf::Map, PathKind> = phf_map! { UniCase::ascii("geo::distance") => PathKind::Function, UniCase::ascii("geo::hash::decode") => PathKind::Function, UniCase::ascii("geo::hash::encode") => PathKind::Function, + UniCase::ascii("geo::is::valid") => PathKind::Function, // UniCase::ascii("http::head") => PathKind::Function, UniCase::ascii("http::get") => PathKind::Function, diff --git a/sdk/fuzz/fuzz_targets/fuzz_executor.dict b/sdk/fuzz/fuzz_targets/fuzz_executor.dict index 9cbd9bb1..ed453524 100644 --- a/sdk/fuzz/fuzz_targets/fuzz_executor.dict +++ b/sdk/fuzz/fuzz_targets/fuzz_executor.dict @@ -231,6 +231,7 @@ "hash" "geo::hash::decode(" "geo::hash::encode(" +"geo::is::valid(" "http" "http::" "http::head(" diff --git a/sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict b/sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict index d14d3af7..3cb11a75 100644 --- a/sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict +++ b/sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict @@ -230,6 +230,7 @@ "hash" "geo::hash::decode(" "geo::hash::encode(" +"geo::is::valid(" "http" "http::" "http::head(" diff --git a/sdk/tests/function.rs b/sdk/tests/function.rs index f9d073c2..258f0571 100644 --- a/sdk/tests/function.rs +++ b/sdk/tests/function.rs @@ -1902,6 +1902,40 @@ async fn function_parse_geo_hash_decode() -> Result<(), Error> { Ok(()) } +#[tokio::test] +async fn function_geo_is_valid() -> Result<(), Error> { + let sql = r#" + RETURN geo::is::valid((-0.118092, 51.509865)); + RETURN geo::is::valid((-181.0, 51.509865)); + RETURN geo::is::valid((181.0, 51.509865)); + RETURN geo::is::valid((-0.118092, -91.0)); + RETURN geo::is::valid((-0.118092, 91.0)); + "#; + let mut test = Test::new(sql).await?; + // + let tmp = test.next()?.result?; + let val = Value::from(true); + assert_eq!(tmp, val); + // + let tmp = test.next()?.result?; + let val = Value::from(false); + assert_eq!(tmp, val); + // + let tmp = test.next()?.result?; + let val = Value::from(false); + assert_eq!(tmp, val); + // + let tmp = test.next()?.result?; + let val = Value::from(false); + assert_eq!(tmp, val); + // + let tmp = test.next()?.result?; + let val = Value::from(false); + assert_eq!(tmp, val); + // + Ok(()) +} + // -------------------------------------------------- // math // --------------------------------------------------