Skip to main content

vortex_geo/extension/
polygon.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! The [`Polygon`] geometry extension type (`vortex.geo.polygon`): rings of the
5//! [`Point`](super::Point) coordinate struct, stored as `List<List<Struct<x, y[, z][, m]>>>` and tagged with
6//! [`GeoMetadata`] (CRS). The first ring is the exterior boundary; the rest are holes.
7
8use std::sync::Arc;
9
10use arrow_array::ArrayRef as ArrowArrayRef;
11use arrow_schema::DataType;
12use arrow_schema::Field;
13use arrow_schema::extension::ExtensionType;
14use geo_traits::to_geo::ToGeoGeometry;
15use geo_types::Geometry;
16use geoarrow::array::GeoArrowArrayAccessor;
17use geoarrow::array::IntoArrow;
18use geoarrow::array::PolygonArray;
19use geoarrow::datatypes::CoordType;
20use geoarrow::datatypes::PolygonType;
21use prost::Message;
22use vortex_array::ArrayRef;
23use vortex_array::ExecutionCtx;
24use vortex_array::IntoArray;
25use vortex_array::arrays::ExtensionArray;
26use vortex_array::arrays::extension::ExtensionArrayExt;
27use vortex_array::arrow::ArrowExport;
28use vortex_array::arrow::ArrowExportVTable;
29use vortex_array::arrow::ArrowImport;
30use vortex_array::arrow::ArrowImportVTable;
31use vortex_array::arrow::ArrowSession;
32use vortex_array::arrow::ArrowSessionExt;
33use vortex_array::arrow::FromArrowArray;
34use vortex_array::dtype::DType;
35use vortex_array::dtype::Nullability;
36use vortex_array::dtype::arrow::FromArrowType;
37use vortex_array::dtype::extension::ExtDType;
38use vortex_array::dtype::extension::ExtId;
39use vortex_array::dtype::extension::ExtVTable;
40use vortex_array::scalar::ScalarValue;
41use vortex_error::VortexResult;
42use vortex_error::vortex_bail;
43use vortex_error::vortex_ensure;
44use vortex_error::vortex_err;
45use vortex_session::registry::CachedId;
46use vortex_session::registry::Id;
47
48use super::GeoMetadata;
49use super::coordinate::Dimension;
50use super::coordinate::coordinate_dimension;
51use super::coordinate::coordinate_storage_dtype;
52use super::geo_metadata_from_arrow;
53use super::geoarrow_metadata;
54
55/// A polygon: `geoarrow.polygon`, stored as `List<List<Struct<x, y[, z][, m]>>>` (rings of vertices).
56#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
57pub struct Polygon;
58
59impl ExtVTable for Polygon {
60    type Metadata = GeoMetadata;
61    // No cheap owned value like Point's `Coordinate`; expose the raw storage scalar.
62    type NativeValue<'a> = &'a ScalarValue;
63
64    fn id(&self) -> ExtId {
65        static ID: CachedId = CachedId::new("vortex.geo.polygon");
66        *ID
67    }
68
69    fn serialize_metadata(&self, metadata: &Self::Metadata) -> VortexResult<Vec<u8>> {
70        Ok(metadata.encode_to_vec())
71    }
72
73    fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult<Self::Metadata> {
74        Ok(GeoMetadata::decode(metadata)?)
75    }
76
77    fn validate_dtype(ext_dtype: &ExtDType<Self>) -> VortexResult<()> {
78        polygon_dimension(ext_dtype.storage_dtype()).map(|_| ())
79    }
80
81    fn unpack_native<'a>(
82        _ext_dtype: &'a ExtDType<Self>,
83        storage_value: &'a ScalarValue,
84    ) -> VortexResult<&'a ScalarValue> {
85        Ok(storage_value)
86    }
87}
88
89/// Canonical polygon storage: an outer list of rings, each a list of the coordinate `Struct`.
90pub(crate) fn polygon_storage_dtype(dim: Dimension, nullability: Nullability) -> DType {
91    let coords = coordinate_storage_dtype(dim, Nullability::NonNullable);
92    let ring = DType::List(Arc::new(coords), Nullability::NonNullable);
93    DType::List(Arc::new(ring), nullability)
94}
95
96/// Validate `dtype` is `List<List<coordinate-struct>>` and return its [`Dimension`].
97pub(crate) fn polygon_dimension(dtype: &DType) -> VortexResult<Dimension> {
98    let DType::List(ring, _) = dtype else {
99        vortex_bail!("polygon storage must be a List of rings, was {dtype}");
100    };
101    let DType::List(coords, _) = ring.as_ref() else {
102        vortex_bail!("polygon ring storage must be a List of coordinates, was {ring}");
103    };
104    coordinate_dimension(coords)
105}
106
107static ARROW_POLYGON: CachedId = CachedId::new(PolygonType::NAME);
108
109/// The `geoarrow.polygon` extension type for `dimension`, with separated (struct) coordinates
110/// matching `Polygon` storage.
111fn polygon_type(geo_metadata: &GeoMetadata, dimension: Dimension) -> PolygonType {
112    PolygonType::new(dimension.into(), geoarrow_metadata(geo_metadata))
113}
114
115/// Decode `Polygon` storage (`List<List<coordinate>>`) to `geo_types` polygons, for the geo scalar
116/// functions. CRS does not affect planar geometry ops, so default metadata is used.
117pub(crate) fn polygon_geometries(
118    storage: &ArrayRef,
119    ctx: &mut ExecutionCtx,
120) -> VortexResult<Vec<Geometry<f64>>> {
121    let polygon_type = polygon_type(&GeoMetadata::default(), polygon_dimension(storage.dtype())?);
122    let session = ctx.session().clone();
123    let arrow = session.arrow().execute_arrow(storage.clone(), None, ctx)?;
124    let polygons = PolygonArray::try_from((arrow.as_ref(), polygon_type))
125        .map_err(|e| vortex_err!("failed to construct PolygonArray: {e}"))?;
126    polygons
127        .iter()
128        .map(|geometry| -> VortexResult<Geometry<f64>> {
129            Ok(geometry
130                .ok_or_else(|| vortex_err!("geo: null geometry is not supported"))?
131                .map_err(|e| vortex_err!("geo: geometry access failed: {e}"))?
132                .to_geometry())
133        })
134        .collect()
135}
136
137impl ArrowExportVTable for Polygon {
138    fn arrow_ext_id(&self) -> Id {
139        *ARROW_POLYGON
140    }
141
142    fn vortex_id(&self) -> Id {
143        self.id()
144    }
145
146    fn to_arrow_field(
147        &self,
148        name: &str,
149        dtype: &DType,
150        session: &ArrowSession,
151    ) -> VortexResult<Option<Field>> {
152        let ext_type = dtype.as_extension();
153        let geo_metadata = ext_type.metadata::<Polygon>();
154        let dimension = polygon_dimension(ext_type.storage_dtype())?;
155
156        let mut field = session.to_arrow_field(name, ext_type.storage_dtype())?;
157        field.try_with_extension_type(polygon_type(geo_metadata, dimension))?;
158
159        Ok(Some(field))
160    }
161
162    fn execute_arrow(
163        &self,
164        array: ArrayRef,
165        target: &Field,
166        ctx: &mut ExecutionCtx,
167    ) -> VortexResult<ArrowExport> {
168        let is_polygon = array
169            .dtype()
170            .as_extension_opt()
171            .map(|ext| ext.is::<Polygon>())
172            .unwrap_or(false);
173        if !is_polygon {
174            return Ok(ArrowExport::Unsupported(array));
175        }
176
177        let Ok(polygon_meta) = target.try_extension_type::<PolygonType>() else {
178            return Ok(ArrowExport::Unsupported(array));
179        };
180        if polygon_meta.coord_type() != CoordType::Separated {
181            return Ok(ArrowExport::Unsupported(array));
182        }
183
184        let executed = array.execute::<ExtensionArray>(ctx)?;
185        let storage = executed.storage_array().clone();
186
187        let storage_field = Field::new(
188            String::new(),
189            target.data_type().clone(),
190            target.is_nullable(),
191        );
192        let session = ctx.session().clone();
193        let arrow_storage = session
194            .arrow()
195            .execute_arrow(storage, Some(&storage_field), ctx)?;
196
197        // Round-trip through GeoArrow's polygon array; `into_arrow` is concrete, so wrap in `Arc`.
198        let polygons = PolygonArray::try_from((arrow_storage.as_ref(), polygon_meta))
199            .map_err(|e| vortex_err!("failed to construct PolygonArray: {e}"))?;
200
201        Ok(ArrowExport::Exported(Arc::new(polygons.into_arrow())))
202    }
203}
204
205impl ArrowImportVTable for Polygon {
206    fn arrow_ext_id(&self) -> Id {
207        *ARROW_POLYGON
208    }
209
210    /// Import a `geoarrow.polygon` field as the [`Polygon`] dtype. Keyed off the standard GeoArrow
211    /// name, so any producer (DataFusion, DuckDB, geoarrow-rs, …) resolves here. Accepts the full
212    /// `PolygonType` extension, or — for a metadata-less geometry literal — the name alone, inferring
213    /// the dimension from the coordinate field names.
214    fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>> {
215        let (dimension, metadata) =
216            if let Ok(polygon_meta) = field.try_extension_type::<PolygonType>() {
217                vortex_ensure!(
218                    polygon_meta.coord_type() == CoordType::Separated,
219                    "geoarrow.polygon with interleaved coordinates is not supported; \
220                 re-encode with separated (struct) coordinates"
221                );
222                (
223                    polygon_meta.dimension().into(),
224                    geo_metadata_from_arrow(polygon_meta.metadata()),
225                )
226            } else {
227                // Infer the dimension from the field names, not the canonical storage check: a literal's
228                // coordinate fields may be nullable, which that check rejects. Peel the two `List` layers
229                // (polygon → rings → coordinates) to reach the struct.
230                if field.extension_type_name() != Some(PolygonType::NAME) {
231                    return Ok(None);
232                }
233                let DType::List(ring, _) = DType::from_arrow(field) else {
234                    return Ok(None);
235                };
236                let DType::List(coords, _) = ring.as_ref() else {
237                    return Ok(None);
238                };
239                let DType::Struct(fields, _) = coords.as_ref() else {
240                    return Ok(None);
241                };
242                let Ok(dimension) = Dimension::from_field_names(fields.names()) else {
243                    return Ok(None);
244                };
245                (dimension, GeoMetadata::default())
246            };
247
248        let storage_dtype = polygon_storage_dtype(dimension, field.is_nullable().into());
249        Ok(Some(DType::Extension(
250            ExtDType::try_with_vtable(Polygon, metadata, storage_dtype)?.erased(),
251        )))
252    }
253
254    fn from_arrow_array(
255        &self,
256        array: ArrowArrayRef,
257        field: &Field,
258        dtype: &DType,
259    ) -> VortexResult<ArrowImport> {
260        let Some(ext_dtype) = dtype.as_extension_opt() else {
261            return Ok(ArrowImport::Unsupported(array));
262        };
263        if !ext_dtype.is::<Polygon>()
264            || field.try_extension_type::<PolygonType>().is_err()
265            || !matches!(array.data_type(), DataType::List(_))
266        {
267            return Ok(ArrowImport::Unsupported(array));
268        }
269
270        let storage = ArrayRef::from_arrow(array.as_ref(), field.is_nullable())?;
271        Ok(ArrowImport::Imported(
272            ExtensionArray::try_new(ext_dtype.clone(), storage)?.into_array(),
273        ))
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use std::sync::Arc;
280
281    use rstest::rstest;
282    use vortex_array::dtype::DType;
283    use vortex_array::dtype::Nullability;
284    use vortex_array::dtype::PType;
285    use vortex_array::dtype::extension::ExtDType;
286    use vortex_error::VortexResult;
287
288    use super::Polygon;
289    use super::polygon_storage_dtype;
290    use crate::extension::GeoMetadata;
291    use crate::extension::coordinate::Dimension;
292    use crate::extension::coordinate::coordinate_storage_dtype;
293
294    fn geo_meta() -> GeoMetadata {
295        GeoMetadata {
296            crs: Some("EPSG:4326".to_string()),
297        }
298    }
299
300    /// `Polygon` accepts the canonical `List<List<coordinate-struct>>` storage of every dimension.
301    #[rstest]
302    #[case::xy(Dimension::Xy)]
303    #[case::xyz(Dimension::Xyz)]
304    #[case::xym(Dimension::Xym)]
305    #[case::xyzm(Dimension::Xyzm)]
306    fn polygon_validates_every_dimension(#[case] dim: Dimension) -> VortexResult<()> {
307        let storage = polygon_storage_dtype(dim, Nullability::NonNullable);
308        ExtDType::<Polygon>::try_new(geo_meta(), storage)?;
309        Ok(())
310    }
311
312    /// Non-polygon storage is rejected at dtype construction: a bare struct (point) and a single
313    /// list (linestring) both fail.
314    #[test]
315    fn polygon_rejects_invalid_storage() -> VortexResult<()> {
316        let primitive = DType::Primitive(PType::F64, Nullability::NonNullable);
317        assert!(ExtDType::<Polygon>::try_new(geo_meta(), primitive).is_err());
318
319        // A single list of coordinates is a LineString, not a Polygon.
320        let coords = coordinate_storage_dtype(Dimension::Xy, Nullability::NonNullable);
321        let line = DType::List(Arc::new(coords), Nullability::NonNullable);
322        assert!(ExtDType::<Polygon>::try_new(geo_meta(), line).is_err());
323        Ok(())
324    }
325}