Skip to main content

iceberg/spec/
geospatial.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Geospatial types defined by the Iceberg specification.
19
20use std::hash::{Hash, Hasher};
21
22use serde::{Deserialize, Serialize};
23
24use super::PrimitiveType;
25use crate::error::{Result, invalid_data};
26
27pub(crate) const DEFAULT_GEOSPATIAL_CRS: &str = "OGC:CRS84";
28
29#[derive(Debug, Serialize, Deserialize, Clone)]
30struct Crs(String);
31
32impl Crs {
33    fn new(crs: Option<String>) -> Result<Self> {
34        let crs = crs.unwrap_or_else(|| DEFAULT_GEOSPATIAL_CRS.to_string());
35        let crs = crs.trim().to_string();
36        if crs.is_empty() || crs.contains([',', ')']) {
37            return Err(crate::Error::new(
38                crate::ErrorKind::DataInvalid,
39                "Geospatial CRS must be non-empty and must not contain ',' or ')'",
40            ));
41        }
42
43        Ok(Self(crs))
44    }
45}
46
47impl Default for Crs {
48    fn default() -> Self {
49        Self(DEFAULT_GEOSPATIAL_CRS.to_string())
50    }
51}
52
53impl PartialEq for Crs {
54    fn eq(&self, other: &Self) -> bool {
55        self.0.eq_ignore_ascii_case(&other.0)
56    }
57}
58
59impl Eq for Crs {}
60
61impl Hash for Crs {
62    fn hash<H: Hasher>(&self, state: &mut H) {
63        for byte in self.0.bytes() {
64            state.write_u8(byte.to_ascii_lowercase());
65        }
66    }
67}
68
69/// Iceberg geometry type.
70#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash, Default)]
71pub struct GeometryType {
72    crs: Crs,
73}
74
75impl GeometryType {
76    /// Creates a geometry type with an optional coordinate reference system.
77    pub fn new(crs: Option<String>) -> Result<Self> {
78        Ok(Self {
79            crs: Crs::new(crs)?,
80        })
81    }
82
83    /// Returns the coordinate reference system.
84    pub fn crs(&self) -> &str {
85        &self.crs.0
86    }
87}
88
89/// Iceberg geography edge interpolation algorithm.
90#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, Default)]
91#[serde(rename_all = "lowercase")]
92pub enum EdgeInterpolationAlgorithm {
93    /// Spherical edge interpolation.
94    #[default]
95    Spherical,
96    /// Vincenty edge interpolation.
97    Vincenty,
98    /// Thomas edge interpolation.
99    Thomas,
100    /// Andoyer edge interpolation.
101    Andoyer,
102    /// Karney edge interpolation.
103    Karney,
104}
105
106impl EdgeInterpolationAlgorithm {
107    pub(super) fn as_str(self) -> &'static str {
108        match self {
109            Self::Spherical => "spherical",
110            Self::Vincenty => "vincenty",
111            Self::Thomas => "thomas",
112            Self::Andoyer => "andoyer",
113            Self::Karney => "karney",
114        }
115    }
116
117    fn parse(value: &str) -> Result<Self> {
118        match value.trim().to_ascii_lowercase().as_str() {
119            "spherical" => Ok(Self::Spherical),
120            "vincenty" => Ok(Self::Vincenty),
121            "thomas" => Ok(Self::Thomas),
122            "andoyer" => Ok(Self::Andoyer),
123            "karney" => Ok(Self::Karney),
124            _ => Err(invalid_data!(
125                "Unknown geography edge interpolation algorithm: {value}"
126            )),
127        }
128    }
129}
130
131/// Iceberg geography type.
132#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
133pub struct GeographyType {
134    crs: Crs,
135    algorithm: EdgeInterpolationAlgorithm,
136}
137
138impl Default for GeographyType {
139    fn default() -> Self {
140        Self {
141            crs: Crs::default(),
142            algorithm: EdgeInterpolationAlgorithm::Spherical,
143        }
144    }
145}
146
147impl GeographyType {
148    /// Creates a geography type with an optional coordinate reference system and edge interpolation algorithm.
149    pub fn new(crs: Option<String>, algorithm: EdgeInterpolationAlgorithm) -> Result<Self> {
150        Ok(Self {
151            crs: Crs::new(crs)?,
152            algorithm,
153        })
154    }
155
156    /// Returns the coordinate reference system.
157    pub fn crs(&self) -> &str {
158        &self.crs.0
159    }
160
161    /// Returns the edge interpolation algorithm.
162    pub fn algorithm(&self) -> EdgeInterpolationAlgorithm {
163        self.algorithm
164    }
165}
166
167fn parse_geospatial_params<'a>(value: &'a str, type_name: &str) -> Result<Vec<&'a str>> {
168    if value.eq_ignore_ascii_case(type_name) {
169        return Ok(vec![]);
170    }
171
172    let params = value
173        .get(..type_name.len())
174        .filter(|prefix| prefix.eq_ignore_ascii_case(type_name))
175        .map(|_| &value[type_name.len()..])
176        .map(str::trim_start)
177        .and_then(|s| s.strip_prefix('('))
178        .and_then(|s| s.strip_suffix(')'))
179        .ok_or_else(|| invalid_data!("Invalid {type_name} type: {value}"))?;
180
181    if params.trim().is_empty() {
182        return Err(invalid_data!("{type_name} requires a non-empty CRS"));
183    }
184
185    Ok(params.split(',').map(str::trim).collect())
186}
187
188pub(super) fn parse_geometry(value: &str) -> Result<PrimitiveType> {
189    let params = parse_geospatial_params(value.trim(), "geometry")?;
190    let geometry = match params.as_slice() {
191        [] => GeometryType::default(),
192        [crs] if !crs.is_empty() => GeometryType::new(Some((*crs).to_string()))?,
193        _ => return Err(invalid_data!("Invalid geometry type: {value}")),
194    };
195
196    Ok(PrimitiveType::Geometry(geometry))
197}
198
199pub(super) fn parse_geography(value: &str) -> Result<PrimitiveType> {
200    let params = parse_geospatial_params(value.trim(), "geography")?;
201    let geography = match params.as_slice() {
202        [] => GeographyType::default(),
203        [crs] if !crs.is_empty() => GeographyType::new(
204            Some((*crs).to_string()),
205            EdgeInterpolationAlgorithm::Spherical,
206        )?,
207        [crs, algorithm] if !crs.is_empty() && !algorithm.is_empty() => GeographyType::new(
208            Some((*crs).to_string()),
209            EdgeInterpolationAlgorithm::parse(algorithm)?,
210        )?,
211        _ => return Err(invalid_data!("Invalid geography type: {value}")),
212    };
213
214    Ok(PrimitiveType::Geography(geography))
215}