1use crate::geokeys::{self, GeoKeyDirectory, GeoKeyValue};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ModelType {
8 Projected,
9 Geographic,
10 Geocentric,
11 Unknown(u16),
12}
13
14impl ModelType {
15 pub fn from_code(code: u16) -> Self {
16 match code {
17 1 => Self::Projected,
18 2 => Self::Geographic,
19 3 => Self::Geocentric,
20 other => Self::Unknown(other),
21 }
22 }
23
24 pub fn code(&self) -> u16 {
25 match self {
26 Self::Projected => 1,
27 Self::Geographic => 2,
28 Self::Geocentric => 3,
29 Self::Unknown(v) => *v,
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum RasterType {
37 PixelIsArea,
38 PixelIsPoint,
39 Unknown(u16),
40}
41
42impl RasterType {
43 pub fn from_code(code: u16) -> Self {
44 match code {
45 1 => Self::PixelIsArea,
46 2 => Self::PixelIsPoint,
47 other => Self::Unknown(other),
48 }
49 }
50
51 pub fn code(&self) -> u16 {
52 match self {
53 Self::PixelIsArea => 1,
54 Self::PixelIsPoint => 2,
55 Self::Unknown(v) => *v,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Default)]
62pub struct HorizontalCrs {
63 pub projected_epsg: Option<u16>,
65 pub geodetic_epsg: Option<u16>,
67 pub projection_citation: Option<String>,
69 pub geodetic_citation: Option<String>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Default)]
75pub struct VerticalCrs {
76 pub epsg: Option<u16>,
78 pub datum: Option<u16>,
80 pub units: Option<u16>,
82 pub citation: Option<String>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum CrsKind {
89 Unspecified,
91 Horizontal {
93 model_type: ModelType,
94 horizontal: HorizontalCrs,
95 },
96 Vertical(VerticalCrs),
98 Compound {
100 model_type: ModelType,
101 horizontal: HorizontalCrs,
102 vertical: VerticalCrs,
103 },
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct CrsInfo {
109 pub model_type: u16,
111 pub raster_type: u16,
113 pub horizontal: Option<HorizontalCrs>,
115 pub vertical: Option<VerticalCrs>,
117}
118
119impl CrsInfo {
120 pub fn from_geokeys(geokeys: &GeoKeyDirectory) -> Self {
122 let model_type = geokeys.get_short(geokeys::GT_MODEL_TYPE).unwrap_or(0);
123 let horizontal = HorizontalCrs {
124 projected_epsg: geokeys.get_short(geokeys::PROJECTED_CRS_TYPE),
125 geodetic_epsg: geokeys.get_short(geokeys::GEODETIC_CRS_TYPE),
126 projection_citation: geokeys.get_ascii(geokeys::PROJ_CITATION).map(String::from),
127 geodetic_citation: geokeys
128 .get_ascii(geokeys::GEODETIC_CITATION)
129 .map(String::from),
130 };
131 let vertical = VerticalCrs {
132 epsg: geokeys.get_short(geokeys::VERTICAL_CS_TYPE),
133 datum: geokeys.get_short(geokeys::VERTICAL_DATUM),
134 units: geokeys.get_short(geokeys::VERTICAL_UNITS),
135 citation: geokeys
136 .get_ascii(geokeys::VERTICAL_CITATION)
137 .map(String::from),
138 };
139
140 Self {
141 model_type,
142 raster_type: geokeys.get_short(geokeys::GT_RASTER_TYPE).unwrap_or(1),
143 horizontal: horizontal.is_present(model_type).then_some(horizontal),
144 vertical: vertical.is_present().then_some(vertical),
145 }
146 }
147
148 pub fn epsg(&self) -> Option<u32> {
150 self.primary_horizontal_epsg()
151 .or(self.vertical_epsg())
152 .map(|epsg| epsg as u32)
153 }
154
155 pub fn raster_type_enum(&self) -> RasterType {
157 RasterType::from_code(self.raster_type)
158 }
159
160 pub fn model_type_enum(&self) -> ModelType {
162 ModelType::from_code(self.model_type)
163 }
164
165 pub fn crs_kind(&self) -> CrsKind {
167 match (&self.horizontal, &self.vertical) {
168 (Some(horizontal), Some(vertical)) => CrsKind::Compound {
169 model_type: self.model_type_enum(),
170 horizontal: horizontal.clone(),
171 vertical: vertical.clone(),
172 },
173 (Some(horizontal), None) => CrsKind::Horizontal {
174 model_type: self.model_type_enum(),
175 horizontal: horizontal.clone(),
176 },
177 (None, Some(vertical)) => CrsKind::Vertical(vertical.clone()),
178 (None, None) => CrsKind::Unspecified,
179 }
180 }
181
182 pub fn horizontal(&self) -> Option<&HorizontalCrs> {
184 self.horizontal.as_ref()
185 }
186
187 pub fn vertical(&self) -> Option<&VerticalCrs> {
189 self.vertical.as_ref()
190 }
191
192 pub fn projected_epsg(&self) -> Option<u16> {
194 self.horizontal
195 .as_ref()
196 .and_then(|horizontal| horizontal.projected_epsg)
197 }
198
199 pub fn geodetic_epsg(&self) -> Option<u16> {
201 self.horizontal
202 .as_ref()
203 .and_then(|horizontal| horizontal.geodetic_epsg)
204 }
205
206 pub fn geographic_epsg(&self) -> Option<u16> {
208 matches!(self.model_type_enum(), ModelType::Geographic)
209 .then(|| self.geodetic_epsg())
210 .flatten()
211 }
212
213 pub fn geocentric_epsg(&self) -> Option<u16> {
215 matches!(self.model_type_enum(), ModelType::Geocentric)
216 .then(|| self.geodetic_epsg())
217 .flatten()
218 }
219
220 pub fn vertical_epsg(&self) -> Option<u16> {
222 self.vertical.as_ref().and_then(|vertical| vertical.epsg)
223 }
224
225 pub fn projection_citation(&self) -> Option<&str> {
227 self.horizontal
228 .as_ref()
229 .and_then(|horizontal| horizontal.projection_citation.as_deref())
230 }
231
232 pub fn geodetic_citation(&self) -> Option<&str> {
234 self.horizontal
235 .as_ref()
236 .and_then(|horizontal| horizontal.geodetic_citation.as_deref())
237 }
238
239 pub fn vertical_citation(&self) -> Option<&str> {
241 self.vertical
242 .as_ref()
243 .and_then(|vertical| vertical.citation.as_deref())
244 }
245
246 pub fn vertical_datum(&self) -> Option<u16> {
248 self.vertical.as_ref().and_then(|vertical| vertical.datum)
249 }
250
251 pub fn vertical_units(&self) -> Option<u16> {
253 self.vertical.as_ref().and_then(|vertical| vertical.units)
254 }
255
256 pub fn apply_to_geokeys(&self, geokeys: &mut GeoKeyDirectory) {
258 set_optional_short(
259 geokeys,
260 geokeys::GT_MODEL_TYPE,
261 (self.model_type != 0).then_some(self.model_type),
262 );
263 set_optional_short(
264 geokeys,
265 geokeys::GT_RASTER_TYPE,
266 (self.raster_type != 0).then_some(self.raster_type),
267 );
268
269 if let Some(horizontal) = &self.horizontal {
270 set_optional_short(
271 geokeys,
272 geokeys::PROJECTED_CRS_TYPE,
273 horizontal.projected_epsg,
274 );
275 set_optional_short(
276 geokeys,
277 geokeys::GEODETIC_CRS_TYPE,
278 horizontal.geodetic_epsg,
279 );
280 set_optional_ascii(
281 geokeys,
282 geokeys::PROJ_CITATION,
283 horizontal.projection_citation.as_deref(),
284 );
285 set_optional_ascii(
286 geokeys,
287 geokeys::GEODETIC_CITATION,
288 horizontal.geodetic_citation.as_deref(),
289 );
290 } else {
291 clear_horizontal_geokeys(geokeys);
292 }
293
294 if let Some(vertical) = &self.vertical {
295 set_optional_short(geokeys, geokeys::VERTICAL_CS_TYPE, vertical.epsg);
296 set_optional_short(geokeys, geokeys::VERTICAL_DATUM, vertical.datum);
297 set_optional_short(geokeys, geokeys::VERTICAL_UNITS, vertical.units);
298 set_optional_ascii(
299 geokeys,
300 geokeys::VERTICAL_CITATION,
301 vertical.citation.as_deref(),
302 );
303 } else {
304 clear_vertical_geokeys(geokeys);
305 }
306 }
307
308 fn primary_horizontal_epsg(&self) -> Option<u16> {
309 let horizontal = self.horizontal.as_ref()?;
310 match self.model_type_enum() {
311 ModelType::Projected => horizontal.projected_epsg.or(horizontal.geodetic_epsg),
312 ModelType::Geographic | ModelType::Geocentric | ModelType::Unknown(_) => {
313 horizontal.geodetic_epsg.or(horizontal.projected_epsg)
314 }
315 }
316 }
317}
318
319impl HorizontalCrs {
320 fn is_present(&self, model_type: u16) -> bool {
321 model_type != 0
322 || self.projected_epsg.is_some()
323 || self.geodetic_epsg.is_some()
324 || self.projection_citation.is_some()
325 || self.geodetic_citation.is_some()
326 }
327}
328
329impl VerticalCrs {
330 fn is_present(&self) -> bool {
331 self.epsg.is_some()
332 || self.datum.is_some()
333 || self.units.is_some()
334 || self.citation.is_some()
335 }
336}
337
338fn set_optional_short(geokeys: &mut GeoKeyDirectory, id: u16, value: Option<u16>) {
339 if let Some(value) = value {
340 geokeys.set(id, GeoKeyValue::Short(value));
341 } else {
342 geokeys.remove(id);
343 }
344}
345
346fn set_optional_ascii(geokeys: &mut GeoKeyDirectory, id: u16, value: Option<&str>) {
347 if let Some(value) = value {
348 geokeys.set(id, GeoKeyValue::Ascii(value.to_string()));
349 } else {
350 geokeys.remove(id);
351 }
352}
353
354fn clear_horizontal_geokeys(geokeys: &mut GeoKeyDirectory) {
355 geokeys.remove(geokeys::PROJECTED_CRS_TYPE);
356 geokeys.remove(geokeys::GEODETIC_CRS_TYPE);
357 geokeys.remove(geokeys::PROJ_CITATION);
358 geokeys.remove(geokeys::GEODETIC_CITATION);
359}
360
361fn clear_vertical_geokeys(geokeys: &mut GeoKeyDirectory) {
362 geokeys.remove(geokeys::VERTICAL_CS_TYPE);
363 geokeys.remove(geokeys::VERTICAL_DATUM);
364 geokeys.remove(geokeys::VERTICAL_UNITS);
365 geokeys.remove(geokeys::VERTICAL_CITATION);
366}
367
368#[cfg(test)]
369mod tests {
370 use super::{CrsInfo, CrsKind, GeoKeyDirectory, GeoKeyValue, ModelType, RasterType};
371 use crate::geokeys;
372
373 #[test]
374 fn parses_geocentric_horizontal_crs() {
375 let mut geokeys = GeoKeyDirectory::new();
376 geokeys.set(
377 geokeys::GT_MODEL_TYPE,
378 GeoKeyValue::Short(ModelType::Geocentric.code()),
379 );
380 geokeys.set(
381 geokeys::GT_RASTER_TYPE,
382 GeoKeyValue::Short(RasterType::PixelIsArea.code()),
383 );
384 geokeys.set(geokeys::GEODETIC_CRS_TYPE, GeoKeyValue::Short(4978));
385
386 let crs = CrsInfo::from_geokeys(&geokeys);
387 assert_eq!(crs.geocentric_epsg(), Some(4978));
388 assert_eq!(crs.geographic_epsg(), None);
389 assert!(matches!(
390 crs.crs_kind(),
391 CrsKind::Horizontal {
392 model_type: ModelType::Geocentric,
393 ..
394 }
395 ));
396 }
397
398 #[test]
399 fn parses_compound_projected_vertical_crs() {
400 let mut geokeys = GeoKeyDirectory::new();
401 geokeys.set(
402 geokeys::GT_MODEL_TYPE,
403 GeoKeyValue::Short(ModelType::Projected.code()),
404 );
405 geokeys.set(geokeys::PROJECTED_CRS_TYPE, GeoKeyValue::Short(32616));
406 geokeys.set(geokeys::VERTICAL_CS_TYPE, GeoKeyValue::Short(5703));
407 geokeys.set(
408 geokeys::VERTICAL_CITATION,
409 GeoKeyValue::Ascii("NAVD88 height".into()),
410 );
411 geokeys.set(geokeys::VERTICAL_UNITS, GeoKeyValue::Short(9001));
412
413 let crs = CrsInfo::from_geokeys(&geokeys);
414 assert_eq!(crs.projected_epsg(), Some(32616));
415 assert_eq!(crs.vertical_epsg(), Some(5703));
416 assert_eq!(crs.vertical_units(), Some(9001));
417 assert_eq!(crs.vertical_citation(), Some("NAVD88 height"));
418 assert!(matches!(
419 crs.crs_kind(),
420 CrsKind::Compound {
421 model_type: ModelType::Projected,
422 ..
423 }
424 ));
425 }
426
427 #[test]
428 fn apply_to_geokeys_roundtrips_vertical_and_horizontal_components() {
429 let original = CrsInfo {
430 model_type: ModelType::Projected.code(),
431 raster_type: RasterType::PixelIsPoint.code(),
432 horizontal: Some(super::HorizontalCrs {
433 projected_epsg: Some(26916),
434 geodetic_epsg: Some(4269),
435 projection_citation: Some("NAD83 / UTM zone 16N".into()),
436 geodetic_citation: Some("NAD83".into()),
437 }),
438 vertical: Some(super::VerticalCrs {
439 epsg: Some(5703),
440 datum: Some(5103),
441 units: Some(9001),
442 citation: Some("NAVD88 height".into()),
443 }),
444 };
445
446 let mut geokeys = GeoKeyDirectory::new();
447 original.apply_to_geokeys(&mut geokeys);
448 let roundtrip = CrsInfo::from_geokeys(&geokeys);
449
450 assert_eq!(roundtrip, original);
451 assert_eq!(roundtrip.epsg(), Some(26916));
452 }
453}