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