Skip to main content

proj_wkt/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Parser for WKT and PROJ format CRS strings.
4//!
5//! Converts CRS definition strings into [`proj_core::CrsDef`] values that can
6//! be used with [`proj_core::Transform::from_crs_defs()`].
7//!
8//! # Supported formats
9//!
10//! - **Authority codes**: `"EPSG:4326"` — delegates to proj-core's registry
11//! - **PROJ strings**: `"+proj=utm +zone=18 +datum=WGS84"` — parsed into CrsDef
12//! - **WKT1**: `GEOGCS[...]` / `PROJCS[...]` — extracts AUTHORITY tag when present,
13//!   otherwise parses projection parameters
14//!
15//! # Example
16//!
17//! ```
18//! use proj_wkt::parse_crs;
19//! use proj_core::Transform;
20//!
21//! let from = parse_crs("+proj=longlat +datum=WGS84").unwrap();
22//! let to = parse_crs("EPSG:3857").unwrap();
23//! let t = Transform::from_crs_defs(&from, &to).unwrap();
24//! let (x, y) = t.convert((-74.006, 40.7128)).unwrap();
25//! ```
26
27mod proj_string;
28mod projjson;
29mod wkt;
30
31use proj_core::{Bounds, Coord, Coord3D, CrsDef, Transform, Transformable, Transformable3D};
32
33/// Parse error.
34#[derive(Debug, thiserror::Error)]
35pub enum ParseError {
36    #[error("failed to parse CRS string: {0}")]
37    Parse(String),
38    #[error(transparent)]
39    Core(#[from] proj_core::Error),
40}
41
42pub type Result<T> = std::result::Result<T, ParseError>;
43
44/// Parse a CRS definition string in any supported format.
45///
46/// Automatically detects and handles:
47/// - **Authority codes**: `"EPSG:4326"`
48/// - **Bare EPSG codes**: `"4326"` (numeric-only strings)
49/// - **URN format**: `"urn:ogc:def:crs:EPSG::4326"`
50/// - **OGC CRS84**: `"CRS:84"`, `"OGC:CRS84"`
51/// - **PROJ strings**: `"+proj=utm +zone=18 +datum=WGS84"`
52/// - **PROJJSON**: `{"type": "ProjectedCRS", ...}`
53/// - **WKT1**: `GEOGCS[...]` / `PROJCS[...]`
54/// - **WKT2**: `GEODCRS[...]` / `PROJCRS[...]`
55pub fn parse_crs(s: &str) -> Result<CrsDef> {
56    let s = s.trim();
57
58    // Normalize common aliases
59    let upper = s.to_uppercase();
60    if upper == "CRS:84" || upper == "OGC:CRS84" {
61        return proj_core::lookup_epsg(4326)
62            .ok_or_else(|| ParseError::Parse("CRS:84 not found in registry".into()));
63    }
64
65    // URN format: urn:ogc:def:crs:EPSG::4326
66    if upper.starts_with("URN:OGC:DEF:CRS:") {
67        let parts: Vec<&str> = s.split(':').collect();
68        // Format: urn:ogc:def:crs:AUTHORITY::CODE or urn:ogc:def:crs:AUTHORITY:VERSION:CODE
69        if parts.len() >= 7 {
70            let code_str = parts.last().unwrap_or(&"");
71            if let Ok(code) = code_str.parse::<u32>() {
72                return proj_core::lookup_epsg(code)
73                    .ok_or_else(|| ParseError::Parse(format!("unknown EPSG code in URN: {code}")));
74            }
75        }
76        return Err(ParseError::Parse(format!("invalid URN format: {s}")));
77    }
78
79    // Try authority code (EPSG:XXXX)
80    if s.contains(':')
81        && !s.starts_with('+')
82        && !upper.starts_with("GEOG")
83        && !upper.starts_with("PROJ")
84    {
85        if let Ok(crs) = proj_core::lookup_authority_code(s) {
86            return Ok(crs);
87        }
88    }
89
90    // Bare numeric EPSG code (e.g., "4326")
91    if let Ok(code) = s.parse::<u32>() {
92        if let Some(crs) = proj_core::lookup_epsg(code) {
93            return Ok(crs);
94        }
95    }
96
97    // PROJ string
98    if s.starts_with('+') {
99        return proj_string::parse_proj_string(s);
100    }
101
102    // PROJJSON
103    if s.starts_with('{') {
104        return projjson::parse_projjson(s);
105    }
106
107    // WKT
108    if upper.starts_with("GEOGCS")
109        || upper.starts_with("PROJCS")
110        || upper.starts_with("GEODCRS")
111        || upper.starts_with("GEOGCRS")
112        || upper.starts_with("PROJCRS")
113    {
114        return wkt::parse_wkt(s);
115    }
116
117    Err(ParseError::Parse(format!(
118        "unrecognized CRS format: {:.80}",
119        s
120    )))
121}
122
123/// Create a [`Transform`] from two CRS strings in any format.
124///
125/// Convenience function for downstream projects that need to handle free-form CRS strings.
126pub fn transform_from_crs_strings(
127    from: &str,
128    to: &str,
129) -> std::result::Result<proj_core::Transform, ParseError> {
130    let from_crs = parse_crs(from)?;
131    let to_crs = parse_crs(to)?;
132    Ok(proj_core::Transform::from_crs_defs(&from_crs, &to_crs)?)
133}
134
135/// Lightweight compatibility facade for downstream code that currently expects
136/// a `proj::Proj`-like flow:
137/// 1. parse a CRS definition with [`Proj::new`]
138/// 2. build a CRS-to-CRS transform with [`Proj::create_crs_to_crs_from_pj`]
139/// 3. convert coordinates with [`Proj::convert`]
140pub struct Proj {
141    inner: ProjInner,
142}
143
144enum ProjInner {
145    Definition(CrsDef),
146    Transform(Box<Transform>),
147}
148
149impl Proj {
150    /// Parse a single CRS definition in any supported format.
151    pub fn new(definition: &str) -> Result<Self> {
152        Ok(Self {
153            inner: ProjInner::Definition(parse_crs(definition)?),
154        })
155    }
156
157    /// Build a transform directly from two CRS strings.
158    pub fn new_known_crs(from: &str, to: &str, _area: Option<&str>) -> Result<Self> {
159        Ok(Self {
160            inner: ProjInner::Transform(Box::new(transform_from_crs_strings(from, to)?)),
161        })
162    }
163
164    /// Build a transform from two parsed CRS definitions.
165    pub fn create_crs_to_crs_from_pj(
166        &self,
167        target: &Self,
168        _area: Option<&str>,
169        _options: Option<&str>,
170    ) -> Result<Self> {
171        let source = self.definition()?;
172        let target = target.definition()?;
173        Ok(Self {
174            inner: ProjInner::Transform(Box::new(Transform::from_crs_defs(source, target)?)),
175        })
176    }
177
178    /// Transform a coordinate using a CRS-to-CRS transform.
179    pub fn convert<T: Transformable>(&self, coord: T) -> proj_core::Result<T> {
180        match &self.inner {
181            ProjInner::Transform(transform) => transform.convert(coord),
182            ProjInner::Definition(_) => Err(proj_core::Error::InvalidDefinition(
183                "coordinate conversion requires a CRS-to-CRS transform, not a standalone CRS definition".into(),
184            )),
185        }
186    }
187
188    /// Transform a 3D coordinate using a CRS-to-CRS transform.
189    pub fn convert_3d<T: Transformable3D>(&self, coord: T) -> proj_core::Result<T> {
190        match &self.inner {
191            ProjInner::Transform(transform) => transform.convert_3d(coord),
192            ProjInner::Definition(_) => Err(proj_core::Error::InvalidDefinition(
193                "coordinate conversion requires a CRS-to-CRS transform, not a standalone CRS definition".into(),
194            )),
195        }
196    }
197
198    /// Transform a coordinate using the native [`Coord`] type.
199    pub fn convert_coord(&self, coord: Coord) -> proj_core::Result<Coord> {
200        self.convert(coord)
201    }
202
203    /// Transform a 3D coordinate using the native [`Coord3D`] type.
204    pub fn convert_coord_3d(&self, coord: Coord3D) -> proj_core::Result<Coord3D> {
205        self.convert_3d(coord)
206    }
207
208    /// Return the inverse of a CRS-to-CRS transform.
209    pub fn inverse(&self) -> Result<Self> {
210        match &self.inner {
211            ProjInner::Transform(transform) => Ok(Self {
212                inner: ProjInner::Transform(Box::new(transform.inverse()?)),
213            }),
214            ProjInner::Definition(_) => Err(ParseError::Parse(
215                "inverse requires a CRS-to-CRS transform, not a standalone CRS definition".into(),
216            )),
217        }
218    }
219
220    /// Reproject an axis-aligned bounding box by sampling its perimeter.
221    pub fn transform_bounds(
222        &self,
223        bounds: Bounds,
224        densify_points: usize,
225    ) -> proj_core::Result<Bounds> {
226        match &self.inner {
227            ProjInner::Transform(transform) => transform.transform_bounds(bounds, densify_points),
228            ProjInner::Definition(_) => Err(proj_core::Error::InvalidDefinition(
229                "bounds reprojection requires a CRS-to-CRS transform, not a standalone CRS definition".into(),
230            )),
231        }
232    }
233
234    fn definition(&self) -> Result<&CrsDef> {
235        match &self.inner {
236            ProjInner::Definition(crs) => Ok(crs),
237            ProjInner::Transform(_) => Err(ParseError::Parse(
238                "expected a CRS definition, found a transform".into(),
239            )),
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn bare_epsg_code() {
250        let crs = parse_crs("4326").unwrap();
251        assert!(crs.is_geographic());
252        assert_eq!(crs.epsg(), 4326);
253    }
254
255    #[test]
256    fn bare_epsg_projected() {
257        let crs = parse_crs("32618").unwrap();
258        assert!(crs.is_projected());
259    }
260
261    #[test]
262    fn urn_format() {
263        let crs = parse_crs("urn:ogc:def:crs:EPSG::4326").unwrap();
264        assert!(crs.is_geographic());
265        assert_eq!(crs.epsg(), 4326);
266    }
267
268    #[test]
269    fn urn_with_version() {
270        let crs = parse_crs("urn:ogc:def:crs:EPSG:9.8.15:3857").unwrap();
271        assert!(crs.is_projected());
272    }
273
274    #[test]
275    fn crs84() {
276        let crs = parse_crs("CRS:84").unwrap();
277        assert!(crs.is_geographic());
278    }
279
280    #[test]
281    fn ogc_crs84() {
282        let crs = parse_crs("OGC:CRS84").unwrap();
283        assert!(crs.is_geographic());
284    }
285
286    #[test]
287    fn epsg_authority_code() {
288        let crs = parse_crs("EPSG:3857").unwrap();
289        assert!(crs.is_projected());
290    }
291
292    #[test]
293    fn unsupported_format_error() {
294        assert!(parse_crs("not a crs").is_err());
295    }
296
297    #[test]
298    fn transform_from_strings() {
299        let t = transform_from_crs_strings("EPSG:4326", "EPSG:3857").unwrap();
300        let (x, _y) = t.convert((-74.006, 40.7128)).unwrap();
301        assert!((x - (-8238310.0)).abs() < 100.0);
302    }
303
304    #[test]
305    fn transform_bare_to_authority() {
306        let t = transform_from_crs_strings("4326", "EPSG:3857").unwrap();
307        let (x, _y) = t.convert((-74.006, 40.7128)).unwrap();
308        assert!((x - (-8238310.0)).abs() < 100.0);
309    }
310
311    #[test]
312    fn proj_facade_from_known_crs() {
313        let proj = Proj::new_known_crs("EPSG:4326", "EPSG:3857", None).unwrap();
314        let (x, _y) = proj.convert((-74.006, 40.7128)).unwrap();
315        assert!((x - (-8238310.0)).abs() < 100.0);
316    }
317
318    #[test]
319    fn proj_facade_from_known_crs_3d() {
320        let proj = Proj::new_known_crs("EPSG:4326", "EPSG:3857", None).unwrap();
321        let (x, _y, z) = proj.convert_3d((-74.006, 40.7128, 25.0)).unwrap();
322        assert!((x - (-8238310.0)).abs() < 100.0);
323        assert!((z - 25.0).abs() < 1e-12);
324    }
325
326    #[test]
327    fn proj_facade_create_from_definitions() {
328        let from = Proj::new("+proj=longlat +datum=WGS84").unwrap();
329        let to = Proj::new("EPSG:3857").unwrap();
330        let proj = from.create_crs_to_crs_from_pj(&to, None, None).unwrap();
331        let (x, _y) = proj.convert((-74.006, 40.7128)).unwrap();
332        assert!((x - (-8238310.0)).abs() < 100.0);
333    }
334
335    #[test]
336    fn proj_facade_inverse() {
337        let proj = Proj::new_known_crs("EPSG:4326", "EPSG:3857", None).unwrap();
338        let inv = proj.inverse().unwrap();
339        let (lon, lat) = inv.convert((-8_238_310.0, 4_970_072.0)).unwrap();
340        assert!(lon < -70.0);
341        assert!(lat > 40.0);
342    }
343
344    #[test]
345    fn proj_facade_transform_bounds() {
346        let proj = Proj::new_known_crs("EPSG:4326", "EPSG:3857", None).unwrap();
347        let result = proj
348            .transform_bounds(Bounds::new(-74.3, 40.45, -73.65, 40.95), 4)
349            .unwrap();
350        assert!(result.max_x > result.min_x);
351        assert!(result.max_y > result.min_y);
352    }
353}