Skip to main content

epsg_utils/
lib.rs

1//! Utilities for working with EPSG coordinate reference system definitions.
2//!
3//! This crate provides three main capabilities:
4//!
5//! 1. **EPSG lookup** -- look up the WKT2 or PROJJSON representation of a CRS
6//!    by its EPSG code (via [`epsg_to_wkt2`] and [`epsg_to_projjson`]).
7//! 2. **Parsing** -- parse OGC WKT2 strings ([`parse_wkt2`]) or PROJJSON
8//!    strings ([`parse_projjson`]) into structured Rust types.
9//! 3. **Conversion** -- convert between WKT2 and PROJJSON using
10//!    [`Crs::to_wkt2`] and [`Crs::to_projjson`].
11//!
12//! # Crate structure
13//!
14//! The top-level [`Crs`] enum is the entry point for all parsed CRS data. It
15//! dispatches to one of the concrete CRS types:
16//!
17//! - [`ProjectedCrs`] -- a projected CRS (`PROJCRS`)
18//! - [`GeogCrs`] -- a geographic CRS (`GEOGCRS`)
19//! - [`GeodCrs`] -- a geodetic CRS (`GEODCRS`)
20//! - [`VertCrs`] -- a vertical CRS (`VERTCRS`)
21//! - [`CompoundCrs`] -- a compound CRS (`COMPOUNDCRS`)
22//!
23//! These types and their components (datums, coordinate systems, ellipsoids,
24//! etc.) live in the [`crs`] module and are all publicly accessible.
25//!
26//! # Features
27//!
28//! - **`wkt2-definitions`** (enabled by default) -- embeds compressed WKT2
29//!   strings for all supported EPSG codes, enabling [`epsg_to_wkt2`].
30//! - **`projjson-definitions`** (enabled by default) -- embeds compressed
31//!   PROJJSON strings for all supported EPSG codes, enabling [`epsg_to_projjson`].
32//!
33//! # Examples
34//!
35//! ## Look up an EPSG code
36//!
37//! ```
38//! # #[cfg(feature = "wkt2-definitions")]
39//! # {
40//! let wkt = epsg_utils::epsg_to_wkt2(6678).unwrap();
41//! # }
42//! # #[cfg(feature = "projjson-definitions")]
43//! # {
44//! let projjson = epsg_utils::epsg_to_projjson(6678).unwrap();
45//! # }
46//! ```
47//!
48//! ## Parse WKT2
49//!
50//! ```
51//! let crs = epsg_utils::parse_wkt2(r#"PROJCRS["WGS 84 / UTM zone 31N",
52//!     BASEGEOGCRS["WGS 84", DATUM["World Geodetic System 1984",
53//!         ELLIPSOID["WGS 84", 6378137, 298.257223563]]],
54//!     CONVERSION["UTM zone 31N", METHOD["Transverse Mercator"]],
55//!     CS[Cartesian, 2],
56//!     ID["EPSG", 32631]]"#).unwrap();
57//!
58//! assert_eq!(crs.to_epsg(), Some(32631));
59//! ```
60//!
61//! ## Parse PROJJSON
62//!
63//! ```
64//! # #[cfg(feature = "projjson-definitions")]
65//! # {
66//! let projjson = epsg_utils::epsg_to_projjson(6678).unwrap();
67//! let crs = epsg_utils::parse_projjson(projjson).unwrap();
68//! assert_eq!(crs.name, "JGD2011 / Japan Plane Rectangular CS X");
69//! # }
70//! ```
71//!
72//! ## Convert between WKT2 and PROJJSON
73//!
74//! ```
75//! # #[cfg(feature = "wkt2-definitions")]
76//! # {
77//! # let wkt = epsg_utils::epsg_to_wkt2(6678).unwrap();
78//! let crs = epsg_utils::parse_wkt2(wkt).unwrap();
79//!
80//! // To PROJJSON (serde_json::Value)
81//! let projjson_value = crs.to_projjson();
82//!
83//! // Back to WKT2
84//! let wkt2 = crs.to_wkt2();
85//! # }
86//! ```
87//!
88//! # EPSG Dataset
89//!
90//! The definitions in this crate are based on the EPSG Dataset v12.054, and
91//! cover 99.5% (7365/7396) of the EPSG codes (engineering CRS and derived
92//! projected CRS are not supported).
93//!
94//! The EPSG Dataset is owned by the [International Association of Oil & Gas
95//! Producers (IOGP)](https://www.iogp.org/). The source definitions included
96//! in this crate were downloaded from <https://epsg.org/download-dataset.html>.
97
98#[cfg(any(feature = "wkt2-definitions", feature = "projjson-definitions"))]
99mod chunked_definitions;
100pub mod crs;
101mod error;
102mod projjson;
103#[cfg(feature = "projjson-definitions")]
104mod projjson_definitions;
105mod wkt2;
106#[cfg(feature = "wkt2-definitions")]
107mod wkt2_definitions;
108
109pub use crs::{CompoundCrs, Crs, GeodCrs, GeogCrs, ProjectedCrs, VertCrs};
110pub use error::ParseError;
111
112/// Parse a WKT2 string into a [`Crs`].
113pub fn parse_wkt2(input: &str) -> Result<Crs, ParseError> {
114    wkt2::Parser::new(input).parse_crs()
115}
116
117/// Parse a PROJJSON string into a [`ProjectedCrs`].
118pub fn parse_projjson(input: &str) -> Result<ProjectedCrs, ParseError> {
119    projjson::reader::parse_projjson(input)
120}
121
122/// Look up the WKT2 string for an EPSG projected CRS code.
123///
124/// Returns the static WKT2 string, or an error if the code is not found.
125#[cfg(feature = "wkt2-definitions")]
126pub fn epsg_to_wkt2(code: i32) -> Result<&'static str, ParseError> {
127    wkt2_definitions::lookup(code).ok_or(ParseError::UnknownEpsgCode { code })
128}
129
130/// Look up the PROJJSON string for an EPSG projected CRS code.
131///
132/// Returns the static PROJJSON string, or an error if the code is not found.
133#[cfg(feature = "projjson-definitions")]
134pub fn epsg_to_projjson(code: i32) -> Result<&'static str, ParseError> {
135    projjson_definitions::lookup(code).ok_or(ParseError::UnknownEpsgCode { code })
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn to_epsg_found() {
144        let crs = parse_wkt2(
145            r#"PROJCRS["test",
146                BASEGEOGCRS["x",DATUM["d",ELLIPSOID["e",6378137,298.257]]],
147                CONVERSION["y",METHOD["m"]],
148                CS[Cartesian,2],
149                ID["EPSG",32631]]"#,
150        )
151        .unwrap();
152        assert_eq!(crs.to_epsg(), Some(32631));
153    }
154
155    #[test]
156    fn to_epsg_not_found() {
157        let crs = parse_wkt2(
158            r#"PROJCRS["test",
159                BASEGEOGCRS["x",DATUM["d",ELLIPSOID["e",6378137,298.257]]],
160                CONVERSION["y",METHOD["m"]],
161                CS[Cartesian,2]]"#,
162        )
163        .unwrap();
164        assert_eq!(crs.to_epsg(), None);
165    }
166
167    #[test]
168    #[cfg(feature = "wkt2-definitions")]
169    fn epsg_to_wkt2_6678() {
170        let wkt = epsg_to_wkt2(6678).unwrap();
171        assert!(wkt.starts_with("PROJCRS["));
172        let Crs::ProjectedCrs(crs) = parse_wkt2(wkt).unwrap() else {
173            panic!("expected ProjectedCrs");
174        };
175        assert_eq!(crs.name, "JGD2011 / Japan Plane Rectangular CS X");
176    }
177
178    #[test]
179    #[cfg(feature = "projjson-definitions")]
180    fn epsg_to_projjson_6678() {
181        let json = epsg_to_projjson(6678).unwrap();
182        assert!(json.contains("\"ProjectedCRS\""));
183        let crs = parse_projjson(json).unwrap();
184        assert_eq!(crs.name, "JGD2011 / Japan Plane Rectangular CS X");
185    }
186
187    // -----------------------------------------------------------------------
188    // Lookup tests covering every CRS type and chunk boundaries
189    // -----------------------------------------------------------------------
190
191    /// EPSG:4326 -- GEOGCRS (WGS 84), the most widely used geographic CRS.
192    #[test]
193    #[cfg(feature = "wkt2-definitions")]
194    fn lookup_geogcrs_4326() {
195        let wkt = epsg_to_wkt2(4326).unwrap();
196        let Crs::GeogCrs(crs) = parse_wkt2(wkt).unwrap() else {
197            panic!("expected GeogCrs");
198        };
199        assert_eq!(crs.name, "WGS 84");
200        assert_eq!(crs.to_epsg(), Some(4326));
201    }
202
203    /// EPSG:4978 -- GEODCRS (WGS 84 geocentric).
204    #[test]
205    #[cfg(feature = "wkt2-definitions")]
206    fn lookup_geodcrs_4978() {
207        let wkt = epsg_to_wkt2(4978).unwrap();
208        let Crs::GeodCrs(crs) = parse_wkt2(wkt).unwrap() else {
209            panic!("expected GeodCrs");
210        };
211        assert_eq!(crs.name, "WGS 84");
212        assert_eq!(crs.to_epsg(), Some(4978));
213    }
214
215    /// EPSG:5714 -- VERTCRS (MSL height).
216    #[test]
217    #[cfg(feature = "wkt2-definitions")]
218    fn lookup_vertcrs_5714() {
219        let wkt = epsg_to_wkt2(5714).unwrap();
220        let Crs::VertCrs(crs) = parse_wkt2(wkt).unwrap() else {
221            panic!("expected VertCrs");
222        };
223        assert_eq!(crs.name, "MSL height");
224        assert_eq!(crs.to_epsg(), Some(5714));
225    }
226
227    /// EPSG:10364 -- derived VERTCRS (Cascais depth, uses BASEVERTCRS).
228    #[test]
229    #[cfg(feature = "wkt2-definitions")]
230    fn lookup_derived_vertcrs_10364() {
231        let wkt = epsg_to_wkt2(10364).unwrap();
232        let Crs::VertCrs(crs) = parse_wkt2(wkt).unwrap() else {
233            panic!("expected VertCrs");
234        };
235        assert_eq!(crs.name, "Cascais depth");
236        assert!(matches!(crs.source, crs::VertCrsSource::Derived { .. }));
237    }
238
239    /// EPSG:9518 -- COMPOUNDCRS (WGS 84 + EGM2008 height).
240    #[test]
241    #[cfg(feature = "wkt2-definitions")]
242    fn lookup_compoundcrs_9518() {
243        let wkt = epsg_to_wkt2(9518).unwrap();
244        let Crs::CompoundCrs(crs) = parse_wkt2(wkt).unwrap() else {
245            panic!("expected CompoundCrs");
246        };
247        assert_eq!(crs.name, "WGS 84 + EGM2008 height");
248        assert_eq!(crs.to_epsg(), Some(9518));
249    }
250
251    /// EPSG:32631 -- PROJCRS (WGS 84 / UTM zone 31N).
252    #[test]
253    #[cfg(feature = "wkt2-definitions")]
254    fn lookup_projcrs_32631() {
255        let wkt = epsg_to_wkt2(32631).unwrap();
256        let Crs::ProjectedCrs(crs) = parse_wkt2(wkt).unwrap() else {
257            panic!("expected ProjectedCrs");
258        };
259        assert_eq!(crs.name, "WGS 84 / UTM zone 31N");
260        assert_eq!(crs.to_epsg(), Some(32631));
261    }
262
263    /// EPSG:2000 -- first code in the dataset (boundary test).
264    #[test]
265    #[cfg(feature = "wkt2-definitions")]
266    fn lookup_first_code_2000() {
267        let wkt = epsg_to_wkt2(2000).unwrap();
268        let crs = parse_wkt2(wkt).unwrap();
269        assert_eq!(crs.to_epsg(), Some(2000));
270    }
271
272    /// EPSG:32766 -- last code in the dataset (boundary test).
273    #[test]
274    #[cfg(feature = "wkt2-definitions")]
275    fn lookup_last_code_32766() {
276        let wkt = epsg_to_wkt2(32766).unwrap();
277        let crs = parse_wkt2(wkt).unwrap();
278        assert_eq!(crs.to_epsg(), Some(32766));
279    }
280
281    /// Verify that WKT2 and PROJJSON lookups agree on the CRS name
282    /// (for projected CRSs, since `parse_projjson` currently only supports those).
283    #[test]
284    #[cfg(all(feature = "wkt2-definitions", feature = "projjson-definitions"))]
285    fn wkt2_and_projjson_agree() {
286        for code in [2000, 6678, 32631] {
287            let wkt = epsg_to_wkt2(code).unwrap();
288            let json = epsg_to_projjson(code).unwrap();
289            let wkt_crs = parse_wkt2(wkt).unwrap();
290            let json_crs = parse_projjson(json).unwrap();
291            assert_eq!(
292                wkt_crs.to_epsg(),
293                Some(code),
294                "WKT2 EPSG mismatch for {code}"
295            );
296            assert_eq!(
297                json_crs.to_epsg(),
298                Some(code),
299                "PROJJSON EPSG mismatch for {code}"
300            );
301        }
302    }
303
304    #[test]
305    #[cfg(feature = "wkt2-definitions")]
306    fn epsg_to_wkt2_unknown() {
307        assert!(matches!(
308            epsg_to_wkt2(99999),
309            Err(ParseError::UnknownEpsgCode { code: 99999 })
310        ));
311    }
312
313    #[test]
314    #[cfg(feature = "projjson-definitions")]
315    fn epsg_to_projjson_unknown() {
316        assert!(matches!(
317            epsg_to_projjson(99999),
318            Err(ParseError::UnknownEpsgCode { code: 99999 })
319        ));
320    }
321}