1#![forbid(unsafe_code)]
2
3mod proj_string;
28mod projjson;
29mod wkt;
30
31use proj_core::{Bounds, Coord, Coord3D, CrsDef, Transform, Transformable, Transformable3D};
32
33#[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
44pub fn parse_crs(s: &str) -> Result<CrsDef> {
56 let s = s.trim();
57
58 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 if upper.starts_with("URN:OGC:DEF:CRS:") {
67 let parts: Vec<&str> = s.split(':').collect();
68 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 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 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 if s.starts_with('+') {
99 return proj_string::parse_proj_string(s);
100 }
101
102 if s.starts_with('{') {
104 return projjson::parse_projjson(s);
105 }
106
107 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
123pub 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
135pub struct Proj {
141 inner: ProjInner,
142}
143
144enum ProjInner {
145 Definition(CrsDef),
146 Transform(Box<Transform>),
147}
148
149impl Proj {
150 pub fn new(definition: &str) -> Result<Self> {
152 Ok(Self {
153 inner: ProjInner::Definition(parse_crs(definition)?),
154 })
155 }
156
157 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 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 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 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 pub fn convert_coord(&self, coord: Coord) -> proj_core::Result<Coord> {
200 self.convert(coord)
201 }
202
203 pub fn convert_coord_3d(&self, coord: Coord3D) -> proj_core::Result<Coord3D> {
205 self.convert_3d(coord)
206 }
207
208 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 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}