vortex_geo/extension/
polygon.rs1use 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::VortexError;
42use vortex_error::VortexResult;
43use vortex_error::vortex_bail;
44use vortex_error::vortex_ensure;
45use vortex_error::vortex_err;
46use vortex_session::registry::CachedId;
47use vortex_session::registry::Id;
48
49use super::GeoMetadata;
50use super::coordinate::Dimension;
51use super::coordinate::coordinate_dimension;
52use super::coordinate::coordinate_storage_dtype;
53use super::geo_metadata_from_arrow;
54use super::geoarrow_metadata;
55use super::geoarrow_to_wkb;
56
57#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
59pub struct Polygon;
60
61impl ExtVTable for Polygon {
62 type Metadata = GeoMetadata;
63 type NativeValue<'a> = &'a ScalarValue;
65
66 fn id(&self) -> ExtId {
67 static ID: CachedId = CachedId::new("vortex.geo.polygon");
68 *ID
69 }
70
71 fn serialize_metadata(&self, metadata: &Self::Metadata) -> VortexResult<Vec<u8>> {
72 Ok(metadata.encode_to_vec())
73 }
74
75 fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult<Self::Metadata> {
76 Ok(GeoMetadata::decode(metadata)?)
77 }
78
79 fn validate_dtype(ext_dtype: &ExtDType<Self>) -> VortexResult<()> {
80 polygon_dimension(ext_dtype.storage_dtype()).map(|_| ())
81 }
82
83 fn unpack_native<'a>(
84 _ext_dtype: &'a ExtDType<Self>,
85 storage_value: &'a ScalarValue,
86 ) -> VortexResult<&'a ScalarValue> {
87 Ok(storage_value)
88 }
89}
90
91pub(crate) fn polygon_storage_dtype(dim: Dimension, nullability: Nullability) -> DType {
93 let coords = coordinate_storage_dtype(dim, Nullability::NonNullable);
94 let ring = DType::List(Arc::new(coords), Nullability::NonNullable);
95 DType::List(Arc::new(ring), nullability)
96}
97
98pub(crate) fn polygon_dimension(dtype: &DType) -> VortexResult<Dimension> {
100 let DType::List(ring, _) = dtype else {
101 vortex_bail!("polygon storage must be a List of rings, was {dtype}");
102 };
103 let DType::List(coords, _) = ring.as_ref() else {
104 vortex_bail!("polygon ring storage must be a List of coordinates, was {ring}");
105 };
106 coordinate_dimension(coords)
107}
108
109static ARROW_POLYGON: CachedId = CachedId::new(PolygonType::NAME);
110
111fn polygon_type(geo_metadata: &GeoMetadata, dimension: Dimension) -> PolygonType {
114 PolygonType::new(dimension.into(), geoarrow_metadata(geo_metadata))
115}
116
117pub(crate) fn polygon_geometries(
120 storage: &ArrayRef,
121 ctx: &mut ExecutionCtx,
122) -> VortexResult<Vec<Geometry<f64>>> {
123 polygon_array(storage, ctx)?
124 .iter()
125 .map(|geometry| -> VortexResult<Geometry<f64>> {
126 Ok(geometry
127 .ok_or_else(|| vortex_err!("geo: null geometry is not supported"))?
128 .map_err(|e| vortex_err!("geo: geometry access failed: {e}"))?
129 .to_geometry())
130 })
131 .collect()
132}
133
134fn polygon_array(storage: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<PolygonArray> {
136 let polygon_type = polygon_type(&GeoMetadata::default(), polygon_dimension(storage.dtype())?);
137 let session = ctx.session().clone();
138 let arrow = session.arrow().execute_arrow(storage.clone(), None, ctx)?;
139 PolygonArray::try_from((arrow.as_ref(), polygon_type))
140 .map_err(|e| vortex_err!("failed to construct PolygonArray: {e}"))
141}
142
143pub struct PolygonData(ExtensionArray);
145
146impl TryFrom<ExtensionArray> for PolygonData {
147 type Error = VortexError;
148
149 fn try_from(ext: ExtensionArray) -> Result<Self, Self::Error> {
150 vortex_ensure!(
151 ext.ext_dtype().is::<Polygon>(),
152 "expected a Polygon extension array"
153 );
154 Ok(PolygonData(ext))
155 }
156}
157
158impl PolygonData {
159 pub fn to_wkb(&self, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
161 geoarrow_to_wkb(&polygon_array(self.0.storage_array(), ctx)?)
162 }
163}
164
165impl ArrowExportVTable for Polygon {
166 fn arrow_ext_id(&self) -> Id {
167 *ARROW_POLYGON
168 }
169
170 fn vortex_id(&self) -> Id {
171 self.id()
172 }
173
174 fn to_arrow_field(
175 &self,
176 name: &str,
177 dtype: &DType,
178 session: &ArrowSession,
179 ) -> VortexResult<Option<Field>> {
180 let ext_type = dtype.as_extension();
181 let geo_metadata = ext_type.metadata::<Polygon>();
182 let dimension = polygon_dimension(ext_type.storage_dtype())?;
183
184 let mut field = session.to_arrow_field(name, ext_type.storage_dtype())?;
185 field.try_with_extension_type(polygon_type(geo_metadata, dimension))?;
186
187 Ok(Some(field))
188 }
189
190 fn execute_arrow(
191 &self,
192 array: ArrayRef,
193 target: &Field,
194 ctx: &mut ExecutionCtx,
195 ) -> VortexResult<ArrowExport> {
196 let is_polygon = array
197 .dtype()
198 .as_extension_opt()
199 .map(|ext| ext.is::<Polygon>())
200 .unwrap_or(false);
201 if !is_polygon {
202 return Ok(ArrowExport::Unsupported(array));
203 }
204
205 let Ok(polygon_meta) = target.try_extension_type::<PolygonType>() else {
206 return Ok(ArrowExport::Unsupported(array));
207 };
208 if polygon_meta.coord_type() != CoordType::Separated {
209 return Ok(ArrowExport::Unsupported(array));
210 }
211
212 let executed = array.execute::<ExtensionArray>(ctx)?;
213 let storage = executed.storage_array().clone();
214
215 let storage_field = Field::new(
216 String::new(),
217 target.data_type().clone(),
218 target.is_nullable(),
219 );
220 let session = ctx.session().clone();
221 let arrow_storage = session
222 .arrow()
223 .execute_arrow(storage, Some(&storage_field), ctx)?;
224
225 let polygons = PolygonArray::try_from((arrow_storage.as_ref(), polygon_meta))
227 .map_err(|e| vortex_err!("failed to construct PolygonArray: {e}"))?;
228
229 Ok(ArrowExport::Exported(Arc::new(polygons.into_arrow())))
230 }
231}
232
233impl ArrowImportVTable for Polygon {
234 fn arrow_ext_id(&self) -> Id {
235 *ARROW_POLYGON
236 }
237
238 fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>> {
243 let (dimension, metadata) =
244 if let Ok(polygon_meta) = field.try_extension_type::<PolygonType>() {
245 vortex_ensure!(
246 polygon_meta.coord_type() == CoordType::Separated,
247 "geoarrow.polygon with interleaved coordinates is not supported; \
248 re-encode with separated (struct) coordinates"
249 );
250 (
251 polygon_meta.dimension().into(),
252 geo_metadata_from_arrow(polygon_meta.metadata()),
253 )
254 } else {
255 if field.extension_type_name() != Some(PolygonType::NAME) {
259 return Ok(None);
260 }
261 let DType::List(ring, _) = DType::from_arrow(field) else {
262 return Ok(None);
263 };
264 let DType::List(coords, _) = ring.as_ref() else {
265 return Ok(None);
266 };
267 let DType::Struct(fields, _) = coords.as_ref() else {
268 return Ok(None);
269 };
270 let Ok(dimension) = Dimension::from_field_names(fields.names()) else {
271 return Ok(None);
272 };
273 (dimension, GeoMetadata::default())
274 };
275
276 let storage_dtype = polygon_storage_dtype(dimension, field.is_nullable().into());
277 Ok(Some(DType::Extension(
278 ExtDType::try_with_vtable(Polygon, metadata, storage_dtype)?.erased(),
279 )))
280 }
281
282 fn from_arrow_array(
283 &self,
284 array: ArrowArrayRef,
285 field: &Field,
286 dtype: &DType,
287 ) -> VortexResult<ArrowImport> {
288 let Some(ext_dtype) = dtype.as_extension_opt() else {
289 return Ok(ArrowImport::Unsupported(array));
290 };
291 if !ext_dtype.is::<Polygon>()
292 || field.try_extension_type::<PolygonType>().is_err()
293 || !matches!(array.data_type(), DataType::List(_))
294 {
295 return Ok(ArrowImport::Unsupported(array));
296 }
297
298 let storage = ArrayRef::from_arrow(array.as_ref(), field.is_nullable())?;
299 Ok(ArrowImport::Imported(
300 ExtensionArray::try_new(ext_dtype.clone(), storage)?.into_array(),
301 ))
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use std::sync::Arc;
308
309 use rstest::rstest;
310 use vortex_array::dtype::DType;
311 use vortex_array::dtype::Nullability;
312 use vortex_array::dtype::PType;
313 use vortex_array::dtype::extension::ExtDType;
314 use vortex_error::VortexResult;
315
316 use super::Polygon;
317 use super::polygon_storage_dtype;
318 use crate::extension::GeoMetadata;
319 use crate::extension::coordinate::Dimension;
320 use crate::extension::coordinate::coordinate_storage_dtype;
321
322 fn geo_meta() -> GeoMetadata {
323 GeoMetadata {
324 crs: Some("EPSG:4326".to_string()),
325 }
326 }
327
328 #[rstest]
330 #[case::xy(Dimension::Xy)]
331 #[case::xyz(Dimension::Xyz)]
332 #[case::xym(Dimension::Xym)]
333 #[case::xyzm(Dimension::Xyzm)]
334 fn polygon_validates_every_dimension(#[case] dim: Dimension) -> VortexResult<()> {
335 let storage = polygon_storage_dtype(dim, Nullability::NonNullable);
336 ExtDType::<Polygon>::try_new(geo_meta(), storage)?;
337 Ok(())
338 }
339
340 #[test]
343 fn polygon_rejects_invalid_storage() -> VortexResult<()> {
344 let primitive = DType::Primitive(PType::F64, Nullability::NonNullable);
345 assert!(ExtDType::<Polygon>::try_new(geo_meta(), primitive).is_err());
346
347 let coords = coordinate_storage_dtype(Dimension::Xy, Nullability::NonNullable);
349 let line = DType::List(Arc::new(coords), Nullability::NonNullable);
350 assert!(ExtDType::<Polygon>::try_new(geo_meta(), line).is_err());
351 Ok(())
352 }
353}