geoarrow_schema/
dimension.rs

1use std::collections::HashSet;
2use std::fmt::Display;
3
4use arrow_schema::{ArrowError, Field, Fields};
5
6use crate::error::{GeoArrowError, GeoArrowResult};
7
8/// The dimension of the geometry array.
9///
10/// [Dimension] implements [TryFrom] for integers:
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Dimension {
13    /// Two-dimensional.
14    XY,
15
16    /// Three-dimensional.
17    XYZ,
18
19    /// XYM (2D with measure).
20    XYM,
21
22    /// XYZM (3D with measure).
23    XYZM,
24}
25
26impl Dimension {
27    pub(crate) fn from_interleaved_field(field: &Field) -> GeoArrowResult<Self> {
28        let dim = match field.name().as_str() {
29            "xy" => Dimension::XY,
30            "xyz" => Dimension::XYZ,
31            "xym" => Dimension::XYM,
32            "xyzm" => Dimension::XYZM,
33            _ => {
34                return Err(ArrowError::SchemaError(format!(
35                    "Invalid interleaved field name: {}",
36                    field.name()
37                ))
38                .into());
39            }
40        };
41        Ok(dim)
42    }
43
44    pub(crate) fn from_separated_field(fields: &Fields) -> GeoArrowResult<Self> {
45        let dim = if fields.len() == 2 {
46            Self::XY
47        } else if fields.len() == 3 {
48            let field_names: HashSet<&str> =
49                HashSet::from_iter(fields.iter().map(|f| f.name().as_str()));
50            let xym_field_names = HashSet::<&str>::from_iter(["x", "y", "m"]);
51            let xyz_field_names = HashSet::<&str>::from_iter(["x", "y", "z"]);
52
53            if field_names.eq(&xym_field_names) {
54                Self::XYM
55            } else if field_names.eq(&xyz_field_names) {
56                Self::XYZ
57            } else {
58                return Err(ArrowError::SchemaError(format!(
59                    "Invalid field names for separated coordinates with 3 dimensions: {field_names:?}",
60
61                ))
62                .into());
63            }
64        } else if fields.len() == 4 {
65            Self::XYZM
66        } else {
67            return Err(ArrowError::SchemaError(format!(
68                "Invalid fields for separated coordinates: {fields:?}",
69            ))
70            .into());
71        };
72        Ok(dim)
73    }
74
75    /// Returns the number of dimensions.
76    pub fn size(&self) -> usize {
77        match self {
78            Dimension::XY => 2,
79            Dimension::XYZ => 3,
80            Dimension::XYM => 3,
81            Dimension::XYZM => 4,
82        }
83    }
84}
85
86impl From<Dimension> for geo_traits::Dimensions {
87    fn from(value: Dimension) -> Self {
88        match value {
89            Dimension::XY => geo_traits::Dimensions::Xy,
90            Dimension::XYZ => geo_traits::Dimensions::Xyz,
91            Dimension::XYM => geo_traits::Dimensions::Xym,
92            Dimension::XYZM => geo_traits::Dimensions::Xyzm,
93        }
94    }
95}
96
97impl TryFrom<geo_traits::Dimensions> for Dimension {
98    type Error = GeoArrowError;
99
100    fn try_from(value: geo_traits::Dimensions) -> std::result::Result<Self, Self::Error> {
101        match value {
102            geo_traits::Dimensions::Xy | geo_traits::Dimensions::Unknown(2) => Ok(Dimension::XY),
103            geo_traits::Dimensions::Xyz | geo_traits::Dimensions::Unknown(3) => Ok(Dimension::XYZ),
104            geo_traits::Dimensions::Xym => Ok(Dimension::XYM),
105            geo_traits::Dimensions::Xyzm | geo_traits::Dimensions::Unknown(4) => {
106                Ok(Dimension::XYZM)
107            }
108            _ => Err(GeoArrowError::InvalidGeoArrow(format!(
109                "Unsupported dimension {value:?}"
110            ))),
111        }
112    }
113}
114
115impl Display for Dimension {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        match self {
118            Dimension::XY => write!(f, "XY"),
119            Dimension::XYZ => write!(f, "XYZ"),
120            Dimension::XYM => write!(f, "XYM"),
121            Dimension::XYZM => write!(f, "XYZM"),
122        }
123    }
124}
125
126#[cfg(test)]
127mod test {
128    use std::iter::zip;
129
130    use arrow_schema::DataType;
131
132    use super::*;
133
134    #[test]
135    fn from_interleaved() {
136        assert!(matches!(
137            Dimension::from_interleaved_field(&Field::new("xy", DataType::Null, false)).unwrap(),
138            Dimension::XY
139        ));
140
141        assert!(matches!(
142            Dimension::from_interleaved_field(&Field::new("xyz", DataType::Null, false)).unwrap(),
143            Dimension::XYZ
144        ));
145
146        assert!(matches!(
147            Dimension::from_interleaved_field(&Field::new("xym", DataType::Null, false)).unwrap(),
148            Dimension::XYM
149        ));
150
151        assert!(matches!(
152            Dimension::from_interleaved_field(&Field::new("xyzm", DataType::Null, false)).unwrap(),
153            Dimension::XYZM
154        ));
155    }
156
157    #[test]
158    fn from_bad_interleaved() {
159        assert!(
160            Dimension::from_interleaved_field(&Field::new("banana", DataType::Null, false))
161                .is_err()
162        );
163        assert!(
164            Dimension::from_interleaved_field(&Field::new("x", DataType::Null, false)).is_err()
165        );
166        assert!(
167            Dimension::from_interleaved_field(&Field::new("xyzmt", DataType::Null, false)).is_err()
168        );
169    }
170
171    fn test_fields(dims: &[&str]) -> Fields {
172        dims.iter()
173            .map(|dim| Field::new(*dim, DataType::Null, false))
174            .collect()
175    }
176
177    #[test]
178    fn from_separated() {
179        assert!(matches!(
180            Dimension::from_separated_field(&test_fields(&["x", "y"])).unwrap(),
181            Dimension::XY
182        ));
183
184        assert!(matches!(
185            Dimension::from_separated_field(&test_fields(&["x", "y", "z"])).unwrap(),
186            Dimension::XYZ
187        ));
188
189        assert!(matches!(
190            Dimension::from_separated_field(&test_fields(&["x", "y", "m"])).unwrap(),
191            Dimension::XYM
192        ));
193
194        assert!(matches!(
195            Dimension::from_separated_field(&test_fields(&["x", "y", "z", "m"])).unwrap(),
196            Dimension::XYZM
197        ));
198    }
199
200    #[test]
201    fn from_bad_separated() {
202        assert!(Dimension::from_separated_field(&test_fields(&["x"])).is_err());
203        assert!(Dimension::from_separated_field(&test_fields(&["x", "y", "a"])).is_err());
204        assert!(Dimension::from_separated_field(&test_fields(&["x", "y", "z", "m", "t"])).is_err());
205    }
206
207    #[test]
208    fn geotraits_dimensions() {
209        let geoarrow_dims = [
210            Dimension::XY,
211            Dimension::XYZ,
212            Dimension::XYM,
213            Dimension::XYZM,
214        ];
215        let geotraits_dims = [
216            geo_traits::Dimensions::Xy,
217            geo_traits::Dimensions::Xyz,
218            geo_traits::Dimensions::Xym,
219            geo_traits::Dimensions::Xyzm,
220        ];
221
222        for (geoarrow_dim, geotraits_dim) in zip(geoarrow_dims, geotraits_dims) {
223            let into_geotraits_dim: geo_traits::Dimensions = geoarrow_dim.into();
224            assert_eq!(into_geotraits_dim, geotraits_dim);
225
226            let into_geoarrow_dim: Dimension = geotraits_dim.try_into().unwrap();
227            assert_eq!(into_geoarrow_dim, geoarrow_dim);
228
229            assert_eq!(geoarrow_dim.size(), geotraits_dim.size());
230        }
231
232        let dims2: Dimension = geo_traits::Dimensions::Unknown(2).try_into().unwrap();
233        assert_eq!(dims2, Dimension::XY);
234
235        let dims3: Dimension = geo_traits::Dimensions::Unknown(3).try_into().unwrap();
236        assert_eq!(dims3, Dimension::XYZ);
237
238        let dims4: Dimension = geo_traits::Dimensions::Unknown(4).try_into().unwrap();
239        assert_eq!(dims4, Dimension::XYZM);
240
241        let dims_err: Result<Dimension, GeoArrowError> =
242            geo_traits::Dimensions::Unknown(0).try_into();
243        assert_eq!(
244            dims_err.unwrap_err().to_string(),
245            "Data not conforming to GeoArrow specification: Unsupported dimension Unknown(0)"
246        );
247    }
248}