ogc_cql2/
crs.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4#![allow(dead_code)]
5
6//! Coordinate Reference System (CRS) types and traits in this library.
7//!
8
9use crate::{MyError, config::config};
10use core::fmt;
11use proj::Proj;
12use tracing::info;
13
14#[derive(Debug)]
15struct EoV {
16    /// horizontal or longitudinal extent bounds.
17    x_range: (f64, f64),
18    /// vertical or latitudinal extent bounds.
19    y_range: (f64, f64),
20}
21
22/// Representation of a Coordinate Reference System
23#[derive(Debug)]
24#[allow(clippy::upper_case_acronyms)]
25pub struct CRS {
26    definition: String,
27    inner: Proj,
28    extent_of_validity: EoV,
29}
30
31// FIXME (rsn) 20250807 - add LRU cache to store instances + reduce duplication.
32
33impl fmt::Display for CRS {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(f, "{}", self.definition)
36    }
37}
38
39impl Default for CRS {
40    fn default() -> Self {
41        let default_crs = config().default_crs();
42        info!("Will try using '{default_crs}' as the default CRS...");
43        Self::new(default_crs).expect("Failed instantiating default CRS")
44    }
45}
46
47impl CRS {
48    /// Try constructing a new instance and ensure that CRS has a non-trivial
49    /// extent of validity which will be later used to validate geometry
50    /// coordinates.
51    pub fn new(code: &str) -> Result<Self, MyError> {
52        let inner = Proj::new(code)?;
53        let definition = code.into();
54        let (mb_eov, _mb_def) = inner.area_of_use()?;
55        // tracing::trace!("area-of-use for '{definition}' = {mb_eov:?}, '{_mb_def:?}'");
56        // for now reject input w/ no known validity-extent bounds...
57        let eov = mb_eov.expect("CRSes w/ no known Area-of-Use are not supported. Abort");
58        let extent_of_validity = EoV {
59            x_range: (eov.west, eov.east),
60            y_range: (eov.south, eov.north),
61        };
62        let crs = CRS {
63            definition,
64            inner,
65            extent_of_validity,
66        };
67
68        // FIXME - cache it...
69
70        Ok(crs)
71    }
72
73    /// Check if the given point coordinates are w/in the area-of-validity of this.
74    pub fn check_point(&self, coord: &[f64]) -> Result<(), MyError> {
75        // FIXME (rsn) 2250807 - so far we only handle 2D coordinates...
76        let (x, y) = (&coord[0], &coord[1]);
77        let (x_min, x_max) = self.extent_of_validity.x_range;
78        let x_ok = *x >= x_min && *x <= x_max;
79        if !x_ok {
80            return Err(MyError::Runtime(
81                "Point x (longitude) coordinate is out-of-bounds".into(),
82            ));
83        }
84        let (y_min, y_max) = self.extent_of_validity.y_range;
85        let y_ok = *y >= y_min && *y <= y_max;
86        if !y_ok {
87            return Err(MyError::Runtime(
88                "Point y (latitude) coordinate is out-of-bounds".into(),
89            ));
90        }
91        Ok(())
92    }
93
94    /// Check if the given line coordinates are w/in the area-of-validity of this.
95    pub fn check_line(&self, coord: &[Vec<f64>]) -> Result<(), MyError> {
96        if coord.iter().all(|xy| self.check_point(xy).is_ok()) {
97            Ok(())
98        } else {
99            Err(MyError::Runtime(
100                "One or more line coordinates are out-of-bounds".into(),
101            ))
102        }
103    }
104
105    /// Check if the given polygon coordinates are w/in the area-of-validity of this.
106    pub fn check_polygon(&self, rings: &[Vec<Vec<f64>>]) -> Result<(), MyError> {
107        if rings.iter().all(|r| self.check_line(r).is_ok()) {
108            Ok(())
109        } else {
110            Err(MyError::Runtime(
111                "One or more polygon coordinates are out-of-bounds".into(),
112            ))
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use proj::Proj;
120    use tracing_test::traced_test;
121
122    #[test]
123    #[traced_test]
124    fn test_name() {
125        let epsg_4326 = Proj::new("EPSG:4326").unwrap();
126        // tracing::debug!("EPSG:4326 = {epsg_4326:?}");
127
128        let (aou, _) = epsg_4326.area_of_use().unwrap();
129        // tracing::debug!("EPSG:4326 area of use = {aou:?}");
130
131        if let Some(a) = aou {
132            assert_eq!(a.west, -180.0);
133            assert_eq!(a.east, 180.0);
134            assert_eq!(a.south, -90.0);
135            assert_eq!(a.north, 90.0);
136        } else {
137            panic!("Failed getting Area of Use for EPSG:4326")
138        }
139    }
140}