Skip to main content

vortex_geo/extension/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4pub(crate) mod coordinate;
5mod linestring;
6mod multilinestring;
7mod multipoint;
8mod multipolygon;
9mod point;
10mod polygon;
11mod rect;
12mod wkb;
13
14use std::fmt::Display;
15use std::sync::Arc;
16
17use ::wkb::reader::GeometryType;
18use arrow_array::BinaryArray;
19use geo_types::Geometry;
20use geoarrow::array::GenericWkbArray;
21use geoarrow::array::GeoArrowArray;
22use geoarrow::datatypes::CoordType;
23use geoarrow::datatypes::Crs;
24use geoarrow::datatypes::Dimension;
25use geoarrow::datatypes::GeoArrowType;
26use geoarrow::datatypes::LineStringType;
27use geoarrow::datatypes::Metadata;
28use geoarrow::datatypes::MultiLineStringType;
29use geoarrow::datatypes::MultiPointType;
30use geoarrow::datatypes::MultiPolygonType;
31use geoarrow::datatypes::PointType;
32use geoarrow::datatypes::PolygonType;
33use geoarrow::datatypes::WkbType;
34use geoarrow_cast::cast::cast;
35pub use linestring::*;
36pub use multilinestring::*;
37pub use multipoint::*;
38pub use multipolygon::*;
39pub use point::*;
40pub use polygon::*;
41pub use rect::*;
42use vortex_array::ArrayRef;
43use vortex_array::ExecutionCtx;
44use vortex_array::IntoArray;
45use vortex_array::arrays::ConstantArray;
46use vortex_array::arrays::ExtensionArray;
47use vortex_array::arrays::ListViewArray;
48use vortex_array::arrays::StructArray;
49use vortex_array::arrays::extension::ExtensionArrayExt;
50use vortex_array::arrays::list::ListArraySlotsExt;
51use vortex_array::arrays::listview::ListViewArraySlotsExt;
52use vortex_array::arrays::listview::list_from_list_view;
53use vortex_array::builtins::ArrayBuiltins;
54use vortex_array::dtype::DType;
55use vortex_array::dtype::Nullability;
56use vortex_array::dtype::PType;
57use vortex_array::dtype::extension::ExtDType;
58use vortex_array::dtype::extension::ExtVTable;
59use vortex_array::scalar::Scalar;
60use vortex_arrow::FromArrowArray;
61use vortex_buffer::Buffer;
62use vortex_error::VortexResult;
63use vortex_error::vortex_bail;
64use vortex_error::vortex_err;
65pub use wkb::*;
66
67/// Whether `dtype` is one of the native geometry extension types the geo kernels operate on.
68pub(crate) fn is_native_geometry(dtype: &DType) -> bool {
69    dtype.as_extension_opt().is_some_and(|ext| {
70        ext.is::<Point>()
71            || ext.is::<LineString>()
72            || ext.is::<MultiPoint>()
73            || ext.is::<Polygon>()
74            || ext.is::<MultiLineString>()
75            || ext.is::<MultiPolygon>()
76            || ext.is::<Rect>()
77    })
78}
79
80/// Flatten a native geometry column into a single coordinate `Struct<x, y, ...>` containing
81/// every vertex of every geometry.
82pub(crate) fn flatten_coordinates(
83    array: &ArrayRef,
84    ctx: &mut ExecutionCtx,
85) -> VortexResult<StructArray> {
86    if !is_native_geometry(array.dtype()) {
87        vortex_bail!(
88            "geo: operand is not a native geometry extension type, was {}",
89            array.dtype()
90        );
91    }
92    let mut node = array
93        .clone()
94        .execute::<ExtensionArray>(ctx)?
95        .storage_array()
96        .clone();
97    while node.dtype().is_list() {
98        node = node.execute::<ListViewArray>(ctx)?.elements().clone();
99    }
100    node.execute::<StructArray>(ctx)
101}
102
103/// Flatten a native geometry `storage` array to its leaf coordinates, keeping track of which
104/// row owns which coordinates.
105///
106/// Returns the flat coordinate `Struct` plus `row_offsets` (one entry per row plus a final
107/// cap): row `r`'s coordinates are `coordinates[row_offsets[r]..row_offsets[r + 1]]`, an empty
108/// range if the row has none.
109///
110/// Row boundaries are pushed down one `List` level at a time. A boundary at list `e` moves to
111/// `offsets[e]`, where that list's children start. For example, a 2-row `MultiPolygon` column —
112/// row 0 = one polygon of two rings (3 + 4 vertices), row 1 = one polygon of one ring (5
113/// vertices):
114///
115/// ```text
116/// level                offsets       row_offsets after the level
117/// rows     → polygons  [0,1,2]       [0,1,2]    row 1 starts at polygon 1
118/// polygons → rings     [0,2,3]       [0,2,3]    row 1 starts at ring 2
119/// rings    → vertices  [0,3,7,12]    [0,7,12]   row 1 starts at vertex 7
120/// ```
121pub(crate) fn flatten_row_offsets(
122    storage: ArrayRef,
123    ctx: &mut ExecutionCtx,
124) -> VortexResult<(Vec<usize>, StructArray)> {
125    let len = storage.len();
126
127    // At the outermost level, row `r` starts at element `r`; the extra entry caps the last row.
128    let mut row_offsets: Vec<usize> = (0..=len).collect();
129    let mut level = storage;
130    while level.dtype().is_list() {
131        let list = list_from_list_view(level.execute::<ListViewArray>(ctx)?, ctx)?;
132        let offsets = list
133            .offsets()
134            .clone()
135            .cast(DType::Primitive(PType::U64, Nullability::NonNullable))?
136            .execute::<Buffer<u64>>(ctx)?;
137        for row_offset in &mut row_offsets {
138            *row_offset = usize::try_from(offsets[*row_offset])
139                .map_err(|_| vortex_err!("geo: list offset exceeds usize"))?;
140        }
141        level = list.elements().clone();
142    }
143    Ok((row_offsets, level.execute::<StructArray>(ctx)?))
144}
145
146/// Decode a native geometry column to `geo_types`. A non-geometry operand is an error.
147pub(crate) fn geometries(
148    array: &ArrayRef,
149    ctx: &mut ExecutionCtx,
150) -> VortexResult<Vec<Geometry<f64>>> {
151    let Some(ext) = array.dtype().as_extension_opt() else {
152        vortex_bail!(
153            "geo: operand is not a geometry extension type, was {}",
154            array.dtype()
155        );
156    };
157    let storage = array
158        .clone()
159        .execute::<ExtensionArray>(ctx)?
160        .storage_array()
161        .clone();
162    if ext.is::<Point>() {
163        point_geometries(&storage, ctx)
164    } else if ext.is::<LineString>() {
165        linestring_geometries(&storage, ctx)
166    } else if ext.is::<MultiPoint>() {
167        multipoint_geometries(&storage, ctx)
168    } else if ext.is::<Polygon>() {
169        polygon_geometries(&storage, ctx)
170    } else if ext.is::<MultiLineString>() {
171        multilinestring_geometries(&storage, ctx)
172    } else if ext.is::<MultiPolygon>() {
173        multipolygon_geometries(&storage, ctx)
174    } else if ext.is::<Rect>() {
175        rect_geometries(&storage, ctx)
176    } else {
177        vortex_bail!("geo: unsupported geometry extension {}", array.dtype())
178    }
179}
180
181/// Decode a constant operand scalar to one geo geometry, a constant of any
182/// supported geometry type is decoded exactly like a column.
183pub(crate) fn single_geometry(
184    scalar: &Scalar,
185    ctx: &mut ExecutionCtx,
186) -> VortexResult<Geometry<f64>> {
187    let array = ConstantArray::new(scalar.clone(), 1).into_array();
188    geometries(&array, ctx)?
189        .pop()
190        .ok_or_else(|| vortex_err!("geo: constant operand decoded to no geometry"))
191}
192
193/// Decode a WKB geometry literal (DuckDB's wire form for `GEOMETRY` constants) to its native
194/// `Point`/`Polygon`/`MultiPolygon` scalar. `None` for unsupported types. Plan-time, one value only.
195pub fn native_geometry_scalar_from_wkb(bytes: &[u8]) -> VortexResult<Option<Scalar>> {
196    let metadata = geoarrow_metadata(&GeoMetadata::default());
197    let binary = BinaryArray::from(vec![Some(bytes)]);
198    let wkb = GenericWkbArray::<i32>::try_from((
199        &binary as &dyn arrow_array::Array,
200        WkbType::new(Arc::clone(&metadata)),
201    ))
202    .map_err(|e| vortex_err!("failed to read WKB literal: {e}"))?;
203
204    // Cast the WKB value to `target`, import its native storage as a Vortex array.
205    let to_storage = |target: &GeoArrowType| -> VortexResult<ArrayRef> {
206        let native =
207            cast(&wkb, target).map_err(|e| vortex_err!("failed to cast WKB literal: {e}"))?;
208        ArrayRef::from_arrow(native.to_array_ref().as_ref(), false)
209    };
210
211    let scalar = match Wkb::try_from_bytes(bytes)?.geometry_type() {
212        GeometryType::Point => {
213            let target = GeoArrowType::Point(
214                PointType::new(Dimension::XY, metadata).with_coord_type(CoordType::Separated),
215            );
216            geo_ext_scalar(Point, to_storage(&target)?)?
217        }
218        GeometryType::LineString => {
219            let target = GeoArrowType::LineString(
220                LineStringType::new(Dimension::XY, metadata).with_coord_type(CoordType::Separated),
221            );
222            geo_ext_scalar(LineString, to_storage(&target)?)?
223        }
224        GeometryType::Polygon => {
225            let target = GeoArrowType::Polygon(
226                PolygonType::new(Dimension::XY, metadata).with_coord_type(CoordType::Separated),
227            );
228            geo_ext_scalar(Polygon, to_storage(&target)?)?
229        }
230        GeometryType::MultiPoint => {
231            let target = GeoArrowType::MultiPoint(
232                MultiPointType::new(Dimension::XY, metadata).with_coord_type(CoordType::Separated),
233            );
234            geo_ext_scalar(MultiPoint, to_storage(&target)?)?
235        }
236        GeometryType::MultiLineString => {
237            let target = GeoArrowType::MultiLineString(
238                MultiLineStringType::new(Dimension::XY, metadata)
239                    .with_coord_type(CoordType::Separated),
240            );
241            geo_ext_scalar(MultiLineString, to_storage(&target)?)?
242        }
243        GeometryType::MultiPolygon => {
244            let target = GeoArrowType::MultiPolygon(
245                MultiPolygonType::new(Dimension::XY, metadata)
246                    .with_coord_type(CoordType::Separated),
247            );
248            geo_ext_scalar(MultiPolygon, to_storage(&target)?)?
249        }
250        _ => return Ok(None),
251    };
252    Ok(Some(scalar))
253}
254
255/// Wrap cast-from-WKB `storage` in its `vtable` extension type and pull out the single scalar.
256// `scalar_at` is deprecated for `execute_scalar`, but there is no execution context at plan time.
257#[allow(deprecated)]
258fn geo_ext_scalar<V: ExtVTable<Metadata = GeoMetadata>>(
259    vtable: V,
260    storage: ArrayRef,
261) -> VortexResult<Scalar> {
262    let ext = ExtDType::try_with_vtable(vtable, GeoMetadata::default(), storage.dtype().clone())?
263        .erased();
264    ExtensionArray::try_new(ext, storage)?
265        .into_array()
266        .scalar_at(0)
267}
268
269/// Extension metadata that is common to all the geospatial extension types.
270///
271/// Currently, this is just the coordinate reference system (CRS).
272/// We may wish to add a second field for edges interpretation in the future similar to
273/// the GeoArrow standard.
274#[derive(Clone, PartialEq, Eq, Hash, prost::Message)]
275pub struct GeoMetadata {
276    #[prost(optional, string, tag = "1")]
277    pub crs: Option<String>,
278}
279
280impl Display for GeoMetadata {
281    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282        match self.crs.as_ref() {
283            Some(crs) => write!(f, "Geometry(crs={crs})"),
284            None => write!(f, "Geometry(unreferenced)"),
285        }
286    }
287}
288
289/// The GeoArrow [`Metadata`] equivalent of `geo_metadata`.
290pub(crate) fn geoarrow_metadata(geo_metadata: &GeoMetadata) -> Arc<Metadata> {
291    Arc::new(Metadata::new(
292        geo_metadata
293            .crs
294            .as_ref()
295            .map(|crs| Crs::from_unknown_crs_type(crs.to_string()))
296            .unwrap_or_default(),
297        None,
298    ))
299}
300
301/// Serialize a native geometry array to WKB (a `WkbView` array) via geoarrow's cast.
302/// Shared by the `to_wkb` methods on the geometry extension types.
303pub(crate) fn geoarrow_to_wkb(geo_array: &dyn GeoArrowArray) -> VortexResult<ArrayRef> {
304    let wkb_type = GeoArrowType::WkbView(WkbType::new(geoarrow_metadata(&GeoMetadata::default())));
305    let wkb = cast(geo_array, &wkb_type)
306        .map_err(|e| vortex_err!("failed to cast geometry to WKB: {e}"))?;
307    ArrayRef::from_arrow(wkb.to_array_ref().as_ref(), false)
308}
309
310/// Recover [`GeoMetadata`] from GeoArrow metadata.
311pub(crate) fn geo_metadata_from_arrow(metadata: &Metadata) -> GeoMetadata {
312    let crs = metadata.crs().crs_value().map(|value| {
313        // `Crs::from_unknown_crs_type` stores the user's string verbatim as a JSON string
314        // value, so prefer the raw string when available to round-trip cleanly. For other
315        // CRS encodings (PROJJSON object, etc.), fall back to the JSON-encoded form.
316        value
317            .as_str()
318            .map(str::to_string)
319            .unwrap_or_else(|| value.to_string())
320    });
321    GeoMetadata { crs }
322}
323
324#[cfg(test)]
325mod tests {
326    use prost::Message;
327    use vortex_array::dtype::DType;
328    use vortex_error::VortexResult;
329    use vortex_error::vortex_err;
330
331    use super::LineString;
332    use super::MultiLineString;
333    use super::MultiPoint;
334    use super::Point;
335    use super::Polygon;
336    use super::native_geometry_scalar_from_wkb;
337    use crate::extension::GeoMetadata;
338
339    #[test]
340    fn test_metadata() {
341        let meta = GeoMetadata {
342            crs: Some("EPSG:4326".to_string()),
343        };
344
345        assert_eq!(meta.to_string(), "Geometry(crs=EPSG:4326)");
346        // round trip
347        let bytes = meta.encode_to_vec();
348        let decoded = GeoMetadata::decode(bytes.as_slice()).unwrap();
349        assert_eq!(decoded, meta);
350    }
351
352    /// A little-endian WKB `POINT` literal decodes to the native `Point` extension scalar.
353    #[test]
354    fn decodes_wkb_point_to_native() -> VortexResult<()> {
355        let mut wkb = vec![1u8]; // little-endian byte order
356        wkb.extend_from_slice(&1u32.to_le_bytes()); // geometry type: point
357        wkb.extend_from_slice(&1.0f64.to_le_bytes()); // x
358        wkb.extend_from_slice(&2.0f64.to_le_bytes()); // y
359
360        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a point scalar");
361        let DType::Extension(ext) = scalar.dtype() else {
362            panic!("expected an extension dtype, got {}", scalar.dtype());
363        };
364        assert!(ext.is::<Point>());
365        Ok(())
366    }
367
368    /// A little-endian WKB `POLYGON` literal decodes to the native `Polygon` extension scalar.
369    #[test]
370    fn decodes_wkb_polygon_to_native() -> VortexResult<()> {
371        let ring = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0)];
372        let mut wkb = vec![1u8]; // little-endian byte order
373        wkb.extend_from_slice(&3u32.to_le_bytes()); // geometry type: polygon
374        wkb.extend_from_slice(&1u32.to_le_bytes()); // one ring
375        let ring_len = u32::try_from(ring.len()).map_err(|e| vortex_err!("{e}"))?;
376        wkb.extend_from_slice(&ring_len.to_le_bytes());
377        for (x, y) in ring {
378            wkb.extend_from_slice(&f64::to_le_bytes(x));
379            wkb.extend_from_slice(&f64::to_le_bytes(y));
380        }
381
382        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a polygon scalar");
383        let DType::Extension(ext) = scalar.dtype() else {
384            panic!("expected an extension dtype, got {}", scalar.dtype());
385        };
386        assert!(ext.is::<Polygon>());
387        Ok(())
388    }
389
390    /// A little-endian WKB `LINESTRING` literal decodes to the native `LineString` extension scalar.
391    #[test]
392    fn decodes_wkb_linestring_to_native() -> VortexResult<()> {
393        let points = [(0.0, 0.0), (1.0, 1.0)];
394        let mut wkb = vec![1u8]; // little-endian byte order
395        wkb.extend_from_slice(&2u32.to_le_bytes()); // geometry type: linestring
396        let len = u32::try_from(points.len()).map_err(|e| vortex_err!("{e}"))?;
397        wkb.extend_from_slice(&len.to_le_bytes());
398        for (x, y) in points {
399            wkb.extend_from_slice(&f64::to_le_bytes(x));
400            wkb.extend_from_slice(&f64::to_le_bytes(y));
401        }
402
403        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a linestring scalar");
404        let DType::Extension(ext) = scalar.dtype() else {
405            panic!("expected an extension dtype, got {}", scalar.dtype());
406        };
407        assert!(ext.is::<LineString>());
408        Ok(())
409    }
410
411    /// A little-endian WKB `MULTIPOINT` literal decodes to the native `MultiPoint` extension scalar.
412    #[test]
413    fn decodes_wkb_multipoint_to_native() -> VortexResult<()> {
414        let points = [(0.0, 0.0), (1.0, 1.0)];
415        let mut wkb = vec![1u8]; // little-endian byte order
416        wkb.extend_from_slice(&4u32.to_le_bytes()); // geometry type: multipoint
417        let len = u32::try_from(points.len()).map_err(|e| vortex_err!("{e}"))?;
418        wkb.extend_from_slice(&len.to_le_bytes());
419        for (x, y) in points {
420            // each member is a full WKB point
421            wkb.push(1u8);
422            wkb.extend_from_slice(&1u32.to_le_bytes());
423            wkb.extend_from_slice(&f64::to_le_bytes(x));
424            wkb.extend_from_slice(&f64::to_le_bytes(y));
425        }
426
427        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a multipoint scalar");
428        let DType::Extension(ext) = scalar.dtype() else {
429            panic!("expected an extension dtype, got {}", scalar.dtype());
430        };
431        assert!(ext.is::<MultiPoint>());
432        Ok(())
433    }
434
435    /// A little-endian WKB `MULTILINESTRING` literal decodes to the native `MultiLineString` scalar.
436    #[test]
437    fn decodes_wkb_multilinestring_to_native() -> VortexResult<()> {
438        let lines = [[(0.0, 0.0), (1.0, 1.0)], [(2.0, 2.0), (3.0, 3.0)]];
439        let mut wkb = vec![1u8]; // little-endian byte order
440        wkb.extend_from_slice(&5u32.to_le_bytes()); // geometry type: multilinestring
441        let num_lines = u32::try_from(lines.len()).map_err(|e| vortex_err!("{e}"))?;
442        wkb.extend_from_slice(&num_lines.to_le_bytes());
443        for line in lines {
444            // each member is a full WKB linestring
445            wkb.push(1u8);
446            wkb.extend_from_slice(&2u32.to_le_bytes());
447            let len = u32::try_from(line.len()).map_err(|e| vortex_err!("{e}"))?;
448            wkb.extend_from_slice(&len.to_le_bytes());
449            for (x, y) in line {
450                wkb.extend_from_slice(&f64::to_le_bytes(x));
451                wkb.extend_from_slice(&f64::to_le_bytes(y));
452            }
453        }
454
455        let scalar = native_geometry_scalar_from_wkb(&wkb)?.expect("a multilinestring scalar");
456        let DType::Extension(ext) = scalar.dtype() else {
457            panic!("expected an extension dtype, got {}", scalar.dtype());
458        };
459        assert!(ext.is::<MultiLineString>());
460        Ok(())
461    }
462}