1#[cfg(feature = "no_std")]
4use alloc::string::String;
5
6use thiserror::Error;
7
8#[derive(Debug, Clone, PartialEq, Eq, Error)]
15#[non_exhaustive]
16pub enum ProjError {
17 #[error("invalid PROJ string syntax or operation")]
19 InvalidOp,
20
21 #[error("invalid PROJ string syntax")]
23 WrongSyntax,
24
25 #[error("missing required operation parameter")]
28 MissingArg,
29
30 #[error("one of the operation parameters has an illegal value")]
33 IllegalArgValue,
34
35 #[error("mutually exclusive arguments")]
38 MutuallyExclusiveArgs,
39
40 #[error("file not found or with invalid content")]
43 FileNotFound,
44
45 #[error("value does not correspond to an ellipsoid")]
48 NotEllipsoid,
49
50 #[error("generic error of coordinate transformation")]
53 CoordTransfm,
54
55 #[error("invalid coordinate")]
57 InvalidCoord,
58
59 #[error("coordinate outside projection domain")]
62 OutsideProjectionDomain,
63
64 #[error("no operation found matching criteria")]
67 NoOperation,
68
69 #[error("point to transform falls outside grid or subgrid")]
72 OutsideGrid,
73
74 #[error("point to transform falls in a grid cell that evaluates to nodata")]
77 GridAtNodata,
78
79 #[error("iterative algorithm did not converge")]
82 NoConvergence,
83
84 #[error("missing required time coordinate")]
87 MissingTime,
88
89 #[error("Invalid CRS definition: {message}")]
95 InvalidCrs {
96 message: String,
98 suggestion: Option<String>,
100 },
101
102 #[error("Coordinate out of bounds: {message}")]
106 OutOfBounds {
107 message: String,
109 suggestion: Option<String>,
111 },
112
113 #[error("Projection failed: {message}")]
117 ProjectionFailed {
118 message: String,
120 },
121
122 #[error("operation not supported")]
124 UnsupportedOperation,
125
126 #[error("other error")]
128 Other,
129
130 #[error("error caused by incorrect use of PROJ API")]
133 ApiMisuse,
134
135 #[error("no inverse operation")]
137 NoInverseOp,
138
139 #[error("failure when accessing a network resource")]
142 NetworkError,
143}
144
145impl ProjError {
146 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 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
217pub 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}