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}