Skip to main content

oxigdal_proj/
error.rs

1//! Error types for projection and coordinate transformation operations.
2//!
3//! This module provides comprehensive error handling for all projection-related operations,
4//! following the no-unwrap policy.
5
6#[cfg(not(feature = "std"))]
7use alloc::string::String;
8
9/// Result type for projection operations.
10pub type Result<T> = core::result::Result<T, Error>;
11
12/// Comprehensive error type for projection operations.
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15    /// Invalid EPSG code
16    #[error("Invalid EPSG code: {code}")]
17    InvalidEpsgCode {
18        /// The invalid EPSG code
19        code: u32,
20    },
21
22    /// EPSG code not found in database
23    #[error("EPSG code {code} not found in database")]
24    EpsgCodeNotFound {
25        /// The EPSG code that was not found
26        code: u32,
27    },
28
29    /// Invalid PROJ string
30    #[error("Invalid PROJ string: {reason}")]
31    InvalidProjString {
32        /// Reason for the invalid PROJ string
33        reason: String,
34    },
35
36    /// Invalid WKT (Well-Known Text) string
37    #[error("Invalid WKT string: {reason}")]
38    InvalidWkt {
39        /// Reason for the invalid WKT
40        reason: String,
41    },
42
43    /// WKT parsing error
44    #[error("WKT parsing error at position {position}: {message}")]
45    WktParseError {
46        /// Position in the WKT string where error occurred
47        position: usize,
48        /// Error message
49        message: String,
50    },
51
52    /// Coordinate transformation error
53    #[error("Coordinate transformation failed: {reason}")]
54    TransformationError {
55        /// Reason for transformation failure
56        reason: String,
57    },
58
59    /// Unsupported CRS (Coordinate Reference System)
60    #[error("Unsupported CRS: {crs_type}")]
61    UnsupportedCrs {
62        /// Type of CRS that is not supported
63        crs_type: String,
64    },
65
66    /// Incompatible source and target CRS
67    #[error("Incompatible CRS for transformation: source={src}, target={tgt}")]
68    IncompatibleCrs {
69        /// Source CRS description
70        src: String,
71        /// Target CRS description
72        tgt: String,
73    },
74
75    /// Invalid coordinate
76    #[error("Invalid coordinate: {reason}")]
77    InvalidCoordinate {
78        /// Reason for invalid coordinate
79        reason: String,
80    },
81
82    /// Out of bounds coordinate
83    #[error("Coordinate out of valid bounds: ({x}, {y})")]
84    CoordinateOutOfBounds {
85        /// X coordinate
86        x: f64,
87        /// Y coordinate
88        y: f64,
89    },
90
91    /// Invalid bounding box
92    #[error("Invalid bounding box: {reason}")]
93    InvalidBoundingBox {
94        /// Reason for invalid bounding box
95        reason: String,
96    },
97
98    /// Missing required parameter
99    #[error("Missing required parameter: {parameter}")]
100    MissingParameter {
101        /// Name of missing parameter
102        parameter: String,
103    },
104
105    /// Invalid parameter value
106    #[error("Invalid parameter value for {parameter}: {reason}")]
107    InvalidParameter {
108        /// Parameter name
109        parameter: String,
110        /// Reason for invalid value
111        reason: String,
112    },
113
114    /// Datum transformation error
115    #[error("Datum transformation failed: {reason}")]
116    DatumTransformError {
117        /// Reason for datum transformation failure
118        reason: String,
119    },
120
121    /// Projection initialization error
122    #[error("Failed to initialize projection: {reason}")]
123    ProjectionInitError {
124        /// Reason for initialization failure
125        reason: String,
126    },
127
128    /// Unsupported projection
129    #[error("Unsupported projection: {projection}")]
130    UnsupportedProjection {
131        /// Name of unsupported projection
132        projection: String,
133    },
134
135    /// Numerical error (e.g., division by zero, sqrt of negative)
136    #[error("Numerical error in projection calculation: {operation}")]
137    NumericalError {
138        /// Operation that caused the error
139        operation: String,
140    },
141
142    /// Convergence failure in iterative algorithms
143    #[error("Failed to converge after {iterations} iterations")]
144    ConvergenceError {
145        /// Number of iterations attempted
146        iterations: usize,
147    },
148
149    /// JSON serialization/deserialization error
150    #[cfg(feature = "std")]
151    #[error("JSON error: {0}")]
152    JsonError(#[from] serde_json::Error),
153
154    /// I/O error
155    #[cfg(feature = "std")]
156    #[error("I/O error: {0}")]
157    IoError(#[from] std::io::Error),
158
159    /// UTF-8 conversion error
160    #[cfg(feature = "std")]
161    #[error("UTF-8 conversion error: {0}")]
162    Utf8Error(#[from] std::str::Utf8Error),
163
164    /// Error from proj4rs library
165    #[cfg(feature = "std")]
166    #[error("Proj4rs error: {0}")]
167    Proj4rsError(String),
168
169    /// Error from PROJ C library (when using proj-sys feature)
170    #[cfg(feature = "proj-sys")]
171    #[error("PROJ library error: {0}")]
172    ProjSysError(String),
173
174    /// Coordinate is outside the source CRS area of use
175    #[error("Coordinate ({lon}, {lat}) is outside area of use for CRS '{crs}'")]
176    OutOfAreaOfUse {
177        /// Longitude
178        lon: f64,
179        /// Latitude
180        lat: f64,
181        /// CRS description
182        crs: String,
183    },
184
185    /// Coordinate lies outside the registered area-of-use bounding box for
186    /// the source EPSG (raised by `Transformer` when the per-instance check
187    /// mode is `AreaOfUseCheck::Strict`).
188    #[error(
189        "coordinate ({lon}, {lat}) outside area-of-use for EPSG:{epsg} \
190         (west={west}, south={south}, east={east}, north={north})"
191    )]
192    OutsideAreaOfUse {
193        /// Longitude of the offending point (degrees, WGS84).
194        lon: f64,
195        /// Latitude of the offending point (degrees, WGS84).
196        lat: f64,
197        /// Source EPSG code whose area-of-use was violated.
198        epsg: u32,
199        /// Western bound of the area-of-use (degrees).
200        west: f64,
201        /// Southern bound of the area-of-use (degrees).
202        south: f64,
203        /// Eastern bound of the area-of-use (degrees).
204        east: f64,
205        /// Northern bound of the area-of-use (degrees).
206        north: f64,
207    },
208
209    /// NTv2 binary grid-shift parse error
210    #[error("NTv2 parse error: {0}")]
211    Ntv2ParseError(String),
212
213    /// Coordinate is outside all loaded NTv2 sub-grid extents
214    #[error("Coordinate ({lon}, {lat}) is outside NTv2 grid extent")]
215    Ntv2OutOfGrid {
216        /// Longitude in degrees
217        lon: f64,
218        /// Latitude in degrees
219        lat: f64,
220    },
221
222    /// Invalid arguments for compound CRS construction.
223    #[error("Invalid compound CRS: {reason}")]
224    InvalidCompoundCrs {
225        /// Reason the compound CRS is invalid
226        reason: String,
227    },
228
229    /// Geoid model required for vertical datum transformation is not available.
230    #[error("Geoid model not available for vertical CRS: {vertical_crs}")]
231    GeoidNotAvailable {
232        /// Name/description of the vertical CRS that requires a geoid model
233        vertical_crs: String,
234    },
235
236    /// Geoid grid file has an unexpected size, layout, or could not be read.
237    #[error("geoid file format: {0}")]
238    GeoidFileFormat(String),
239
240    /// No geoid model has been attached to a [`crate::transform::Transformer`]
241    /// that is being asked to perform a compound-CRS transform requiring a
242    /// vertical-datum shift.
243    #[error("no geoid model available for compound CRS transform")]
244    NoGeoidAvailable,
245
246    /// Error from PROJ.db SQLite file (always present so non-proj-db builds also compile)
247    #[error("PROJ.db error: {0}")]
248    ProjDbError(String),
249
250    /// Generic error for cases not covered by specific error types
251    #[error("{0}")]
252    Other(String),
253
254    /// Pipeline PROJ string could not be parsed.
255    #[error("Pipeline parse error: {0}")]
256    PipelineParseError(String),
257
258    /// A step inside a coordinate pipeline failed.
259    #[error("Pipeline step {step} failed: {inner}")]
260    PipelineStepError {
261        /// Zero-based index of the failing step
262        step: usize,
263        /// Error message from the step
264        inner: String,
265    },
266}
267
268impl Error {
269    /// Creates an invalid EPSG code error.
270    pub fn invalid_epsg_code(code: u32) -> Self {
271        Self::InvalidEpsgCode { code }
272    }
273
274    /// Creates an EPSG code not found error.
275    pub fn epsg_not_found(code: u32) -> Self {
276        Self::EpsgCodeNotFound { code }
277    }
278
279    /// Creates an invalid PROJ string error.
280    pub fn invalid_proj_string<S: Into<String>>(reason: S) -> Self {
281        Self::InvalidProjString {
282            reason: reason.into(),
283        }
284    }
285
286    /// Creates an invalid WKT error.
287    pub fn invalid_wkt<S: Into<String>>(reason: S) -> Self {
288        Self::InvalidWkt {
289            reason: reason.into(),
290        }
291    }
292
293    /// Creates a WKT parsing error.
294    pub fn wkt_parse_error<S: Into<String>>(position: usize, message: S) -> Self {
295        Self::WktParseError {
296            position,
297            message: message.into(),
298        }
299    }
300
301    /// Creates a transformation error.
302    pub fn transformation_error<S: Into<String>>(reason: S) -> Self {
303        Self::TransformationError {
304            reason: reason.into(),
305        }
306    }
307
308    /// Creates an unsupported CRS error.
309    pub fn unsupported_crs<S: Into<String>>(crs_type: S) -> Self {
310        Self::UnsupportedCrs {
311            crs_type: crs_type.into(),
312        }
313    }
314
315    /// Creates an incompatible CRS error.
316    pub fn incompatible_crs<S: Into<String>>(src: S, tgt: S) -> Self {
317        Self::IncompatibleCrs {
318            src: src.into(),
319            tgt: tgt.into(),
320        }
321    }
322
323    /// Creates an invalid coordinate error.
324    pub fn invalid_coordinate<S: Into<String>>(reason: S) -> Self {
325        Self::InvalidCoordinate {
326            reason: reason.into(),
327        }
328    }
329
330    /// Creates a coordinate out of bounds error.
331    pub fn coordinate_out_of_bounds(x: f64, y: f64) -> Self {
332        Self::CoordinateOutOfBounds { x, y }
333    }
334
335    /// Creates an invalid bounding box error.
336    pub fn invalid_bounding_box<S: Into<String>>(reason: S) -> Self {
337        Self::InvalidBoundingBox {
338            reason: reason.into(),
339        }
340    }
341
342    /// Creates a missing parameter error.
343    pub fn missing_parameter<S: Into<String>>(parameter: S) -> Self {
344        Self::MissingParameter {
345            parameter: parameter.into(),
346        }
347    }
348
349    /// Creates an invalid parameter error.
350    pub fn invalid_parameter<S: Into<String>>(parameter: S, reason: S) -> Self {
351        Self::InvalidParameter {
352            parameter: parameter.into(),
353            reason: reason.into(),
354        }
355    }
356
357    /// Creates a datum transform error.
358    pub fn datum_transform_error<S: Into<String>>(reason: S) -> Self {
359        Self::DatumTransformError {
360            reason: reason.into(),
361        }
362    }
363
364    /// Creates a projection initialization error.
365    pub fn projection_init_error<S: Into<String>>(reason: S) -> Self {
366        Self::ProjectionInitError {
367            reason: reason.into(),
368        }
369    }
370
371    /// Creates an unsupported projection error.
372    pub fn unsupported_projection<S: Into<String>>(projection: S) -> Self {
373        Self::UnsupportedProjection {
374            projection: projection.into(),
375        }
376    }
377
378    /// Creates a numerical error.
379    pub fn numerical_error<S: Into<String>>(operation: S) -> Self {
380        Self::NumericalError {
381            operation: operation.into(),
382        }
383    }
384
385    /// Creates a convergence error.
386    pub fn convergence_error(iterations: usize) -> Self {
387        Self::ConvergenceError { iterations }
388    }
389
390    /// Creates an error from proj4rs library.
391    #[cfg(feature = "std")]
392    pub fn from_proj4rs<S: Into<String>>(message: S) -> Self {
393        Self::Proj4rsError(message.into())
394    }
395
396    /// Creates an out-of-area-of-use error.
397    pub fn out_of_area_of_use<S: Into<String>>(lon: f64, lat: f64, crs: S) -> Self {
398        Self::OutOfAreaOfUse {
399            lon,
400            lat,
401            crs: crs.into(),
402        }
403    }
404
405    /// Creates an NTv2 parse error.
406    pub fn ntv2_parse_error<S: Into<String>>(message: S) -> Self {
407        Self::Ntv2ParseError(message.into())
408    }
409
410    /// Creates an NTv2 out-of-grid error.
411    pub fn ntv2_out_of_grid(lon: f64, lat: f64) -> Self {
412        Self::Ntv2OutOfGrid { lon, lat }
413    }
414
415    /// Creates an invalid compound CRS error.
416    pub fn invalid_compound_crs(reason: impl Into<String>) -> Self {
417        Self::InvalidCompoundCrs {
418            reason: reason.into(),
419        }
420    }
421
422    /// Creates a geoid-not-available error.
423    pub fn geoid_not_available(vertical_crs: impl Into<String>) -> Self {
424        Self::GeoidNotAvailable {
425            vertical_crs: vertical_crs.into(),
426        }
427    }
428
429    /// Creates a generic other error.
430    pub fn other<S: Into<String>>(message: S) -> Self {
431        Self::Other(message.into())
432    }
433}
434
435// Implement conversion from proj4rs errors
436#[cfg(feature = "std")]
437impl From<proj4rs::errors::Error> for Error {
438    fn from(err: proj4rs::errors::Error) -> Self {
439        Self::from_proj4rs(format!("{:?}", err))
440    }
441}
442
443#[cfg(feature = "proj-sys")]
444impl From<proj::ProjError> for Error {
445    fn from(err: proj::ProjError) -> Self {
446        Self::ProjSysError(format!("{}", err))
447    }
448}
449
450#[cfg(test)]
451#[allow(clippy::expect_used)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_error_creation() {
457        let err = Error::invalid_epsg_code(12345);
458        assert!(matches!(err, Error::InvalidEpsgCode { code: 12345 }));
459
460        let err = Error::epsg_not_found(4326);
461        assert!(matches!(err, Error::EpsgCodeNotFound { code: 4326 }));
462
463        let err = Error::invalid_proj_string("missing parameter");
464        assert!(matches!(err, Error::InvalidProjString { .. }));
465
466        let err = Error::transformation_error("invalid coordinates");
467        assert!(matches!(err, Error::TransformationError { .. }));
468    }
469
470    #[test]
471    fn test_error_display() {
472        let err = Error::invalid_epsg_code(12345);
473        assert_eq!(format!("{}", err), "Invalid EPSG code: 12345");
474
475        let err = Error::coordinate_out_of_bounds(180.5, 90.5);
476        assert_eq!(
477            format!("{}", err),
478            "Coordinate out of valid bounds: (180.5, 90.5)"
479        );
480    }
481
482    #[test]
483    fn test_result_type() {
484        fn returns_ok() -> Result<i32> {
485            Ok(42)
486        }
487
488        fn returns_error() -> Result<i32> {
489            Err(Error::invalid_epsg_code(0))
490        }
491
492        assert!(returns_ok().is_ok());
493        assert!(returns_error().is_err());
494    }
495}