1#[derive(Debug, thiserror::Error)]
5pub enum GeoError {
6 #[error("longitude out of range [-180, 180]: {value}")]
8 LongitudeOutOfRange {
9 value: f32,
11 },
12
13 #[error("latitude out of range [-90, 90]: {value}")]
15 LatitudeOutOfRange {
16 value: f32,
18 },
19
20 #[error("degenerate bounding box: {axis} min ({min}) >= max ({max})")]
22 DegenerateBbox {
23 axis: &'static str,
25 min: f32,
27 max: f32,
29 },
30
31 #[error("geometry must not be empty")]
33 EmptyGeometry,
34
35 #[error("coordinate must be finite, got {value}")]
37 NonFiniteCoordinate {
38 value: f32,
40 },
41
42 #[error("outlet longitude must be finite, got {value}")]
44 NonFiniteOutletLongitude {
45 value: f64,
47 },
48
49 #[error("outlet latitude must be finite, got {value}")]
51 NonFiniteOutletLatitude {
52 value: f64,
54 },
55
56 #[error("outlet longitude out of range [-180, 180]: {value}")]
58 OutletLongitudeOutOfRange {
59 value: f64,
61 },
62
63 #[error("outlet latitude out of range [-90, 90]: {value}")]
65 OutletLatitudeOutOfRange {
66 value: f64,
68 },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
73pub struct Longitude(f32);
74
75impl Longitude {
76 pub fn new(raw: f32) -> Result<Self, GeoError> {
86 if !raw.is_finite() {
87 return Err(GeoError::NonFiniteCoordinate { value: raw });
88 }
89 if !(-180.0..=180.0).contains(&raw) {
90 return Err(GeoError::LongitudeOutOfRange { value: raw });
91 }
92 Ok(Self(raw))
93 }
94
95 pub fn get(self) -> f32 {
97 self.0
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
103pub struct Latitude(f32);
104
105impl Latitude {
106 pub fn new(raw: f32) -> Result<Self, GeoError> {
116 if !raw.is_finite() {
117 return Err(GeoError::NonFiniteCoordinate { value: raw });
118 }
119 if !(-90.0..=90.0).contains(&raw) {
120 return Err(GeoError::LatitudeOutOfRange { value: raw });
121 }
122 Ok(Self(raw))
123 }
124
125 pub fn get(self) -> f32 {
127 self.0
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq)]
133pub struct BoundingBox {
134 min_x: Longitude,
135 min_y: Latitude,
136 max_x: Longitude,
137 max_y: Latitude,
138}
139
140impl BoundingBox {
141 pub fn new(minx: f32, miny: f32, maxx: f32, maxy: f32) -> Result<Self, GeoError> {
155 let min_x = Longitude::new(minx)?;
156 let max_x = Longitude::new(maxx)?;
157 let min_y = Latitude::new(miny)?;
158 let max_y = Latitude::new(maxy)?;
159
160 if minx >= maxx {
161 return Err(GeoError::DegenerateBbox {
162 axis: "x",
163 min: minx,
164 max: maxx,
165 });
166 }
167 if miny >= maxy {
168 return Err(GeoError::DegenerateBbox {
169 axis: "y",
170 min: miny,
171 max: maxy,
172 });
173 }
174
175 Ok(Self {
176 min_x,
177 min_y,
178 max_x,
179 max_y,
180 })
181 }
182
183 pub fn min_x(&self) -> Longitude {
185 self.min_x
186 }
187
188 pub fn min_y(&self) -> Latitude {
190 self.min_y
191 }
192
193 pub fn max_x(&self) -> Longitude {
195 self.max_x
196 }
197
198 pub fn max_y(&self) -> Latitude {
200 self.max_y
201 }
202
203 pub fn contains(&self, lon: Longitude, lat: Latitude) -> bool {
206 lon.get() >= self.min_x.get()
207 && lon.get() <= self.max_x.get()
208 && lat.get() >= self.min_y.get()
209 && lat.get() <= self.max_y.get()
210 }
211
212 pub fn intersects(&self, other: &BoundingBox) -> bool {
215 self.min_x.get() <= other.max_x.get()
216 && self.max_x.get() >= other.min_x.get()
217 && self.min_y.get() <= other.max_y.get()
218 && self.max_y.get() >= other.min_y.get()
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq)]
224pub struct OutletCoord {
225 lon: f64,
226 lat: f64,
227}
228
229impl OutletCoord {
230 pub fn new(lon: f64, lat: f64) -> Result<Self, GeoError> {
241 if !lon.is_finite() {
242 return Err(GeoError::NonFiniteOutletLongitude { value: lon });
243 }
244 if !lat.is_finite() {
245 return Err(GeoError::NonFiniteOutletLatitude { value: lat });
246 }
247 if !(-180.0..=180.0).contains(&lon) {
248 return Err(GeoError::OutletLongitudeOutOfRange { value: lon });
249 }
250 if !(-90.0..=90.0).contains(&lat) {
251 return Err(GeoError::OutletLatitudeOutOfRange { value: lat });
252 }
253 Ok(Self { lon, lat })
254 }
255
256 pub fn lon(self) -> f64 {
258 self.lon
259 }
260
261 pub fn lat(self) -> f64 {
263 self.lat
264 }
265}
266
267#[derive(Debug, Clone, PartialEq)]
273pub struct WkbGeometry(Vec<u8>);
274
275impl WkbGeometry {
276 pub fn new(raw: Vec<u8>) -> Result<Self, GeoError> {
284 if raw.is_empty() {
285 return Err(GeoError::EmptyGeometry);
286 }
287 Ok(Self(raw))
288 }
289
290 pub fn as_bytes(&self) -> &[u8] {
292 &self.0
293 }
294
295 pub fn into_bytes(self) -> Vec<u8> {
297 self.0
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
308 fn longitude_valid_boundaries() {
309 assert!(Longitude::new(-180.0).is_ok());
310 assert!(Longitude::new(180.0).is_ok());
311 assert!(Longitude::new(0.0).is_ok());
312 }
313
314 #[test]
315 fn longitude_out_of_range() {
316 assert!(matches!(
317 Longitude::new(180.1),
318 Err(GeoError::LongitudeOutOfRange { value }) if (value - 180.1).abs() < 0.001
319 ));
320 assert!(matches!(
321 Longitude::new(-180.1),
322 Err(GeoError::LongitudeOutOfRange { .. })
323 ));
324 }
325
326 #[test]
327 fn longitude_non_finite() {
328 assert!(matches!(
329 Longitude::new(f32::NAN),
330 Err(GeoError::NonFiniteCoordinate { .. })
331 ));
332 assert!(matches!(
333 Longitude::new(f32::INFINITY),
334 Err(GeoError::NonFiniteCoordinate { .. })
335 ));
336 }
337
338 #[test]
339 fn longitude_get_roundtrips() {
340 let lon = Longitude::new(42.5).unwrap();
341 assert!((lon.get() - 42.5).abs() < f32::EPSILON);
342 }
343
344 #[test]
347 fn latitude_valid_boundaries() {
348 assert!(Latitude::new(-90.0).is_ok());
349 assert!(Latitude::new(90.0).is_ok());
350 assert!(Latitude::new(0.0).is_ok());
351 }
352
353 #[test]
354 fn latitude_out_of_range() {
355 assert!(matches!(
356 Latitude::new(90.1),
357 Err(GeoError::LatitudeOutOfRange { .. })
358 ));
359 assert!(matches!(
360 Latitude::new(-90.1),
361 Err(GeoError::LatitudeOutOfRange { .. })
362 ));
363 }
364
365 #[test]
366 fn latitude_non_finite() {
367 assert!(matches!(
368 Latitude::new(f32::NEG_INFINITY),
369 Err(GeoError::NonFiniteCoordinate { .. })
370 ));
371 }
372
373 #[test]
376 fn bbox_valid() {
377 let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0);
378 assert!(bb.is_ok());
379 }
380
381 #[test]
382 fn bbox_degenerate_x() {
383 assert!(matches!(
384 BoundingBox::new(5.0, -5.0, 5.0, 5.0),
385 Err(GeoError::DegenerateBbox { axis: "x", .. })
386 ));
387 }
388
389 #[test]
390 fn bbox_degenerate_y() {
391 assert!(matches!(
392 BoundingBox::new(-5.0, 5.0, 5.0, 5.0),
393 Err(GeoError::DegenerateBbox { axis: "y", .. })
394 ));
395 }
396
397 #[test]
398 fn bbox_non_finite_propagates() {
399 assert!(matches!(
400 BoundingBox::new(f32::NAN, 0.0, 10.0, 5.0),
401 Err(GeoError::NonFiniteCoordinate { .. })
402 ));
403 }
404
405 #[test]
406 fn bbox_contains() {
407 let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
408 let inside_lon = Longitude::new(0.0).unwrap();
409 let inside_lat = Latitude::new(0.0).unwrap();
410 assert!(bb.contains(inside_lon, inside_lat));
411
412 let outside_lon = Longitude::new(15.0).unwrap();
413 assert!(!bb.contains(outside_lon, inside_lat));
414 }
415
416 #[test]
417 fn bbox_contains_on_boundary() {
418 let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
419 let edge_lon = Longitude::new(-10.0).unwrap();
420 let edge_lat = Latitude::new(5.0).unwrap();
421 assert!(bb.contains(edge_lon, edge_lat));
422 }
423
424 #[test]
425 fn bbox_intersects() {
426 let a = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
427 let b = BoundingBox::new(5.0, 0.0, 20.0, 10.0).unwrap();
428 assert!(a.intersects(&b));
429 assert!(b.intersects(&a));
430 }
431
432 #[test]
433 fn bbox_no_intersect() {
434 let a = BoundingBox::new(-10.0, -5.0, 0.0, 5.0).unwrap();
435 let b = BoundingBox::new(5.0, -5.0, 10.0, 5.0).unwrap();
436 assert!(!a.intersects(&b));
437 }
438
439 #[test]
442 fn outlet_coord_valid_boundaries() {
443 let outlet = OutletCoord::new(-180.0, 90.0).unwrap();
444 assert_eq!(outlet.lon(), -180.0);
445 assert_eq!(outlet.lat(), 90.0);
446 }
447
448 #[test]
449 fn outlet_coord_rejects_non_finite_lon() {
450 assert!(matches!(
451 OutletCoord::new(f64::NAN, 0.0),
452 Err(GeoError::NonFiniteOutletLongitude { .. })
453 ));
454 }
455
456 #[test]
457 fn outlet_coord_rejects_non_finite_lat() {
458 assert!(matches!(
459 OutletCoord::new(0.0, f64::INFINITY),
460 Err(GeoError::NonFiniteOutletLatitude { .. })
461 ));
462 }
463
464 #[test]
465 fn outlet_coord_rejects_out_of_range_lon() {
466 assert!(matches!(
467 OutletCoord::new(180.1, 0.0),
468 Err(GeoError::OutletLongitudeOutOfRange { .. })
469 ));
470 }
471
472 #[test]
473 fn outlet_coord_rejects_out_of_range_lat() {
474 assert!(matches!(
475 OutletCoord::new(0.0, -90.1),
476 Err(GeoError::OutletLatitudeOutOfRange { .. })
477 ));
478 }
479
480 #[test]
481 fn bbox_getters() {
482 let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
483 assert!((bb.min_x().get() - (-10.0)).abs() < f32::EPSILON);
484 assert!((bb.min_y().get() - (-5.0)).abs() < f32::EPSILON);
485 assert!((bb.max_x().get() - 10.0).abs() < f32::EPSILON);
486 assert!((bb.max_y().get() - 5.0).abs() < f32::EPSILON);
487 }
488
489 #[test]
492 fn wkb_valid() {
493 let geom = WkbGeometry::new(vec![0x01, 0x02, 0x03]);
494 assert!(geom.is_ok());
495 }
496
497 #[test]
498 fn wkb_empty_rejected() {
499 assert!(matches!(
500 WkbGeometry::new(vec![]),
501 Err(GeoError::EmptyGeometry)
502 ));
503 }
504
505 #[test]
506 fn wkb_as_bytes() {
507 let geom = WkbGeometry::new(vec![0xDE, 0xAD]).unwrap();
508 assert_eq!(geom.as_bytes(), &[0xDE, 0xAD]);
509 }
510
511 #[test]
512 fn wkb_into_bytes() {
513 let raw = vec![0xBE, 0xEF];
514 let geom = WkbGeometry::new(raw.clone()).unwrap();
515 assert_eq!(geom.into_bytes(), raw);
516 }
517
518 #[test]
519 fn bbox_reversed_x_fails_with_degenerate_bbox() {
520 assert!(matches!(
522 BoundingBox::new(10.0, -5.0, -10.0, 5.0),
523 Err(GeoError::DegenerateBbox { axis: "x", .. })
524 ));
525 }
526
527 #[test]
528 fn bbox_longitude_out_of_range_propagates() {
529 assert!(matches!(
530 BoundingBox::new(-200.0, -5.0, 10.0, 5.0),
531 Err(GeoError::LongitudeOutOfRange { .. })
532 ));
533 }
534
535 #[test]
536 fn bbox_near_antimeridian_succeeds() {
537 assert!(BoundingBox::new(179.0, -5.0, 180.0, 5.0).is_ok());
540 }
541
542 #[test]
543 fn wkb_clone_produces_equal_value() {
544 let geom = WkbGeometry::new(vec![0x01, 0x02, 0x03]).unwrap();
545 let cloned = geom.clone();
546 assert_eq!(geom, cloned);
547 }
548
549 #[test]
550 fn bbox_edge_touching_intersects() {
551 let a = BoundingBox::new(-10.0, -5.0, 0.0, 5.0).unwrap();
553 let b = BoundingBox::new(0.0, -5.0, 10.0, 5.0).unwrap();
554 assert!(a.intersects(&b));
555 assert!(b.intersects(&a));
556 }
557}