use std::fmt::Display;
use std::fmt::Formatter;
use vortex_array::ArrayRef;
use vortex_array::ExecutionCtx;
use vortex_array::arrays::ExtensionArray;
use vortex_array::arrays::PrimitiveArray;
use vortex_array::arrays::StructArray;
use vortex_array::arrays::extension::ExtensionArrayExt;
use vortex_array::arrays::struct_::StructArrayExt;
use vortex_array::dtype::DType;
use vortex_array::dtype::FieldNames;
use vortex_array::dtype::Nullability;
use vortex_array::dtype::PType;
use vortex_array::scalar::Scalar;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_error::vortex_ensure;
use vortex_error::vortex_err;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Dimension {
Xy,
Xyz,
Xym,
Xyzm,
}
impl Dimension {
pub(crate) fn from_field_names(names: &FieldNames) -> VortexResult<Dimension> {
let mut strs = [""; 4];
vortex_ensure!(
names.len() <= strs.len(),
"not a valid GeoArrow coordinate dimension: {names:?}"
);
for (slot, name) in strs.iter_mut().zip(names.iter()) {
*slot = name.as_ref();
}
Ok(match &strs[..names.len()] {
["x", "y"] => Dimension::Xy,
["x", "y", "z"] => Dimension::Xyz,
["x", "y", "m"] => Dimension::Xym,
["x", "y", "z", "m"] => Dimension::Xyzm,
_ => vortex_bail!("not a valid GeoArrow coordinate dimension: {names:?}"),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Coordinate {
pub x: f64,
pub y: f64,
pub z: Option<f64>,
pub m: Option<f64>,
}
impl Coordinate {
pub fn xy(x: f64, y: f64) -> Self {
Coordinate {
x,
y,
z: None,
m: None,
}
}
}
impl Display for Coordinate {
fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result {
match (self.z, self.m) {
(None, None) => write!(fmt, "POINT({} {})", self.x, self.y),
(Some(z), None) => write!(fmt, "POINT Z ({} {} {})", self.x, self.y, z),
(None, Some(m)) => write!(fmt, "POINT M ({} {} {})", self.x, self.y, m),
(Some(z), Some(m)) => write!(fmt, "POINT ZM ({} {} {} {})", self.x, self.y, z, m),
}
}
}
pub(crate) fn coordinate_dimension(dtype: &DType) -> VortexResult<Dimension> {
let DType::Struct(fields, _) = dtype else {
vortex_bail!("coordinate storage must be a Struct, was {dtype}");
};
for (name, field) in fields.names().iter().zip(fields.fields()) {
vortex_ensure!(
matches!(
field,
DType::Primitive(PType::F64, Nullability::NonNullable)
),
"coordinate field {name} must be non-nullable f64, was {field}"
);
}
Dimension::from_field_names(fields.names())
}
pub(crate) fn coordinate_from_struct(scalar: &Scalar) -> VortexResult<Coordinate> {
let fields = scalar.as_struct();
let required = |name: &str| -> VortexResult<f64> {
f64::try_from(
&fields
.field(name)
.ok_or_else(|| vortex_err!("coordinate missing {name}"))?,
)
};
let optional = |name: &str| -> VortexResult<Option<f64>> {
fields
.field(name)
.map(|value| f64::try_from(&value))
.transpose()
};
Ok(Coordinate {
x: required("x")?,
y: required("y")?,
z: optional("z")?,
m: optional("m")?,
})
}
pub(crate) fn coordinate_from_scalar(scalar: &Scalar) -> VortexResult<Coordinate> {
match scalar.as_extension_opt() {
Some(ext_scalar) => coordinate_from_struct(&ext_scalar.to_storage_scalar()),
None => coordinate_from_struct(scalar),
}
}
pub(crate) struct ParsedCoordinates {
pub(crate) xs: PrimitiveArray,
pub(crate) ys: PrimitiveArray,
}
pub(crate) fn parse_storage(
points: &ArrayRef,
ctx: &mut ExecutionCtx,
) -> VortexResult<ParsedCoordinates> {
let storage = points
.clone()
.execute::<ExtensionArray>(ctx)?
.storage_array()
.clone()
.execute::<StructArray>(ctx)?;
coordinate_dimension(storage.dtype())?;
vortex_ensure!(
!storage.dtype().is_nullable(),
"coordinate storage must be non-nullable to read unmasked ordinates, was {}",
storage.dtype()
);
let xs = storage
.unmasked_field_by_name("x")?
.clone()
.execute::<PrimitiveArray>(ctx)?;
let ys = storage
.unmasked_field_by_name("y")?
.clone()
.execute::<PrimitiveArray>(ctx)?;
Ok(ParsedCoordinates { xs, ys })
}
#[cfg(test)]
mod tests {
use vortex_array::IntoArray;
use vortex_array::VortexSessionExecute;
use vortex_array::arrays::ExtensionArray;
use vortex_array::arrays::PrimitiveArray;
use vortex_array::arrays::StructArray;
use vortex_array::dtype::FieldNames;
use vortex_array::dtype::extension::ExtDType;
use vortex_array::session::ArraySession;
use vortex_array::validity::Validity;
use vortex_error::VortexResult;
use vortex_session::VortexSession;
use super::Coordinate;
use super::parse_storage;
use crate::extension::GeoMetadata;
use crate::extension::Point;
#[test]
fn display_is_wkt() {
let coordinate = |z, m| Coordinate {
x: 1.0,
y: 2.0,
z,
m,
};
assert_eq!(coordinate(None, None).to_string(), "POINT(1 2)");
assert_eq!(coordinate(Some(3.0), None).to_string(), "POINT Z (1 2 3)");
assert_eq!(coordinate(None, Some(4.0)).to_string(), "POINT M (1 2 4)");
assert_eq!(
coordinate(Some(3.0), Some(4.0)).to_string(),
"POINT ZM (1 2 3 4)"
);
}
#[test]
fn parse_rejects_nullable_points() -> VortexResult<()> {
let session = VortexSession::empty().with::<ArraySession>();
let mut ctx = session.create_execution_ctx();
let storage = StructArray::try_new(
FieldNames::from(["x", "y"]),
vec![
PrimitiveArray::from_iter(vec![1.0f64]).into_array(),
PrimitiveArray::from_iter(vec![2.0f64]).into_array(),
],
1,
Validity::AllValid,
)?
.into_array();
let dtype = ExtDType::<Point>::try_new(GeoMetadata { crs: None }, storage.dtype().clone())?;
let points = ExtensionArray::new(dtype.erased(), storage).into_array();
assert!(parse_storage(&points, &mut ctx).is_err());
Ok(())
}
}