vortex_geo/extension/
point.rs1use arrow_array::ArrayRef as ArrowArrayRef;
10use arrow_schema::DataType;
11use arrow_schema::Field;
12use arrow_schema::extension::ExtensionType;
13use geo_traits::to_geo::ToGeoGeometry;
14use geo_types::Geometry;
15use geoarrow::array::GeoArrowArrayAccessor;
16use geoarrow::array::IntoArrow;
17use geoarrow::array::PointArray;
18use geoarrow::datatypes::CoordType;
19use geoarrow::datatypes::PointType;
20use prost::Message;
21use vortex_array::ArrayRef;
22use vortex_array::ExecutionCtx;
23use vortex_array::IntoArray;
24use vortex_array::arrays::ExtensionArray;
25use vortex_array::arrays::extension::ExtensionArrayExt;
26use vortex_array::arrow::ArrowExport;
27use vortex_array::arrow::ArrowExportVTable;
28use vortex_array::arrow::ArrowImport;
29use vortex_array::arrow::ArrowImportVTable;
30use vortex_array::arrow::ArrowSession;
31use vortex_array::arrow::ArrowSessionExt;
32use vortex_array::arrow::FromArrowArray;
33use vortex_array::dtype::DType;
34use vortex_array::dtype::arrow::FromArrowType;
35use vortex_array::dtype::extension::ExtDType;
36use vortex_array::dtype::extension::ExtId;
37use vortex_array::dtype::extension::ExtVTable;
38use vortex_array::scalar::Scalar;
39use vortex_array::scalar::ScalarValue;
40use vortex_error::VortexError;
41use vortex_error::VortexResult;
42use vortex_error::vortex_ensure;
43use vortex_error::vortex_err;
44use vortex_session::registry::CachedId;
45use vortex_session::registry::Id;
46
47use super::GeoMetadata;
48use super::coordinate::Coordinate;
49use super::coordinate::Dimension;
50use super::coordinate::coordinate_dimension;
51use super::coordinate::coordinate_from_struct;
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 Point;
60
61impl ExtVTable for Point {
62 type Metadata = GeoMetadata;
63 type NativeValue<'a> = Coordinate;
64
65 fn id(&self) -> ExtId {
66 static ID: CachedId = CachedId::new("vortex.geo.point");
67 *ID
68 }
69
70 fn serialize_metadata(&self, metadata: &Self::Metadata) -> VortexResult<Vec<u8>> {
71 Ok(metadata.encode_to_vec())
72 }
73
74 fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult<Self::Metadata> {
75 Ok(GeoMetadata::decode(metadata)?)
76 }
77
78 fn validate_dtype(ext_dtype: &ExtDType<Self>) -> VortexResult<()> {
79 coordinate_dimension(ext_dtype.storage_dtype()).map(|_| ())
80 }
81
82 fn unpack_native<'a>(
83 ext_dtype: &'a ExtDType<Self>,
84 storage_value: &'a ScalarValue,
85 ) -> VortexResult<Coordinate> {
86 let storage = Scalar::try_new(
87 ext_dtype.storage_dtype().clone(),
88 Some(storage_value.clone()),
89 )?;
90 coordinate_from_struct(&storage)
91 }
92}
93
94static ARROW_POINT: CachedId = CachedId::new(PointType::NAME);
95
96fn point_type(geo_metadata: &GeoMetadata, dimension: Dimension) -> PointType {
99 PointType::new(dimension.into(), geoarrow_metadata(geo_metadata))
100}
101
102pub struct PointData(ExtensionArray);
103
104impl TryFrom<ExtensionArray> for PointData {
105 type Error = VortexError;
106
107 fn try_from(ext: ExtensionArray) -> Result<Self, Self::Error> {
108 vortex_ensure!(
109 ext.ext_dtype().is::<Point>(),
110 "expected a Point extension array"
111 );
112 Ok(PointData(ext))
113 }
114}
115
116impl PointData {
117 pub fn to_wkb(&self, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
119 geoarrow_to_wkb(&point_array(self.0.storage_array(), ctx)?)
120 }
121}
122
123fn point_array(storage: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<PointArray> {
126 let point_type = point_type(
127 &GeoMetadata::default(),
128 coordinate_dimension(storage.dtype())?,
129 );
130 let session = ctx.session().clone();
131 let arrow = session.arrow().execute_arrow(storage.clone(), None, ctx)?;
132 PointArray::try_from((arrow.as_ref(), point_type))
133 .map_err(|e| vortex_err!("failed to construct PointArray: {e}"))
134}
135
136pub(crate) fn point_geometries(
138 storage: &ArrayRef,
139 ctx: &mut ExecutionCtx,
140) -> VortexResult<Vec<Geometry<f64>>> {
141 point_array(storage, ctx)?
142 .iter()
143 .map(|geometry| -> VortexResult<Geometry<f64>> {
144 Ok(geometry
145 .ok_or_else(|| vortex_err!("geo: null geometry is not supported"))?
146 .map_err(|e| vortex_err!("geo: geometry access failed: {e}"))?
147 .to_geometry())
148 })
149 .collect()
150}
151
152impl ArrowExportVTable for Point {
153 fn arrow_ext_id(&self) -> Id {
154 *ARROW_POINT
155 }
156
157 fn vortex_id(&self) -> Id {
158 self.id()
159 }
160
161 fn to_arrow_field(
162 &self,
163 name: &str,
164 dtype: &DType,
165 session: &ArrowSession,
166 ) -> VortexResult<Option<Field>> {
167 let ext_type = dtype.as_extension();
168 let geo_metadata = ext_type.metadata::<Point>();
169 let dimension = coordinate_dimension(ext_type.storage_dtype())?;
170
171 let mut field = session.to_arrow_field(name, ext_type.storage_dtype())?;
172 field.try_with_extension_type(point_type(geo_metadata, dimension))?;
173
174 Ok(Some(field))
175 }
176
177 fn execute_arrow(
178 &self,
179 array: ArrayRef,
180 target: &Field,
181 ctx: &mut ExecutionCtx,
182 ) -> VortexResult<ArrowExport> {
183 let is_point = array
184 .dtype()
185 .as_extension_opt()
186 .map(|ext| ext.is::<Point>())
187 .unwrap_or(false);
188 if !is_point {
189 return Ok(ArrowExport::Unsupported(array));
190 }
191
192 let Ok(point_meta) = target.try_extension_type::<PointType>() else {
193 return Ok(ArrowExport::Unsupported(array));
194 };
195 if point_meta.coord_type() != CoordType::Separated {
196 return Ok(ArrowExport::Unsupported(array));
197 }
198
199 let executed = array.execute::<ExtensionArray>(ctx)?;
200 let storage = executed.storage_array().clone();
201
202 let storage_field = Field::new(
203 String::new(),
204 target.data_type().clone(),
205 target.is_nullable(),
206 );
207 let session = ctx.session().clone();
208 let arrow_storage = session
209 .arrow()
210 .execute_arrow(storage, Some(&storage_field), ctx)?;
211
212 let points = PointArray::try_from((arrow_storage.as_ref(), point_meta))
215 .map_err(|e| vortex_err!("failed to construct PointArray: {e}"))?;
216
217 Ok(ArrowExport::Exported(points.into_arrow()))
218 }
219}
220
221impl ArrowImportVTable for Point {
222 fn arrow_ext_id(&self) -> Id {
223 *ARROW_POINT
224 }
225
226 fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>> {
231 let (dimension, metadata) = if let Ok(point_meta) = field.try_extension_type::<PointType>()
232 {
233 vortex_ensure!(
234 point_meta.coord_type() == CoordType::Separated,
235 "geoarrow.point with interleaved coordinates is not supported; \
236 re-encode with separated (struct) coordinates"
237 );
238 (
239 point_meta.dimension().into(),
240 geo_metadata_from_arrow(point_meta.metadata()),
241 )
242 } else {
243 if field.extension_type_name() != Some(PointType::NAME) {
246 return Ok(None);
247 }
248 let DType::Struct(fields, _) = DType::from_arrow(field) else {
249 return Ok(None);
250 };
251 let Ok(dimension) = Dimension::from_field_names(fields.names()) else {
252 return Ok(None);
253 };
254 (dimension, GeoMetadata::default())
255 };
256
257 let storage_dtype = coordinate_storage_dtype(dimension, field.is_nullable().into());
258 Ok(Some(DType::Extension(
259 ExtDType::try_with_vtable(Point, metadata, storage_dtype)?.erased(),
260 )))
261 }
262
263 fn from_arrow_array(
264 &self,
265 array: ArrowArrayRef,
266 field: &Field,
267 dtype: &DType,
268 ) -> VortexResult<ArrowImport> {
269 let Some(ext_dtype) = dtype.as_extension_opt() else {
270 return Ok(ArrowImport::Unsupported(array));
271 };
272 if !ext_dtype.is::<Point>()
273 || field.try_extension_type::<PointType>().is_err()
274 || !matches!(array.data_type(), DataType::Struct(_))
275 {
276 return Ok(ArrowImport::Unsupported(array));
277 }
278
279 let storage = ArrayRef::from_arrow(array.as_ref(), field.is_nullable())?;
280 Ok(ArrowImport::Imported(
281 ExtensionArray::try_new(ext_dtype.clone(), storage)?.into_array(),
282 ))
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use rstest::rstest;
289 use vortex_array::IntoArray;
290 use vortex_array::VortexSessionExecute;
291 use vortex_array::arrays::PrimitiveArray;
292 use vortex_array::arrays::StructArray;
293 use vortex_array::dtype::DType;
294 use vortex_array::dtype::Nullability;
295 use vortex_array::dtype::PType;
296 use vortex_array::dtype::extension::ExtDType;
297 use vortex_error::VortexResult;
298
299 use super::Point;
300 use crate::extension::GeoMetadata;
301 use crate::extension::coordinate::Coordinate;
302 use crate::extension::coordinate::Dimension;
303 use crate::extension::coordinate::coordinate_storage_dtype;
304 use crate::test_harness::coordinate_from_scalar;
305 use crate::test_harness::point_column;
306
307 fn geo_meta() -> GeoMetadata {
308 GeoMetadata {
309 crs: Some("EPSG:4326".to_string()),
310 }
311 }
312
313 #[rstest]
315 #[case::xy(Dimension::Xy)]
316 #[case::xyz(Dimension::Xyz)]
317 #[case::xym(Dimension::Xym)]
318 #[case::xyzm(Dimension::Xyzm)]
319 fn point_validates_every_dimension(#[case] dim: Dimension) -> VortexResult<()> {
320 let storage = coordinate_storage_dtype(dim, Nullability::NonNullable);
321 ExtDType::<Point>::try_new(geo_meta(), storage)?;
322 Ok(())
323 }
324
325 #[test]
328 fn point_rejects_invalid_storage() -> VortexResult<()> {
329 let primitive = DType::Primitive(PType::F64, Nullability::NonNullable);
330 assert!(ExtDType::<Point>::try_new(geo_meta(), primitive).is_err());
331
332 let wrong_fields = StructArray::from_fields(&[
333 ("a", PrimitiveArray::from_iter(vec![0.0f64]).into_array()),
334 ("b", PrimitiveArray::from_iter(vec![0.0f64]).into_array()),
335 ])?
336 .into_array();
337 assert!(ExtDType::<Point>::try_new(geo_meta(), wrong_fields.dtype().clone()).is_err());
338 Ok(())
339 }
340
341 #[test]
343 fn point_unpacks_coordinates() -> VortexResult<()> {
344 let session = vortex_array::array_session();
345 let mut ctx = session.create_execution_ctx();
346
347 let points = point_column(vec![1.0, -111.7610], vec![2.0, 34.8697])?;
348
349 assert_eq!(
350 coordinate_from_scalar(&points.execute_scalar(0, &mut ctx)?)?,
351 Coordinate::xy(1.0, 2.0)
352 );
353 assert_eq!(
354 coordinate_from_scalar(&points.execute_scalar(1, &mut ctx)?)?,
355 Coordinate::xy(-111.7610, 34.8697)
356 );
357 Ok(())
358 }
359}