Skip to content

Commit 6853702

Browse files
authored
feat: ST_NPoints (#9)
* feat: ST_NPoints * Expose to Python * Add NumPoints alias
1 parent a65bc76 commit 6853702

7 files changed

Lines changed: 212 additions & 4 deletions

File tree

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ Spatial extensions for [Apache DataFusion](https://datafusion.apache.org/), an e
4545
| ST_ExteriorRing | | Returns a LineString representing the exterior ring of a Polygon. |
4646
| ST_GeometryN | | Return an element of a geometry collection. |
4747
| ST_GeometryType | | Returns the SQL-MM type of a geometry as text. |
48-
| ST_HasArc | | Tests if a geometry contains a circular arc |
4948
| ST_InteriorRingN | | Returns the Nth interior ring (hole) of a Polygon. |
5049
| ST_IsClosed | | Tests if a LineStrings's start and end points are coincident. For a PolyhedralSurface tests if it is closed (volumetric). |
5150
| ST_IsCollection | | Tests if a geometry is a geometry collection type. |
@@ -57,13 +56,13 @@ Spatial extensions for [Apache DataFusion](https://datafusion.apache.org/), an e
5756
| ST_M || Returns the M coordinate of a Point. |
5857
| ST_MemSize | | Returns the amount of memory space a geometry takes. |
5958
| ST_NDims || Returns the coordinate dimension of a geometry. |
60-
| ST_NPoints | | Returns the number of points (vertices) in a geometry. |
59+
| ST_NPoints | | Returns the number of points (vertices) in a geometry. |
6160
| ST_NRings | | Returns the number of rings in a polygonal geometry. |
6261
| ST_NumGeometries | | Returns the number of elements in a geometry collection. |
6362
| ST_NumInteriorRings | | Returns the number of interior rings (holes) of a Polygon. |
6463
| ST_NumInteriorRing | | Returns the number of interior rings (holes) of a Polygon. Aias for ST_NumInteriorRings |
6564
| ST_NumPatches | | Return the number of faces on a Polyhedral Surface. Will return null for non-polyhedral geometries. |
66-
| ST_NumPoints | | Returns the number of points in a LineString or CircularString. |
65+
| ST_NumPoints | | Returns the number of points in a LineString or CircularString. |
6766
| ST_PatchN | | Returns the Nth geometry (face) of a PolyhedralSurface. |
6867
| ST_PointN | | Returns the Nth point in the first LineString or circular LineString in a geometry. |
6968
| ST_Points | | Returns a MultiPoint containing the coordinates of a geometry. |

python/python/geodatafusion/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def register_all_native(ctx: SessionContext):
6262
ctx.register_udf(udf(native.CoordDim()))
6363
ctx.register_udf(udf(native.EndPoint()))
6464
ctx.register_udf(udf(native.NDims()))
65+
ctx.register_udf(udf(native.NPoints()))
6566
ctx.register_udf(udf(native.StartPoint()))
6667
ctx.register_udf(udf(native.X()))
6768
ctx.register_udf(udf(native.Y()))

python/python/geodatafusion/native/_accessors.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class NDims:
1010
def __init__(self) -> None: ...
1111
def __datafusion_scalar_udf__(self) -> object: ...
1212

13+
class NPoints:
14+
def __init__(self) -> None: ...
15+
def __datafusion_scalar_udf__(self) -> object: ...
16+
1317
class StartPoint:
1418
def __init__(self) -> None: ...
1519
def __datafusion_scalar_udf__(self) -> object: ...

python/src/udf/native/accessors.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use geodatafusion::udf::native::accessors::{CoordDim, EndPoint, M, NDims, StartPoint, X, Y, Z};
1+
use geodatafusion::udf::native::accessors::{
2+
CoordDim, EndPoint, M, NDims, NPoints, StartPoint, X, Y, Z,
3+
};
24

35
use crate::{impl_udf, impl_udf_coord_type_arg};
46

@@ -10,3 +12,4 @@ impl_udf!(Z, PyZ, "Z");
1012
impl_udf!(M, PyM, "M");
1113
impl_udf_coord_type_arg!(EndPoint, PyEndPoint, "EndPoint");
1214
impl_udf_coord_type_arg!(StartPoint, PyStartPoint, "StartPoint");
15+
impl_udf!(NPoints, PyNPoints, "NPoints");

python/src/udf/native/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub(crate) fn native(m: &Bound<PyModule>) -> PyResult<()> {
1212
m.add_class::<accessors::PyEndPoint>()?;
1313
m.add_class::<accessors::PyM>()?;
1414
m.add_class::<accessors::PyNDims>()?;
15+
m.add_class::<accessors::PyNPoints>()?;
1516
m.add_class::<accessors::PyStartPoint>()?;
1617
m.add_class::<accessors::PyX>()?;
1718
m.add_class::<accessors::PyY>()?;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
mod coord_dim;
22
mod line_string;
3+
mod npoints;
34
mod point;
45

56
pub use coord_dim::{CoordDim, NDims};
67
pub use line_string::{EndPoint, StartPoint};
8+
pub use npoints::NPoints;
79
pub use point::{M, X, Y, Z};
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use std::any::Any;
2+
use std::sync::{Arc, OnceLock};
3+
4+
use arrow_array::builder::UInt32Builder;
5+
use arrow_array::{ArrayRef, UInt32Array};
6+
use arrow_schema::DataType;
7+
use datafusion::error::Result;
8+
use datafusion::logical_expr::scalar_doc_sections::DOC_SECTION_OTHER;
9+
use datafusion::logical_expr::{
10+
ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
11+
};
12+
use geo_traits::*;
13+
use geoarrow_array::array::from_arrow_array;
14+
use geoarrow_array::{GeoArrowArray, GeoArrowArrayAccessor, downcast_geoarrow_array};
15+
use geoarrow_schema::error::GeoArrowResult;
16+
17+
use crate::data_types::any_single_geometry_type_input;
18+
use crate::error::GeoDataFusionResult;
19+
20+
#[derive(Debug)]
21+
pub struct NPoints {
22+
signature: Signature,
23+
aliases: Vec<String>,
24+
}
25+
26+
impl NPoints {
27+
pub fn new() -> Self {
28+
Self {
29+
signature: any_single_geometry_type_input(),
30+
aliases: vec!["st_numpoints".to_string()],
31+
}
32+
}
33+
}
34+
35+
impl Default for NPoints {
36+
fn default() -> Self {
37+
Self::new()
38+
}
39+
}
40+
41+
static DOCUMENTATION: OnceLock<Documentation> = OnceLock::new();
42+
43+
impl ScalarUDFImpl for NPoints {
44+
fn as_any(&self) -> &dyn Any {
45+
self
46+
}
47+
48+
fn name(&self) -> &str {
49+
"st_npoints"
50+
}
51+
52+
fn signature(&self) -> &Signature {
53+
&self.signature
54+
}
55+
56+
fn aliases(&self) -> &[String] {
57+
&self.aliases
58+
}
59+
60+
fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
61+
Ok(DataType::UInt32)
62+
}
63+
64+
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
65+
Ok(coord_dim_impl(args)?)
66+
}
67+
68+
fn documentation(&self) -> Option<&Documentation> {
69+
Some(DOCUMENTATION.get_or_init(|| {
70+
Documentation::builder(
71+
DOC_SECTION_OTHER,
72+
"Return the number of points in a geometry. Works for all geometries.",
73+
"ST_NPoints(geometry)",
74+
)
75+
.with_argument("g1", "geometry")
76+
.build()
77+
}))
78+
}
79+
}
80+
81+
fn coord_dim_impl(args: ScalarFunctionArgs) -> GeoDataFusionResult<ColumnarValue> {
82+
let arrays = ColumnarValue::values_to_arrays(&args.args)?;
83+
let geo_array = from_arrow_array(&arrays[0], &args.arg_fields[0])?;
84+
let result = Arc::new(num_points(&geo_array)?) as ArrayRef;
85+
Ok(ColumnarValue::Array(result))
86+
}
87+
88+
fn num_points(array: &dyn GeoArrowArray) -> GeoArrowResult<UInt32Array> {
89+
downcast_geoarrow_array!(array, _num_points_impl)
90+
}
91+
92+
fn _num_points_impl<'a>(array: &'a impl GeoArrowArrayAccessor<'a>) -> GeoArrowResult<UInt32Array> {
93+
let mut builder = UInt32Builder::with_capacity(array.len());
94+
95+
for item in array.iter() {
96+
if let Some(geom) = item {
97+
builder.append_value(num_coords_geometry(&geom?));
98+
} else {
99+
builder.append_null();
100+
}
101+
}
102+
103+
Ok(builder.finish())
104+
}
105+
106+
#[inline]
107+
fn num_coords_geometry(geom: &impl GeometryTrait) -> u32 {
108+
use geo_traits::GeometryType::*;
109+
110+
match geom.as_type() {
111+
Point(geom) => num_coords_point(geom),
112+
LineString(geom) => num_coords_line_string(geom),
113+
Polygon(geom) => num_coords_polygon(geom),
114+
MultiPoint(geom) => num_coords_multi_point(geom),
115+
MultiLineString(geom) => num_coords_multi_line_string(geom),
116+
MultiPolygon(geom) => num_coords_multi_polygon(geom),
117+
GeometryCollection(geom) => num_coords_geometry_collection(geom),
118+
// Check what postgis says for a Rect
119+
Rect(_) => 4,
120+
Line(_) => 2,
121+
Triangle(_) => 3,
122+
}
123+
}
124+
125+
#[inline]
126+
fn num_coords_point(geom: &impl PointTrait) -> u32 {
127+
if geom.coord().is_some() { 1 } else { 0 }
128+
}
129+
130+
#[inline]
131+
fn num_coords_line_string(geom: &impl LineStringTrait) -> u32 {
132+
geom.num_coords() as u32
133+
}
134+
135+
#[inline]
136+
fn num_coords_polygon(geom: &impl PolygonTrait) -> u32 {
137+
let exterior_coords = geom
138+
.exterior()
139+
.map(|ext| num_coords_line_string(&ext))
140+
.unwrap_or(0);
141+
geom.interiors().fold(exterior_coords, |acc, interior| {
142+
acc + num_coords_line_string(&interior)
143+
})
144+
}
145+
146+
#[inline]
147+
fn num_coords_multi_point(geom: &impl MultiPointTrait) -> u32 {
148+
geom.points()
149+
.fold(0, |acc, point| acc + num_coords_point(&point))
150+
}
151+
152+
#[inline]
153+
fn num_coords_multi_line_string(geom: &impl MultiLineStringTrait) -> u32 {
154+
geom.line_strings().fold(0, |acc, line_string| {
155+
acc + num_coords_line_string(&line_string)
156+
})
157+
}
158+
159+
#[inline]
160+
fn num_coords_multi_polygon(geom: &impl MultiPolygonTrait) -> u32 {
161+
geom.polygons()
162+
.fold(0, |acc, polygon| acc + num_coords_polygon(&polygon))
163+
}
164+
165+
#[inline]
166+
fn num_coords_geometry_collection(geom: &impl GeometryCollectionTrait) -> u32 {
167+
geom.geometries()
168+
.fold(0, |acc, g| acc + num_coords_geometry(&g))
169+
}
170+
171+
#[cfg(test)]
172+
mod test {
173+
use arrow_array::cast::AsArray;
174+
use arrow_array::types::UInt32Type;
175+
use datafusion::prelude::SessionContext;
176+
177+
use super::*;
178+
use crate::udf::native::io::GeomFromText;
179+
180+
#[tokio::test]
181+
async fn test() {
182+
let ctx = SessionContext::new();
183+
184+
ctx.register_udf(NPoints::new().into());
185+
ctx.register_udf(GeomFromText::new(Default::default()).into());
186+
187+
let df = ctx
188+
.sql(
189+
"select ST_NPoints(ST_GeomFromText('LINESTRING(77.29 29.07,77.42 29.26,77.27 29.31,77.29 29.07)'));",
190+
)
191+
.await
192+
.unwrap();
193+
let batch = df.collect().await.unwrap().into_iter().next().unwrap();
194+
let col = batch.column(0);
195+
let val = col.as_primitive::<UInt32Type>().value(0);
196+
assert_eq!(val, 4);
197+
}
198+
}

0 commit comments

Comments
 (0)