Skip to main content

oxiproj_core/
error.rs

1//! Error type ported from PROJ `src/proj.h` (PROJ_ERR_* codes).
2
3#[cfg(feature = "no_std")]
4use alloc::string::String;
5
6use thiserror::Error;
7
8/// Error type mirroring PROJ's `PROJ_ERR_*` codes.
9///
10/// The numeric codes are grouped by base values defined in `src/proj.h`:
11/// - `PROJ_ERR_INVALID_OP` (1024): invalid operation / wrong setup.
12/// - `PROJ_ERR_COORD_TRANSFM` (2048): coordinate transformation failures.
13/// - `PROJ_ERR_OTHER` (4096): all other errors.
14#[derive(Debug, Clone, PartialEq, Eq, Error)]
15#[non_exhaustive]
16pub enum ProjError {
17    /// Generic invalid operation (`PROJ_ERR_INVALID_OP` = 1024).
18    #[error("invalid PROJ string syntax or operation")]
19    InvalidOp,
20
21    /// Invalid PROJ string syntax (`PROJ_ERR_INVALID_OP_WRONG_SYNTAX` = 1025).
22    #[error("invalid PROJ string syntax")]
23    WrongSyntax,
24
25    /// Missing required operation parameter
26    /// (`PROJ_ERR_INVALID_OP_MISSING_ARG` = 1026).
27    #[error("missing required operation parameter")]
28    MissingArg,
29
30    /// One of the operation parameters has an illegal value
31    /// (`PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE` = 1027).
32    #[error("one of the operation parameters has an illegal value")]
33    IllegalArgValue,
34
35    /// Mutually exclusive arguments
36    /// (`PROJ_ERR_INVALID_OP_MUTUALLY_EXCLUSIVE_ARGS` = 1028).
37    #[error("mutually exclusive arguments")]
38    MutuallyExclusiveArgs,
39
40    /// File not found or with invalid content
41    /// (`PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID` = 1029).
42    #[error("file not found or with invalid content")]
43    FileNotFound,
44
45    /// Value does not correspond to an ellipsoid (maps to code 1027,
46    /// same as [`ProjError::IllegalArgValue`]).
47    #[error("value does not correspond to an ellipsoid")]
48    NotEllipsoid,
49
50    /// Generic error of coordinate transformation
51    /// (`PROJ_ERR_COORD_TRANSFM` = 2048).
52    #[error("generic error of coordinate transformation")]
53    CoordTransfm,
54
55    /// Invalid coordinate (`PROJ_ERR_COORD_TRANSFM_INVALID_COORD` = 2049).
56    #[error("invalid coordinate")]
57    InvalidCoord,
58
59    /// Coordinate outside projection domain
60    /// (`PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN` = 2050).
61    #[error("coordinate outside projection domain")]
62    OutsideProjectionDomain,
63
64    /// No operation found matching criteria
65    /// (`PROJ_ERR_COORD_TRANSFM_NO_OPERATION` = 2051).
66    #[error("no operation found matching criteria")]
67    NoOperation,
68
69    /// Point to transform falls outside grid or subgrid
70    /// (`PROJ_ERR_COORD_TRANSFM_OUTSIDE_GRID` = 2052).
71    #[error("point to transform falls outside grid or subgrid")]
72    OutsideGrid,
73
74    /// Point to transform falls in a grid cell that evaluates to nodata
75    /// (`PROJ_ERR_COORD_TRANSFM_GRID_AT_NODATA` = 2053).
76    #[error("point to transform falls in a grid cell that evaluates to nodata")]
77    GridAtNodata,
78
79    /// Iterative algorithm did not converge
80    /// (`PROJ_ERR_COORD_TRANSFM_NO_CONVERGENCE` = 2054).
81    #[error("iterative algorithm did not converge")]
82    NoConvergence,
83
84    /// Missing required time coordinate
85    /// (`PROJ_ERR_COORD_TRANSFM_MISSING_TIME` = 2055).
86    #[error("missing required time coordinate")]
87    MissingTime,
88
89    /// Invalid CRS definition with a descriptive message.
90    ///
91    /// Use this variant when the error is specifically a CRS definition
92    /// problem and you can provide a helpful message (and optionally a
93    /// suggestion for fixing it).
94    #[error("Invalid CRS definition: {message}")]
95    InvalidCrs {
96        /// Human-readable description of the problem.
97        message: String,
98        /// Optional suggestion for resolving the error.
99        suggestion: Option<String>,
100    },
101
102    /// Coordinate out of bounds.
103    ///
104    /// Carries a descriptive message and an optional suggestion.
105    #[error("Coordinate out of bounds: {message}")]
106    OutOfBounds {
107        /// Human-readable description of the problem.
108        message: String,
109        /// Optional suggestion for resolving the error.
110        suggestion: Option<String>,
111    },
112
113    /// Projection computation failed.
114    ///
115    /// Carries a descriptive message about the computation failure.
116    #[error("Projection failed: {message}")]
117    ProjectionFailed {
118        /// Human-readable description of the failure.
119        message: String,
120    },
121
122    /// Operation not supported (e.g. AD path not implemented for this projection).
123    #[error("operation not supported")]
124    UnsupportedOperation,
125
126    /// Other error (`PROJ_ERR_OTHER` = 4096).
127    #[error("other error")]
128    Other,
129
130    /// Error caused by incorrect use of PROJ API
131    /// (`PROJ_ERR_OTHER_API_MISUSE` = 4097).
132    #[error("error caused by incorrect use of PROJ API")]
133    ApiMisuse,
134
135    /// No inverse operation (`PROJ_ERR_OTHER_NO_INVERSE_OP` = 4098).
136    #[error("no inverse operation")]
137    NoInverseOp,
138
139    /// Failure when accessing a network resource
140    /// (`PROJ_ERR_OTHER_NETWORK_ERROR` = 4099).
141    #[error("failure when accessing a network resource")]
142    NetworkError,
143}
144
145impl ProjError {
146    /// Ported from src/proj.h (PROJ_ERR_* numeric codes)
147    pub fn code(&self) -> i32 {
148        match self {
149            ProjError::InvalidOp => 1024,
150            ProjError::WrongSyntax => 1025,
151            ProjError::MissingArg => 1026,
152            ProjError::IllegalArgValue => 1027,
153            ProjError::MutuallyExclusiveArgs => 1028,
154            ProjError::FileNotFound => 1029,
155            ProjError::NotEllipsoid => 1027,
156            ProjError::CoordTransfm => 2048,
157            ProjError::InvalidCoord => 2049,
158            ProjError::OutsideProjectionDomain => 2050,
159            ProjError::NoOperation => 2051,
160            ProjError::OutsideGrid => 2052,
161            ProjError::GridAtNodata => 2053,
162            ProjError::NoConvergence => 2054,
163            ProjError::MissingTime => 2055,
164            ProjError::InvalidCrs { .. } => 1024,
165            ProjError::OutOfBounds { .. } => 2050,
166            ProjError::ProjectionFailed { .. } => 2048,
167            ProjError::UnsupportedOperation => 4096,
168            ProjError::Other => 4096,
169            ProjError::ApiMisuse => 4097,
170            ProjError::NoInverseOp => 4098,
171            ProjError::NetworkError => 4099,
172        }
173    }
174
175    /// Returns a human-readable suggestion for resolving this error, if one
176    /// is available.
177    ///
178    /// Returns `Some` for variants that carry a `suggestion` field (when one
179    /// was provided at construction time) as well as for several well-known
180    /// unit variants where a static hint is always useful.
181    pub fn suggestion(&self) -> Option<&str> {
182        match self {
183            ProjError::InvalidCrs { suggestion, .. } => suggestion.as_deref(),
184            ProjError::OutOfBounds { suggestion, .. } => suggestion.as_deref(),
185            ProjError::MissingArg => Some(
186                "verify that +proj= is specified; \
187                 common values: merc, lcc, tmerc, utm, stere, aeqd, longlat",
188            ),
189            ProjError::IllegalArgValue => {
190                Some("check that all numeric parameters are valid and in the expected range")
191            }
192            ProjError::FileNotFound => Some(
193                "ensure the grid file is accessible; \
194                 use create_with_ctx() to register in-memory grids",
195            ),
196            ProjError::WrongSyntax => Some(
197                "check the proj-string syntax; \
198                 each keyword must start with + (e.g. +proj=merc +ellps=WGS84)",
199            ),
200            ProjError::NoInverseOp => Some(
201                "this projection does not support an inverse operation; \
202                 try a pipeline with an explicit inverse step",
203            ),
204            ProjError::InvalidCoord => Some(
205                "verify that input coordinates are in the expected unit \
206                 (degrees or metres) and within the projection domain",
207            ),
208            ProjError::OutsideGrid => Some(
209                "the coordinate falls outside all available grid files; \
210                 verify the grid coverage area",
211            ),
212            _ => None,
213        }
214    }
215}
216
217/// Convenience alias for results returning a [`ProjError`].
218pub type ProjResult<T> = Result<T, ProjError>;
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    #[cfg(feature = "no_std")]
224    use alloc::string::ToString;
225
226    #[test]
227    fn invalid_op_code() {
228        assert_eq!(ProjError::InvalidOp.code(), 1024);
229    }
230
231    #[test]
232    fn wrong_syntax_code() {
233        assert_eq!(ProjError::WrongSyntax.code(), 1025);
234    }
235
236    #[test]
237    fn missing_arg_code() {
238        assert_eq!(ProjError::MissingArg.code(), 1026);
239    }
240
241    #[test]
242    fn illegal_arg_value_code() {
243        assert_eq!(ProjError::IllegalArgValue.code(), 1027);
244    }
245
246    #[test]
247    fn mutually_exclusive_args_code() {
248        assert_eq!(ProjError::MutuallyExclusiveArgs.code(), 1028);
249    }
250
251    #[test]
252    fn file_not_found_code() {
253        assert_eq!(ProjError::FileNotFound.code(), 1029);
254    }
255
256    #[test]
257    fn not_ellipsoid_code() {
258        assert_eq!(ProjError::NotEllipsoid.code(), 1027);
259    }
260
261    #[test]
262    fn coord_transfm_code() {
263        assert_eq!(ProjError::CoordTransfm.code(), 2048);
264    }
265
266    #[test]
267    fn invalid_coord_code() {
268        assert_eq!(ProjError::InvalidCoord.code(), 2049);
269    }
270
271    #[test]
272    fn outside_projection_domain_code() {
273        assert_eq!(ProjError::OutsideProjectionDomain.code(), 2050);
274    }
275
276    #[test]
277    fn no_operation_code() {
278        assert_eq!(ProjError::NoOperation.code(), 2051);
279    }
280
281    #[test]
282    fn outside_grid_code() {
283        assert_eq!(ProjError::OutsideGrid.code(), 2052);
284    }
285
286    #[test]
287    fn grid_at_nodata_code() {
288        assert_eq!(ProjError::GridAtNodata.code(), 2053);
289    }
290
291    #[test]
292    fn no_convergence_code() {
293        assert_eq!(ProjError::NoConvergence.code(), 2054);
294    }
295
296    #[test]
297    fn missing_time_code() {
298        assert_eq!(ProjError::MissingTime.code(), 2055);
299    }
300
301    #[test]
302    fn other_code() {
303        assert_eq!(ProjError::Other.code(), 4096);
304    }
305
306    #[test]
307    fn api_misuse_code() {
308        assert_eq!(ProjError::ApiMisuse.code(), 4097);
309    }
310
311    #[test]
312    fn no_inverse_op_code() {
313        assert_eq!(ProjError::NoInverseOp.code(), 4098);
314    }
315
316    #[test]
317    fn network_error_code() {
318        assert_eq!(ProjError::NetworkError.code(), 4099);
319    }
320
321    #[test]
322    fn display_messages_non_empty() {
323        assert!(!ProjError::InvalidCoord.to_string().is_empty());
324        assert!(!ProjError::OutsideProjectionDomain.to_string().is_empty());
325    }
326
327    #[test]
328    fn proj_result_alias_works() {
329        let ok: ProjResult<i32> = Ok(7);
330        let err: ProjResult<i32> = Err(ProjError::Other);
331        assert_eq!(ok, Ok(7));
332        assert_eq!(err, Err(ProjError::Other));
333    }
334
335    #[test]
336    fn invalid_crs_code_and_suggestion() {
337        let e = ProjError::InvalidCrs {
338            message: "bad epsg".to_string(),
339            suggestion: Some("try EPSG:4326".to_string()),
340        };
341        assert_eq!(e.code(), 1024);
342        assert_eq!(e.suggestion(), Some("try EPSG:4326"));
343        assert!(!e.to_string().is_empty());
344    }
345
346    #[test]
347    fn out_of_bounds_code_and_suggestion() {
348        let e = ProjError::OutOfBounds {
349            message: "lat 100".to_string(),
350            suggestion: None,
351        };
352        assert_eq!(e.code(), 2050);
353        assert_eq!(e.suggestion(), None);
354    }
355
356    #[test]
357    fn projection_failed_code() {
358        let e = ProjError::ProjectionFailed {
359            message: "singular matrix".to_string(),
360        };
361        assert_eq!(e.code(), 2048);
362        assert_eq!(e.suggestion(), None);
363    }
364
365    #[test]
366    fn suggestion_returns_none_for_unit_variants() {
367        assert_eq!(ProjError::Other.suggestion(), None);
368        assert_eq!(ProjError::InvalidOp.suggestion(), None);
369    }
370
371    #[test]
372    fn suggestion_missing_arg() {
373        assert!(ProjError::MissingArg.suggestion().is_some());
374    }
375
376    #[test]
377    fn suggestion_illegal_arg_value() {
378        assert!(ProjError::IllegalArgValue.suggestion().is_some());
379    }
380
381    #[test]
382    fn suggestion_file_not_found() {
383        assert!(ProjError::FileNotFound.suggestion().is_some());
384    }
385
386    #[test]
387    fn suggestion_wrong_syntax() {
388        assert!(ProjError::WrongSyntax.suggestion().is_some());
389    }
390
391    #[test]
392    fn suggestion_no_inverse_op() {
393        assert!(ProjError::NoInverseOp.suggestion().is_some());
394    }
395
396    #[test]
397    fn suggestion_invalid_coord() {
398        assert!(ProjError::InvalidCoord.suggestion().is_some());
399    }
400
401    #[test]
402    fn suggestion_outside_grid() {
403        assert!(ProjError::OutsideGrid.suggestion().is_some());
404    }
405
406    #[test]
407    fn suggestion_other_returns_none() {
408        assert!(ProjError::Other.suggestion().is_none());
409        assert!(ProjError::CoordTransfm.suggestion().is_none());
410        assert!(ProjError::NetworkError.suggestion().is_none());
411    }
412}