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::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#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
57pub struct Polygon;
58
59impl ExtVTable for Polygon {
60 type Metadata = GeoMetadata;
61 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
89pub(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
96pub(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
109fn polygon_type(geo_metadata: &GeoMetadata, dimension: Dimension) -> PolygonType {
112 PolygonType::new(dimension.into(), geoarrow_metadata(geo_metadata))
113}
114
115pub(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 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 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 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 #[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 #[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 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}