Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions c/sedona-geos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ pub mod register;
mod st_area;
mod st_boundary;
mod st_buffer;
mod st_buildarea;
mod st_centroid;
mod st_concavehull;
mod st_convexhull;
mod st_delaunaytriangles;
mod st_dwithin;
mod st_exteriorring;
mod st_isring;
mod st_issimple;
mod st_isvalid;
Expand All @@ -43,6 +46,7 @@ mod st_nrings;
mod st_numinteriorrings;
mod st_numpoints;
mod st_perimeter;
mod st_pointonsurface;
mod st_polygonize;
mod st_polygonize_agg;
mod st_relate;
Expand Down
6 changes: 6 additions & 0 deletions c/sedona-geos/src/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, Vec<ScalarKernelRef>)> {
define_scalar_kernels!(
"st_area" => crate::st_area::st_area_impl,
"st_boundary" => crate::st_boundary::st_boundary_impl,
"st_buildarea" => crate::st_buildarea::st_build_area_impl,
"st_buffer" => crate::st_buffer::st_buffer_impl,
"st_buffer" => crate::st_buffer::st_buffer_style_impl,
"st_centroid" => crate::st_centroid::st_centroid_impl,
Expand All @@ -51,11 +52,14 @@ pub fn scalar_kernels() -> Vec<(&'static str, Vec<ScalarKernelRef>)> {
"st_coveredby" => crate::binary_predicates::st_covered_by_impl,
"st_covers" => crate::binary_predicates::st_covers_impl,
"st_crosses" => crate::binary_predicates::st_crosses_impl,
"st_delaunaytriangles" => crate::st_delaunaytriangles::st_delaunay_triangles_impl,
"st_delaunaytriangles" => crate::st_delaunaytriangles::st_delaunay_triangles_tolerance_impl,
"st_difference" => crate::overlay::st_difference_impl,
"st_disjoint" => crate::binary_predicates::st_disjoint_impl,
"st_distance" => crate::distance::st_distance_impl,
"st_dwithin" => crate::st_dwithin::st_dwithin_impl,
"st_equals" => crate::binary_predicates::st_equals_impl,
"st_exteriorring" => crate::st_exteriorring::st_exterior_ring_impl,
"st_intersection" => crate::overlay::st_intersection_impl,
"st_intersects" => crate::binary_predicates::st_intersects_impl,
"st_isring" => crate::st_isring::st_is_ring_impl,
Expand All @@ -69,10 +73,12 @@ pub fn scalar_kernels() -> Vec<(&'static str, Vec<ScalarKernelRef>)> {
"st_minimumclearanceline" => crate::st_minimumclearance_line::st_minimum_clearance_line_impl,
"st_normalize" => crate::st_normalize::st_normalize_impl,
"st_nrings" => crate::st_nrings::st_nrings_impl,
"st_numinteriorring" => crate::st_numinteriorrings::st_num_interior_rings_impl,
"st_numinteriorrings" => crate::st_numinteriorrings::st_num_interior_rings_impl,
"st_numpoints" => crate::st_numpoints::st_num_points_impl,
"st_overlaps" => crate::binary_predicates::st_overlaps_impl,
"st_perimeter" => crate::st_perimeter::st_perimeter_impl,
"st_pointonsurface" => crate::st_pointonsurface::st_point_on_surface_impl,
"st_polygonize" => crate::st_polygonize::st_polygonize_impl,
"st_relate" => crate::st_relate::st_relate_impl,
"st_relate" => crate::st_relate::st_relate_pattern_impl,
Expand Down
134 changes: 134 additions & 0 deletions c/sedona-geos/src/st_buildarea.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

use std::sync::Arc;

use arrow_array::builder::BinaryBuilder;
use datafusion_common::{error::Result, DataFusionError};
use datafusion_expr::ColumnarValue;
use geos::Geometry;
use sedona_expr::{
item_crs::ItemCrsKernel,
scalar_udf::{ScalarKernelRef, SedonaScalarKernel},
};
use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
use sedona_schema::{
datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY},
matchers::ArgMatcher,
};

use crate::executor::GeosExecutor;
use crate::geos_to_wkb::write_geos_geometry;

/// ST_BuildArea() implementation using the geos crate
pub fn st_build_area_impl() -> Vec<ScalarKernelRef> {
ItemCrsKernel::wrap_impl(vec![
Arc::new(STBuildArea {
matcher: ArgMatcher::new(vec![ArgMatcher::is_geometry()], WKB_GEOMETRY),
}),
Arc::new(STBuildArea {
matcher: ArgMatcher::new(vec![ArgMatcher::is_geography()], WKB_GEOGRAPHY),
}),
Comment on lines +43 to +45

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Arc::new(STBuildArea {
matcher: ArgMatcher::new(vec![ArgMatcher::is_geography()], WKB_GEOGRAPHY),
}),

Sorry for missing this, but we can't support Geography with this one because the definition of "enclosing" isn't the same on the sphere.

])
}

#[derive(Debug)]
struct STBuildArea {
matcher: ArgMatcher,
}

impl SedonaScalarKernel for STBuildArea {
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
self.matcher.match_args(args)
}

fn invoke_batch(
&self,
arg_types: &[SedonaType],
args: &[ColumnarValue],
) -> Result<ColumnarValue> {
let executor = GeosExecutor::new(arg_types, args);
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_scalar(&geom, &mut builder)?;
builder.append_value([]);
}
_ => builder.append_null(),
}
Ok(())
})?;

