Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions c/sedona-geos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions c/sedona-geos/src/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()),
Expand Down
79 changes: 79 additions & 0 deletions c/sedona-geos/src/st_concavehull.rs
Original file line number Diff line number Diff line change
@@ -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<Option<SedonaType>> {
// 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<ColumnarValue> {
// 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(())
}
Empty file added python/sedonadb/__init__.py
Empty file.
55 changes: 55 additions & 0 deletions python/sedonadb/tests/functions/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
Loading