Skip to main content

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