executor.finish(Arc::new(builder.finish()))
}
}

fn invoke_scalar(geom: &Geometry, writer: &mut impl std::io::Write) -> Result<()> {
let result = geom
.build_area()
.map_err(|e| DataFusionError::Execution(format!("ST_BuildArea failed: {e}")))?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map_err(|e| DataFusionError::Execution(format!("ST_BuildArea failed: {e}")))?;
.map_err(|e| exec_datafusion_err!("ST_BuildArea failed: {e}"))?;

write_geos_geometry(&result, writer)?;
Ok(())
}

#[cfg(test)]
mod tests {
use datafusion_common::ScalarValue;
use rstest::rstest;
use sedona_expr::scalar_udf::SedonaScalarUDF;
use sedona_schema::datatypes::{
WKB_GEOGRAPHY, WKB_GEOGRAPHY_ITEM_CRS, WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS,
};
use sedona_testing::testers::ScalarUdfTester;

use super::*;

#[rstest]
fn udf(#[values(WKB_GEOMETRY, WKB_GEOGRAPHY)] sedona_type: SedonaType) {
let udf = SedonaScalarUDF::from_impl("st_buildarea", st_build_area_impl());
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]);

tester.assert_return_type(sedona_type.clone());

let result = tester
.invoke_scalar("LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)")
.unwrap();
tester.assert_scalar_result_equals(result, "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))");

let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
assert!(result.is_null());
}

#[rstest]
fn udf_invoke_item_crs(
#[values(WKB_GEOMETRY_ITEM_CRS.clone(), WKB_GEOGRAPHY_ITEM_CRS.clone())]
sedona_type: SedonaType,
) {
let udf = SedonaScalarUDF::from_impl("st_buildarea", st_build_area_impl());
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]);
tester.assert_return_type(sedona_type);

let result = tester
.invoke_scalar("LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)")
.unwrap();
tester.assert_scalar_result_equals(result, "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))");
}
}
152 changes: 152 additions & 0 deletions c/sedona-geos/src/st_delaunaytriangles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

use std::sync::Arc;

use arrow_array::builder::BinaryBuilder;
use arrow_schema::DataType;
use datafusion_common::{cast::as_float64_array, DataFusionError, Result};
use datafusion_expr::ColumnarValue;
use geos::{Geom, Geometry};
use sedona_expr::{
item_crs::ItemCrsKernel,
scalar_udf::{ScalarKernelRef, SedonaScalarKernel},
};
use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
use sedona_schema::{
datatypes::{SedonaType, WKB_GEOMETRY},
matchers::ArgMatcher,
};

use crate::executor::GeosExecutor;
use crate::geos_to_wkb::write_geos_geometry;

