Skip to content

Commit 5493b8e

Browse files
authored
feat: Implement ST_Crosses and ST_Overlaps predicates (#204)
1 parent ce779e7 commit 5493b8e

File tree

5 files changed

+222
-4
lines changed

5 files changed

+222
-4
lines changed

c/sedona-geos/benches/geos-functions.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,34 @@ fn criterion_benchmark(c: &mut Criterion) {
267267
"st_within",
268268
ArrayScalar(Polygon(10), Polygon(500)),
269269
);
270+
benchmark::scalar(
271+
c,
272+
&f,
273+
"geos",
274+
"st_crosses",
275+
ArrayScalar(Polygon(10), Polygon(10)),
276+
);
277+
benchmark::scalar(
278+
c,
279+
&f,
280+
"geos",
281+
"st_crosses",
282+
ArrayScalar(Polygon(10), Polygon(500)),
283+
);
284+
benchmark::scalar(
285+
c,
286+
&f,
287+
"geos",
288+
"st_overlaps",
289+
ArrayScalar(Polygon(10), Polygon(10)),
290+
);
291+
benchmark::scalar(
292+
c,
293+
&f,
294+
"geos",
295+
"st_overlaps",
296+
ArrayScalar(Polygon(10), Polygon(500)),
297+
);
270298
}
271299

272300
criterion_group!(benches, criterion_benchmark);

c/sedona-geos/src/binary_predicates.rs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
use std::sync::Arc;
1818

1919
use crate::geos::{
20-
BinaryPredicate, Contains, CoveredBy, Covers, Disjoint, Equals, GeosPredicate, Intersects,
21-
Touches, Within,
20+
BinaryPredicate, Contains, CoveredBy, Covers, Crosses, Disjoint, Equals, GeosPredicate,
21+
Intersects, Overlaps, Touches, Within,
2222
};
2323
use arrow_array::builder::BooleanBuilder;
2424
use arrow_schema::DataType;
@@ -61,6 +61,14 @@ pub fn st_within_impl() -> ScalarKernelRef {
6161
Arc::new(GeosPredicate::<Within>::default())
6262
}
6363

64+
pub fn st_crosses_impl() -> ScalarKernelRef {
65+
Arc::new(GeosPredicate::<Crosses>::default())
66+
}
67+
68+
pub fn st_overlaps_impl() -> ScalarKernelRef {
69+
Arc::new(GeosPredicate::<Overlaps>::default())
70+
}
71+
6472
impl<Op: BinaryPredicate> SedonaScalarKernel for GeosPredicate<Op> {
6573
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
6674
let matcher: ArgMatcher = ArgMatcher::new(
@@ -377,4 +385,81 @@ mod tests {
377385
let expected: ArrayRef = arrow_array!(Boolean, [Some(true), Some(false), None]);
378386
assert_array_equal(&tester.invoke_array_array(arg1, arg2).unwrap(), &expected);
379387
}
388+
389+
#[rstest]
390+
fn crosses_udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) {
391+
use datafusion_common::ScalarValue;
392+
let udf = SedonaScalarUDF::from_kernel("st_crosses", st_crosses_impl());
393+
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone(), sedona_type]);
394+
tester.assert_return_type(DataType::Boolean);
395+
396+
let result = tester
397+
.invoke_scalar_scalar("LINESTRING (0 0, 1 1)", "LINESTRING (0 1, 1 0)")
398+
.unwrap();
399+
tester.assert_scalar_result_equals(result, true);
400+
401+
let result = tester
402+
.invoke_scalar_scalar(ScalarValue::Null, ScalarValue::Null)
403+
.unwrap();
404+
assert!(result.is_null());
405+
406+
let arg1 = create_array(
407+
&[
408+
Some("LINESTRING (0 0, 1 1)"),
409+
Some("LINESTRING (0 0, 1 0)"),
410+
None,
411+
],
412+
&WKB_GEOMETRY,
413+
);
414+
let arg2 = create_array(
415+
&[
416+
Some("LINESTRING (0 1, 1 0)"),
417+
Some("POLYGON ((2 2, 2 3, 3 3, 3 2, 2 2))"),
418+
Some("LINESTRING (0 0, 1 1)"),
419+
],
420+
&WKB_GEOMETRY,
421+
);
422+
let expected: ArrayRef = arrow_array!(Boolean, [Some(true), Some(false), None]);
423+
assert_array_equal(&tester.invoke_array_array(arg1, arg2).unwrap(), &expected);
424+
}
425+
426+
#[rstest]
427+
fn overlaps_udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) {
428+
use datafusion_common::ScalarValue;
429+
let udf = SedonaScalarUDF::from_kernel("st_overlaps", st_overlaps_impl());
430+
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone(), sedona_type]);
431+
tester.assert_return_type(DataType::Boolean);
432+
433+
let result = tester
434+
.invoke_scalar_scalar(
435+
"POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))",
436+
"POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))",
437+
)
438+
.unwrap();
439+
tester.assert_scalar_result_equals(result, true);
440+
441+
let result = tester
442+
.invoke_scalar_scalar(ScalarValue::Null, ScalarValue::Null)
443+
.unwrap();
444+
assert!(result.is_null());
445+
446+
let arg1 = create_array(
447+
&[
448+
Some("POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))"),
449+
Some("LINESTRING (0 0, 2 0)"),
450+
None,
451+
],
452+
&WKB_GEOMETRY,
453+
);
454+
let arg2 = create_array(
455+
&[
456+
Some("POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))"),
457+
Some("LINESTRING (2 0, 3 0)"),
458+
Some("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"),
459+
],
460+
&WKB_GEOMETRY,
461+
);
462+
let expected: ArrayRef = arrow_array!(Boolean, [Some(true), Some(false), None]);
463+
assert_array_equal(&tester.invoke_array_array(arg1, arg2).unwrap(), &expected);
464+
}
380465
}

