1#![warn(missing_docs)]
4#![allow(dead_code)]
5
6use 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 x_range: RangeInclusive<f64>,
19 y_range: RangeInclusive<f64>,
21}
22
23#[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 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 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 pub fn from_epsg(code: NonZero<usize>) -> Result<Self, MyError> {
73 Self::new(&format!("EPSG:{code}"))
74 }
75
76 pub fn check_point(&self, coord: &[f64]) -> Result<(), MyError> {
78 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 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 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}