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
43#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
45pub struct Longitude(f32);
46
47impl Longitude {
48 pub fn new(raw: f32) -> Result<Self, GeoError> {
58 if !raw.is_finite() {
59 return Err(GeoError::NonFiniteCoordinate { value: raw });
60 }
61 if !(-180.0..=180.0).contains(&raw) {
62 return Err(GeoError::LongitudeOutOfRange { value: raw });
63 }
64 Ok(Self(raw))
65 }
66
67 pub fn get(self) -> f32 {
69 self.0
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
75pub struct Latitude(f32);
76
77impl Latitude {
78 pub fn new(raw: f32) -> Result<Self, GeoError> {
88 if !raw.is_finite() {
89 return Err(GeoError::NonFiniteCoordinate { value: raw });
90 }
91 if !(-90.0..=90.0).contains(&raw) {
92 return Err(GeoError::LatitudeOutOfRange { value: raw });
93 }
94 Ok(Self(raw))
95 }
96
97 pub fn get(self) -> f32 {
99 self.0
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct BoundingBox {
106 min_x: Longitude,
107 min_y: Latitude,
108 max_x: Longitude,
109 max_y: Latitude,
110}
111
112impl BoundingBox {
113 pub fn new(minx: f32, miny: f32, maxx: f32, maxy: f32) -> Result<Self, GeoError> {
127 let min_x = Longitude::new(minx)?;
128 let max_x = Longitude::new(maxx)?;
129 let min_y = Latitude::new(miny)?;
130 let max_y = Latitude::new(maxy)?;
131
132 if minx >= maxx {
133 return Err(GeoError::DegenerateBbox {
134 axis: "x",
135 min: minx,
136 max: maxx,
137 });
138 }
139 if miny >= maxy {
140 return Err(GeoError::DegenerateBbox {
141 axis: "y",
142 min: miny,
143 max: maxy,
144 });
145 }
146
147 Ok(Self { min_x, min_y, max_x, max_y })
148 }
149
150 pub fn min_x(&self) -> Longitude {
152 self.min_x
153 }
154
155 pub fn min_y(&self) -> Latitude {
157 self.min_y
158 }
159
160 pub fn max_x(&self) -> Longitude {
162 self.max_x
163 }
164
165 pub fn max_y(&self) -> Latitude {
167 self.max_y
168 }
169
170 pub fn contains(&self, lon: Longitude, lat: Latitude) -> bool {
173 lon.get() >= self.min_x.get()
174 && lon.get() <= self.max_x.get()
175 && lat.get() >= self.min_y.get()
176 && lat.get() <= self.max_y.get()
177 }
178
179 pub fn intersects(&self, other: &BoundingBox) -> bool {
182 self.min_x.get() <= other.max_x.get()
183 && self.max_x.get() >= other.min_x.get()
184 && self.min_y.get() <= other.max_y.get()
185 && self.max_y.get() >= other.min_y.get()
186 }
187}
188
189#[derive(Debug, Clone, PartialEq)]
195pub struct WkbGeometry(Vec<u8>);
196
197impl WkbGeometry {
198 pub fn new(raw: Vec<u8>) -> Result<Self, GeoError> {
206 if raw.is_empty() {
207 return Err(GeoError::EmptyGeometry);
208 }
209 Ok(Self(raw))
210 }
211
212 pub fn as_bytes(&self) -> &[u8] {
214 &self.0
215 }
216
217 pub fn into_bytes(self) -> Vec<u8> {
219 self.0
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
230 fn longitude_valid_boundaries() {
231 assert!(Longitude::new(-180.0).is_ok());
232 assert!(Longitude::new(180.0).is_ok());
233 assert!(Longitude::new(0.0).is_ok());
234 }
235
236 #[test]
237 fn longitude_out_of_range() {
238 assert!(matches!(
239 Longitude::new(180.1),
240 Err(GeoError::LongitudeOutOfRange { value }) if (value - 180.1).abs() < 0.001
241 ));
242 assert!(matches!(
243 Longitude::new(-180.1),
244 Err(GeoError::LongitudeOutOfRange { .. })
245 ));
246 }
247
248 #[test]
249 fn longitude_non_finite() {
250 assert!(matches!(
251 Longitude::new(f32::NAN),
252 Err(GeoError::NonFiniteCoordinate { .. })
253 ));
254 assert!(matches!(
255 Longitude::new(f32::INFINITY),
256 Err(GeoError::NonFiniteCoordinate { .. })
257 ));
258 }
259
260 #[test]
261 fn longitude_get_roundtrips() {
262 let lon = Longitude::new(42.5).unwrap();
263 assert!((lon.get() - 42.5).abs() < f32::EPSILON);
264 }
265
266 #[test]
269 fn latitude_valid_boundaries() {
270 assert!(Latitude::new(-90.0).is_ok());
271 assert!(Latitude::new(90.0).is_ok());
272 assert!(Latitude::new(0.0).is_ok());
273 }
274
275 #[test]
276 fn latitude_out_of_range() {
277 assert!(matches!(
278 Latitude::new(90.1),
279 Err(GeoError::LatitudeOutOfRange { .. })
280 ));
281 assert!(matches!(
282 Latitude::new(-90.1),
283 Err(GeoError::LatitudeOutOfRange { .. })
284 ));
285 }
286
287 #[test]
288 fn latitude_non_finite() {
289 assert!(matches!(
290 Latitude::new(f32::NEG_INFINITY),
291 Err(GeoError::NonFiniteCoordinate { .. })
292 ));
293 }
294
295 #[test]
298 fn bbox_valid() {
299 let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0);
300 assert!(bb.is_ok());
301 }
302
303 #[test]
304 fn bbox_degenerate_x() {
305 assert!(matches!(
306 BoundingBox::new(5.0, -5.0, 5.0, 5.0),
307 Err(GeoError::DegenerateBbox { axis: "x", .. })
308 ));
309 }
310
311 #[test]
312 fn bbox_degenerate_y() {
313 assert!(matches!(
314 BoundingBox::new(-5.0, 5.0, 5.0, 5.0),
315 Err(GeoError::DegenerateBbox { axis: "y", .. })
316 ));
317 }
318
319 #[test]
320 fn bbox_non_finite_propagates() {
321 assert!(matches!(
322 BoundingBox::new(f32::NAN, 0.0, 10.0, 5.0),
323 Err(GeoError::NonFiniteCoordinate { .. })
324 ));
325 }
326
327 #[test]
328 fn bbox_contains() {
329 let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
330 let inside_lon = Longitude::new(0.0).unwrap();
331 let inside_lat = Latitude::new(0.0).unwrap();
332 assert!(bb.contains(inside_lon, inside_lat));
333
334 let outside_lon = Longitude::new(15.0).unwrap();
335 assert!(!bb.contains(outside_lon, inside_lat));
336 }
337
338 #[test]
339 fn bbox_contains_on_boundary() {
340 let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
341 let edge_lon = Longitude::new(-10.0).unwrap();
342 let edge_lat = Latitude::new(5.0).unwrap();
343 assert!(bb.contains(edge_lon, edge_lat));
344 }
345
346 #[test]
347 fn bbox_intersects() {
348 let a = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
349 let b = BoundingBox::new(5.0, 0.0, 20.0, 10.0).unwrap();
350 assert!(a.intersects(&b));
351 assert!(b.intersects(&a));
352 }
353
354 #[test]
355 fn bbox_no_intersect() {
356 let a = BoundingBox::new(-10.0, -5.0, 0.0, 5.0).unwrap();
357 let b = BoundingBox::new(5.0, -5.0, 10.0, 5.0).unwrap();
358 assert!(!a.intersects(&b));
359 }
360
361 #[test]
362 fn bbox_getters() {
363 let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
364 assert!((bb.min_x().get() - (-10.0)).abs() < f32::EPSILON);
365 assert!((bb.min_y().get() - (-5.0)).abs() < f32::EPSILON);
366 assert!((bb.max_x().get() - 10.0).abs() < f32::EPSILON);
367 assert!((bb.max_y().get() - 5.0).abs() < f32::EPSILON);
368 }
369
370 #[test]
373 fn wkb_valid() {
374 let geom = WkbGeometry::new(vec![0x01, 0x02, 0x03]);
375 assert!(geom.is_ok());
376 }
377
378 #[test]
379 fn wkb_empty_rejected() {
380 assert!(matches!(WkbGeometry::new(vec![]), Err(GeoError::EmptyGeometry)));
381 }
382
383 #[test]
384 fn wkb_as_bytes() {
385 let geom = WkbGeometry::new(vec![0xDE, 0xAD]).unwrap();
386 assert_eq!(geom.as_bytes(), &[0xDE, 0xAD]);
387 }
388
389 #[test]
390 fn wkb_into_bytes() {
391 let raw = vec![0xBE, 0xEF];
392 let geom = WkbGeometry::new(raw.clone()).unwrap();
393 assert_eq!(geom.into_bytes(), raw);
394 }
395
396 #[test]
397 fn bbox_reversed_x_fails_with_degenerate_bbox() {
398 assert!(matches!(
400 BoundingBox::new(10.0, -5.0, -10.0, 5.0),
401 Err(GeoError::DegenerateBbox { axis: "x", .. })
402 ));
403 }
404
405 #[test]
406 fn bbox_longitude_out_of_range_propagates() {
407 assert!(matches!(
408 BoundingBox::new(-200.0, -5.0, 10.0, 5.0),
409 Err(GeoError::LongitudeOutOfRange { .. })
410 ));
411 }
412
413 #[test]
414 fn bbox_near_antimeridian_succeeds() {
415 assert!(BoundingBox::new(179.0, -5.0, 180.0, 5.0).is_ok());
418 }
419
420 #[test]
421 fn wkb_clone_produces_equal_value() {
422 let geom = WkbGeometry::new(vec![0x01, 0x02, 0x03]).unwrap();
423 let cloned = geom.clone();
424 assert_eq!(geom, cloned);
425 }
426
427 #[test]
428 fn bbox_edge_touching_intersects() {
429 let a = BoundingBox::new(-10.0, -5.0, 0.0, 5.0).unwrap();
431 let b = BoundingBox::new(0.0, -5.0, 10.0, 5.0).unwrap();
432 assert!(a.intersects(&b));
433 assert!(b.intersects(&a));
434 }
435}