c/sedona-geos/src/geos.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,21 @@ impl BinaryPredicate for Touches {
9696
lhs.touches(rhs)
9797
}
9898
}
99+
100+
/// Check if the geometries cross
101+
#[derive(Debug, Default)]
102+
pub struct Crosses {}
103+
impl BinaryPredicate for Crosses {
104+
fn evaluate(lhs: &Geometry, rhs: &Geometry) -> GResult<bool> {
105+
lhs.crosses(rhs)
106+
}
107+
}
108+
109+
/// Check if the geometries overlap
110+
#[derive(Debug, Default)]
111+
pub struct Overlaps {}
112+
impl BinaryPredicate for Overlaps {
113+
fn evaluate(lhs: &Geometry, rhs: &Geometry) -> GResult<bool> {
114+
lhs.overlaps(rhs)
115+
}
116+
}

c/sedona-geos/src/register.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ use crate::{
2424
};
2525

2626
use crate::binary_predicates::{
27-
st_contains_impl, st_covered_by_impl, st_covers_impl, st_disjoint_impl, st_equals_impl,
28-
st_intersects_impl, st_touches_impl, st_within_impl,
27+
st_contains_impl, st_covered_by_impl, st_covers_impl, st_crosses_impl, st_disjoint_impl,
28+
st_equals_impl, st_intersects_impl, st_overlaps_impl, st_touches_impl, st_within_impl,
2929
};
3030

3131
use crate::overlay::{
@@ -54,5 +54,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> {
5454
("st_touches", st_touches_impl()),
5555
("st_union", st_union_impl()),
5656
("st_within", st_within_impl()),
57+
("st_crosses", st_crosses_impl()),
58+
("st_overlaps", st_overlaps_impl()),
5759
]
5860
}

python/sedonadb/tests/functions/test_predicates.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,88 @@ def test_st_within_skipped(eng, geom1, geom2, expected):
357357
f"SELECT ST_Within({geom_or_null(geom1)}, {geom_or_null(geom2)})",
358358
expected,
359359
)
360+
361+
362+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
363+
@pytest.mark.parametrize(
364+
("geom1", "geom2", "expected"),
365+
[
366+
(None, None, None),
367+
("POINT (0 0)", None, None),
368+
(None, "POINT (0 0)", None),
369+
("POINT (0 0)", "POINT EMPTY", False),
370+
("POINT (0 0)", "POINT (0 0)", False),
371+
("POINT (0.5 0.5)", "LINESTRING (0 0, 1 1)", False),
372+
("POINT (0 0)", "LINESTRING (0 0, 1 1)", False),
373+
("POINT (0.5 0.5)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", False),
374+
("POINT (0 0)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", False),
375+
("LINESTRING (0 0, 1 1)", "LINESTRING (0 1, 1 0)", True),
376+
("LINESTRING (0 0, 1 1)", "LINESTRING (1 1, 2 2)", False),
377+
("LINESTRING (0 0, 2 2)", "LINESTRING (1 1, 3 3)", False),
378+
("LINESTRING (-1 -1, 1 1)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", True),
379+
("LINESTRING (-1 0, 0 0)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", False),
380+
(
381+
"LINESTRING (0.1 0.1, 0.5 0.5)",
382+
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
383+
False,
384+
),
385+
(
386+
"POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
387+
"POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))",
388+
False,
389+
),
390+
],
391+
)
392+
def test_st_crosses(eng, geom1, geom2, expected):
393+
eng = eng.create_or_skip()
394+
eng.assert_query_result(
395+
f"SELECT ST_Crosses({geom_or_null(geom1)}, {geom_or_null(geom2)})",
396+
expected,
397+
)
398+
399+
400+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
401+
@pytest.mark.parametrize(
402+
("geom1", "geom2", "expected"),
403+
[
404+
(None, None, None),
405+
("POINT (0 0)", None, None),
406+
(None, "POINT (0 0)", None),
407+
("POINT (0 0)", "POINT EMPTY", False),
408+
("POINT (0 0)", "LINESTRING (0 0, 1 1)", False),
409+
("LINESTRING (0 0, 2 2)", "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))", False),
410+
("MULTIPOINT ((0 0), (1 1))", "MULTIPOINT ((1 1), (2 2))", True),
411+
("MULTIPOINT ((0 0), (1 1))", "MULTIPOINT ((0 0), (1 1))", False),
412+
("POINT (0 0)", "POINT (0 0)", False),
413+
("LINESTRING (0 0, 2 2)", "LINESTRING (1 1, 3 3)", True),
414+
("LINESTRING (0 0, 1 1)", "LINESTRING (0 1, 1 0)", False),
415+
("LINESTRING (0 0, 1 1)", "LINESTRING (1 1, 2 2)", False),
416+
("LINESTRING (0 0, 1 1)", "LINESTRING (0 0, 1 1)", False),
417+
(
418+
"POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
419+
"POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))",
420+
True,
421+
),
422+
(
423+
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
424+
"POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))",
425+
False,
426+
),
427+
(
428+
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
429+
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
430+
False,
431+
),
432+
(
433+
"POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0))",
434+
"POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))",
435+
False,
436+
),
437+
],
438+
)
439+
def test_st_overlaps(eng, geom1, geom2, expected):
440+
eng = eng.create_or_skip()
441+
eng.assert_query_result(
442+
f"SELECT ST_Overlaps({geom_or_null(geom1)}, {geom_or_null(geom2)})",
443+
expected,
444+
)

0 commit comments

Comments
 (0)