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}