/// ST_DelaunayTriangles(geom) — no tolerance, returns polygons
pub fn st_delaunay_triangles_impl() -> Vec<ScalarKernelRef> {
ItemCrsKernel::wrap_impl(STDelaunayTriangles { tolerance: false })
}

/// ST_DelaunayTriangles(geom, tolerance) — with tolerance
pub fn st_delaunay_triangles_tolerance_impl() -> Vec<ScalarKernelRef> {
ItemCrsKernel::wrap_impl(STDelaunayTriangles { tolerance: true })
}

#[derive(Debug)]
struct STDelaunayTriangles {
tolerance: bool,
}

impl SedonaScalarKernel for STDelaunayTriangles {
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
let arg_matchers = if self.tolerance {
vec![ArgMatcher::is_geometry(), ArgMatcher::is_numeric()]
} else {
vec![ArgMatcher::is_geometry()]
};
let matcher = ArgMatcher::new(arg_matchers, WKB_GEOMETRY);
matcher.match_args(args)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you implement this using two separate SedonaScalarKernel implementations rather than crunching them both into the same one? You can split out the logic into a common invoke_scalar().

While you're here, is it easy to add support for the "flags" parameter? We can't support the 2 (TIN) option but I believe GEOS has the MULTILINESTRING output option.

}

fn invoke_batch(
&self,
arg_types: &[SedonaType],
args: &[ColumnarValue],
) -> Result<ColumnarValue> {
let executor = GeosExecutor::new(arg_types, args);
let mut builder = BinaryBuilder::with_capacity(
executor.num_iterations(),
WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
);

if self.tolerance {
let tolerance_value = args[1]
.cast_to(&DataType::Float64, None)?
.to_array(executor.num_iterations())?;
let tolerance_array = as_float64_array(&tolerance_value)?;
let mut tolerance_iter = tolerance_array.iter();

executor.execute_wkb_void(|maybe_geom| {
match (maybe_geom, tolerance_iter.next().unwrap()) {
(Some(geom), Some(tol)) => {
invoke_scalar(&geom, tol, &mut builder)?;
builder.append_value([]);
}
_ => builder.append_null(),
}
Ok(())
})?;
} else {
executor.execute_wkb_void(|maybe_geom| {
match maybe_geom {
Some(geom) => {
invoke_scalar(&geom, 0.0, &mut builder)?;
builder.append_value([]);
}
_ => builder.append_null(),
}
Ok(())
})?;
}

executor.finish(Arc::new(builder.finish()))
}
}

fn invoke_scalar(geom: &Geometry, tolerance: f64, writer: &mut impl std::io::Write) -> Result<()> {
let result = geom
.delaunay_triangulation(tolerance, false)
.map_err(|e| DataFusionError::Execution(format!("ST_DelaunayTriangles failed: {e}")))?;
write_geos_geometry(&result, writer)?;
Ok(())
}

#[cfg(test)]
mod tests {
use datafusion_common::ScalarValue;
use rstest::rstest;
use sedona_expr::scalar_udf::SedonaScalarUDF;
use sedona_schema::datatypes::{WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS};
use sedona_testing::testers::ScalarUdfTester;

use super::*;

#[rstest]
fn udf_no_tolerance(#[values(WKB_GEOMETRY)] sedona_type: SedonaType) {
let udf = SedonaScalarUDF::from_impl("st_delaunaytriangles", st_delaunay_triangles_impl());
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]);

Comment thread
paleolimbot marked this conversation as resolved.
tester.assert_return_type(WKB_GEOMETRY);

let result = tester
.invoke_scalar("MULTIPOINT ((0 0), (1 1), (0 1))")
.unwrap();
tester.assert_scalar_result_equals(
result,
"GEOMETRYCOLLECTION (POLYGON ((0 1, 0 0, 1 1, 0 1)))",
);

let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
assert!(result.is_null());
}

#[rstest]
fn udf_invoke_item_crs(#[values(WKB_GEOMETRY_ITEM_CRS.clone())] sedona_type: SedonaType) {
let udf = SedonaScalarUDF::from_impl("st_delaunaytriangles", st_delaunay_triangles_impl());
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]);
tester.assert_return_type(sedona_type);
}
}
Loading
Loading