iceberg/spec/
geospatial.rs1use 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#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash, Default)]
71pub struct GeometryType {
72 crs: Crs,
73}
74
75impl GeometryType {
76 pub fn new(crs: Option<String>) -> Result<Self> {
78 Ok(Self {
79 crs: Crs::new(crs)?,
80 })
81 }
82
83 pub fn crs(&self) -> &str {
85 &self.crs.0
86 }
87}
88
89#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, Default)]
91#[serde(rename_all = "lowercase")]
92pub enum EdgeInterpolationAlgorithm {
93 #[default]
95 Spherical,
96 Vincenty,
98 Thomas,
100 Andoyer,
102 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#[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 pub fn new(crs: Option<String>, algorithm: EdgeInterpolationAlgorithm) -> Result<Self> {
150 Ok(Self {
151 crs: Crs::new(crs)?,
152 algorithm,
153 })
154 }
155
156 pub fn crs(&self) -> &str {
158 &self.crs.0
159 }
160
161 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}