Skip to main content

geometry_proj/
crs.rs

1//! A coordinate reference system, and construction of one from the
2//! usual textual sources.
3//!
4//! Wraps [`proj4rs::Proj`]. A CRS is built from a proj4 string, an EPSG
5//! code, or a WKT definition (parsed to a proj string by
6//! [`proj4wkt`]). There is no Boost.Geometry counterpart — Boost defers
7//! projections to its unsupported `extensions/gis/projections/`; the
8//! Rust port fills the gap with the pure-Rust `proj4rs` engine.
9
10use proj4rs::Proj;
11
12/// A coordinate reference system the reprojection functions transform
13/// between.
14///
15/// Thin newtype over [`proj4rs::Proj`]; construct it from a proj4
16/// string ([`Crs::from_proj_string`]), an EPSG code
17/// ([`Crs::from_epsg`]), or a WKT definition ([`Crs::from_wkt`]).
18pub struct Crs {
19    proj: Proj,
20}
21
22/// Failure to build a [`Crs`] from a textual definition.
23#[derive(Debug)]
24pub enum CrsError {
25    /// The proj4 / EPSG definition was rejected by `proj4rs`.
26    Proj(proj4rs::errors::Error),
27    /// The WKT string could not be parsed to a proj definition.
28    Wkt(alloc::string::String),
29}
30
31impl core::fmt::Display for CrsError {
32    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33        match self {
34            CrsError::Proj(e) => write!(f, "invalid CRS definition: {e}"),
35            CrsError::Wkt(m) => write!(f, "invalid WKT: {m}"),
36        }
37    }
38}
39
40impl std::error::Error for CrsError {}
41
42impl Crs {
43    /// Build a CRS from a proj4 definition string, e.g.
44    /// `"+proj=utm +zone=32 +datum=WGS84"`.
45    ///
46    /// # Errors
47    ///
48    /// [`CrsError::Proj`] if `proj4rs` rejects the string.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use geometry_proj::Crs;
54    /// let wgs84 = Crs::from_proj_string("+proj=longlat +datum=WGS84 +no_defs").unwrap();
55    /// # let _ = wgs84;
56    /// ```
57    pub fn from_proj_string(s: &str) -> Result<Self, CrsError> {
58        Proj::from_proj_string(s)
59            .map(|proj| Self { proj })
60            .map_err(CrsError::Proj)
61    }
62
63    /// Build a CRS from an EPSG code, e.g. `4326` (WGS84 lon/lat) or
64    /// `3857` (Web Mercator).
65    ///
66    /// # Errors
67    ///
68    /// [`CrsError::Proj`] if the code is unknown to `proj4rs`.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use geometry_proj::Crs;
74    /// let web_mercator = Crs::from_epsg(3857).unwrap();
75    /// # let _ = web_mercator;
76    /// ```
77    pub fn from_epsg(code: u16) -> Result<Self, CrsError> {
78        Proj::from_epsg_code(code)
79            .map(|proj| Self { proj })
80            .map_err(CrsError::Proj)
81    }
82
83    /// Build a CRS from a WKT definition, parsed to a proj string by
84    /// [`proj4wkt`].
85    ///
86    /// # Errors
87    ///
88    /// [`CrsError::Wkt`] if the WKT cannot be parsed, or
89    /// [`CrsError::Proj`] if the resulting proj string is invalid.
90    pub fn from_wkt(wkt: &str) -> Result<Self, CrsError> {
91        let projstr =
92            proj4wkt::wkt_to_projstring(wkt).map_err(|e| CrsError::Wkt(alloc::format!("{e:?}")))?;
93        Self::from_proj_string(&projstr)
94    }
95
96    /// Borrow the underlying `proj4rs` projection.
97    #[must_use]
98    pub(crate) fn proj(&self) -> &Proj {
99        &self.proj
100    }
101
102    /// Whether this CRS is geographic (lon/lat), whose coordinates are
103    /// carried in **radians** by `proj4rs`.
104    #[must_use]
105    pub fn is_geographic(&self) -> bool {
106        self.proj.is_latlong()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::Crs;
113
114    #[test]
115    fn build_from_epsg() {
116        assert!(Crs::from_epsg(4326).is_ok());
117        assert!(Crs::from_epsg(3857).is_ok());
118    }
119
120    #[test]
121    fn build_from_proj_string() {
122        assert!(Crs::from_proj_string("+proj=longlat +datum=WGS84 +no_defs").is_ok());
123    }
124
125    #[test]
126    fn wgs84_is_geographic() {
127        let wgs84 = Crs::from_epsg(4326).unwrap();
128        assert!(wgs84.is_geographic());
129        let mercator = Crs::from_epsg(3857).unwrap();
130        assert!(!mercator.is_geographic());
131    }
132
133    #[test]
134    fn bad_epsg_errors() {
135        assert!(Crs::from_epsg(1).is_err());
136    }
137
138    /// A WGS84 `GEOGCS[...]` WKT builds a geographic CRS, same as the
139    /// EPSG:4326 path.
140    #[test]
141    fn build_from_wkt_geographic() {
142        let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#;
143        let crs = Crs::from_wkt(wkt).unwrap();
144        assert!(crs.is_geographic());
145    }
146
147    /// Malformed WKT surfaces as `CrsError::Wkt`, and both error
148    /// variants render through `Display`.
149    #[test]
150    fn error_variants_display() {
151        let wkt_err = Crs::from_wkt("NOT WKT AT ALL")
152            .err()
153            .expect("malformed WKT is rejected");
154        let msg = alloc::format!("{wkt_err}");
155        assert!(msg.starts_with("invalid WKT:"), "got: {msg}");
156
157        let proj_err = Crs::from_proj_string("+proj=definitely_not_a_projection")
158            .err()
159            .expect("bad projection string is rejected");
160        let msg = alloc::format!("{proj_err}");
161        assert!(msg.starts_with("invalid CRS definition:"), "got: {msg}");
162    }
163}