From 1ae557c1332dcc971f95d5175bcca6fe490884f7 Mon Sep 17 00:00:00 2001 From: "Lakshmi Sowmya .L" Date: Sat, 8 Nov 2025 22:51:58 +0530 Subject: [PATCH] added convexhull: --- c/sedona-geos/src/lib.rs | 1 + c/sedona-geos/src/register.rs | 2 + c/sedona-geos/src/st_concavehull.rs | 79 +++++++++++++++++++ python/sedonadb/__init__.py | 0 .../tests/functions/test_functions.py | 55 +++++++++++++ 5 files changed, 137 insertions(+) create mode 100644 c/sedona-geos/src/st_concavehull.rs create mode 100644 python/sedonadb/__init__.py diff --git a/c/sedona-geos/src/lib.rs b/c/sedona-geos/src/lib.rs index 6aca11eb..aad87f32 100644 --- a/c/sedona-geos/src/lib.rs +++ b/c/sedona-geos/src/lib.rs @@ -23,6 +23,7 @@ pub mod register; mod st_area; mod st_buffer; mod st_centroid; +mod st_concavehull; mod st_convexhull; mod st_dwithin; mod st_isring; diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs index c4c5e3a3..e95422a4 100644 --- a/c/sedona-geos/src/register.rs +++ b/c/sedona-geos/src/register.rs @@ -21,6 +21,7 @@ use crate::{ st_area::st_area_impl, st_buffer::{st_buffer_impl, st_buffer_style_impl}, st_centroid::st_centroid_impl, + st_concavehull::st_concavehull_impl, st_convexhull::st_convex_hull_impl, st_dwithin::st_dwithin_impl, st_isring::st_is_ring_impl, @@ -50,6 +51,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> { ("st_centroid", st_centroid_impl()), ("st_contains", st_contains_impl()), ("st_convexhull", st_convex_hull_impl()), + ("st_concavehull", st_concavehull_impl()), ("st_coveredby", st_covered_by_impl()), ("st_covers", st_covers_impl()), ("st_crosses", st_crosses_impl()), diff --git a/c/sedona-geos/src/st_concavehull.rs b/c/sedona-geos/src/st_concavehull.rs new file mode 100644 index 00000000..61d60fc0 --- /dev/null +++ b/c/sedona-geos/src/st_concavehull.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use arrow_array::builder::BinaryBuilder; +use datafusion_common::{error::Result, DataFusionError}; +use datafusion_expr::ColumnarValue; +use geos::Geom; +use sedona_expr::scalar_udf::{ArgMatcher, ScalarKernelRef, SedonaScalarKernel}; +use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES; +use sedona_schema::datatypes::{SedonaType, WKB_GEOMETRY}; + +use crate::executor::GeosExecutor; + +/// ST_ConcaveHull(geometry, ratio) implementation using the geos crate +pub fn st_concavehull_impl() -> ScalarKernelRef { + Arc::new(STConcaveHull {}) +} + +#[derive(Debug)] +struct STConcaveHull {} + +impl SedonaScalarKernel for STConcaveHull { + fn return_type(&self, args: &[SedonaType]) -> Result> { + // Expect 2 arguments: geometry and numeric (ratio) + let matcher = ArgMatcher::new( + vec![ArgMatcher::is_geometry(), ArgMatcher::is_numeric()], + WKB_GEOMETRY, + ); + + matcher.match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + // Second argument is the ratio (float) + let ratio = GeosExecutor::get_f64_scalar(&args[1])?; + + // Only the first argument (geometry) is processed as WKB + let executor = GeosExecutor::new(&arg_types[0..1], &args[0..1]); + let mut builder = BinaryBuilder::with_capacity( + executor.num_iterations(), + WKB_MIN_PROBABLE_BYTES * executor.num_iterations(), + ); + + executor.execute_wkb_void(|maybe_geom| { + match maybe_geom { + Some(geom) => { + invoke_concave_hull(&geom, ratio, &mut builder)?; + builder.append_value([]); + } + _ => builder.append_null(), + } + Ok(()) + })?; + + executor.finish(Arc::new(builder.finish())) + } +} + +fn invoke_concave_hull( + geos_geom: &geos::Geometry, + ratio: f64, + writer: &mut impl std::io::Write, +) -> Result<()> { + // Compute the concave hull using GEOS + let geometry = geos_geom + .concave_hull(ratio, false) // false = do not allow holes + .map_err(|e| DataFusionError::Execution(format!("Failed to calculate concave hull: {e}")))?; + + // Convert result back to WKB bytes + let wkb = geometry + .to_wkb() + .map_err(|e| DataFusionError::Execution(format!("Failed to serialize hull: {e}")))?; + + writer.write_all(wkb.as_ref())?; + Ok(()) +} diff --git a/python/sedonadb/__init__.py b/python/sedonadb/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/sedonadb/tests/functions/test_functions.py b/python/sedonadb/tests/functions/test_functions.py index a30f783c..1ebc985f 100644 --- a/python/sedonadb/tests/functions/test_functions.py +++ b/python/sedonadb/tests/functions/test_functions.py @@ -594,6 +594,61 @@ def test_st_makeline(eng): "SELECT ST_MakeLine(ST_Point(0, 1), ST_GeomFromText('LINESTRING (2 3, 4 5)'))", "LINESTRING (0 1, 2 3, 4 5)", ) +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "target_percent", "expected"), + [ + # Null input should return null + (None, 0.5, None), + + # Simple polygon – concave hull of polygon should be the same polygon + ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 0.5, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"), + + # MultiPoint forming a concave hull shape + ( + "MULTIPOINT ((0 0), (1 0), (1 1), (0 1), (0.5 0.5))", + 0.5, + "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", + ), + + # More concave shape with small concavity + ( + "MULTIPOINT ((0 0), (2 0), (2 2), (0 2), (1 1))", + 0.7, + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", + ), + + # Single point — concave hull should return the same point + ("POINT (1 1)", 0.5, "POINT (1 1)"), + + # Empty geometry should remain empty + ("GEOMETRYCOLLECTION EMPTY", 0.5, "GEOMETRYCOLLECTION EMPTY"), + ], +) +def test_st_concavehull(eng, geom, target_percent, expected): + """Test ST_ConcaveHull on different geometry types.""" + eng = eng.create_or_skip() + + # NULL case + if expected is None: + eng.assert_query_result( + f"SELECT ST_ConcaveHull({geom_or_null(geom)}, {target_percent})", + expected, + ) + + # Empty geometry → should return empty geometry + elif "EMPTY" in expected.upper(): + eng.assert_query_result( + f"SELECT ST_IsEmpty(ST_ConcaveHull({geom_or_null(geom)}, {target_percent}))", + True, + ) + + # Otherwise, check geometrical equality + else: + eng.assert_query_result( + f"SELECT ST_Equals(ST_ConcaveHull({geom_or_null(geom)}, {target_percent}), {geom_or_null(expected)})", + True, + ) @pytest.mark.parametrize("eng", [SedonaDB, PostGIS])