#[derive(Debug, thiserror::Error)]
pub enum GeoError {
#[error("longitude out of range [-180, 180]: {value}")]
LongitudeOutOfRange {
value: f32,
},
#[error("latitude out of range [-90, 90]: {value}")]
LatitudeOutOfRange {
value: f32,
},
#[error("degenerate bounding box: {axis} min ({min}) >= max ({max})")]
DegenerateBbox {
axis: &'static str,
min: f32,
max: f32,
},
#[error("geometry must not be empty")]
EmptyGeometry,
#[error("coordinate must be finite, got {value}")]
NonFiniteCoordinate {
value: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Longitude(f32);
impl Longitude {
pub fn new(raw: f32) -> Result<Self, GeoError> {
if !raw.is_finite() {
return Err(GeoError::NonFiniteCoordinate { value: raw });
}
if !(-180.0..=180.0).contains(&raw) {
return Err(GeoError::LongitudeOutOfRange { value: raw });
}
Ok(Self(raw))
}
pub fn get(self) -> f32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Latitude(f32);
impl Latitude {
pub fn new(raw: f32) -> Result<Self, GeoError> {
if !raw.is_finite() {
return Err(GeoError::NonFiniteCoordinate { value: raw });
}
if !(-90.0..=90.0).contains(&raw) {
return Err(GeoError::LatitudeOutOfRange { value: raw });
}
Ok(Self(raw))
}
pub fn get(self) -> f32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundingBox {
min_x: Longitude,
min_y: Latitude,
max_x: Longitude,
max_y: Latitude,
}
impl BoundingBox {
pub fn new(minx: f32, miny: f32, maxx: f32, maxy: f32) -> Result<Self, GeoError> {
let min_x = Longitude::new(minx)?;
let max_x = Longitude::new(maxx)?;
let min_y = Latitude::new(miny)?;
let max_y = Latitude::new(maxy)?;
if minx >= maxx {
return Err(GeoError::DegenerateBbox {
axis: "x",
min: minx,
max: maxx,
});
}
if miny >= maxy {
return Err(GeoError::DegenerateBbox {
axis: "y",
min: miny,
max: maxy,
});
}
Ok(Self { min_x, min_y, max_x, max_y })
}
pub fn min_x(&self) -> Longitude {
self.min_x
}
pub fn min_y(&self) -> Latitude {
self.min_y
}
pub fn max_x(&self) -> Longitude {
self.max_x
}
pub fn max_y(&self) -> Latitude {
self.max_y
}
pub fn contains(&self, lon: Longitude, lat: Latitude) -> bool {
lon.get() >= self.min_x.get()
&& lon.get() <= self.max_x.get()
&& lat.get() >= self.min_y.get()
&& lat.get() <= self.max_y.get()
}
pub fn intersects(&self, other: &BoundingBox) -> bool {
self.min_x.get() <= other.max_x.get()
&& self.max_x.get() >= other.min_x.get()
&& self.min_y.get() <= other.max_y.get()
&& self.max_y.get() >= other.min_y.get()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WkbGeometry(Vec<u8>);
impl WkbGeometry {
pub fn new(raw: Vec<u8>) -> Result<Self, GeoError> {
if raw.is_empty() {
return Err(GeoError::EmptyGeometry);
}
Ok(Self(raw))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn longitude_valid_boundaries() {
assert!(Longitude::new(-180.0).is_ok());
assert!(Longitude::new(180.0).is_ok());
assert!(Longitude::new(0.0).is_ok());
}
#[test]
fn longitude_out_of_range() {
assert!(matches!(
Longitude::new(180.1),
Err(GeoError::LongitudeOutOfRange { value }) if (value - 180.1).abs() < 0.001
));
assert!(matches!(
Longitude::new(-180.1),
Err(GeoError::LongitudeOutOfRange { .. })
));
}
#[test]
fn longitude_non_finite() {
assert!(matches!(
Longitude::new(f32::NAN),
Err(GeoError::NonFiniteCoordinate { .. })
));
assert!(matches!(
Longitude::new(f32::INFINITY),
Err(GeoError::NonFiniteCoordinate { .. })
));
}
#[test]
fn longitude_get_roundtrips() {
let lon = Longitude::new(42.5).unwrap();
assert!((lon.get() - 42.5).abs() < f32::EPSILON);
}
#[test]
fn latitude_valid_boundaries() {
assert!(Latitude::new(-90.0).is_ok());
assert!(Latitude::new(90.0).is_ok());
assert!(Latitude::new(0.0).is_ok());
}
#[test]
fn latitude_out_of_range() {
assert!(matches!(
Latitude::new(90.1),
Err(GeoError::LatitudeOutOfRange { .. })
));
assert!(matches!(
Latitude::new(-90.1),
Err(GeoError::LatitudeOutOfRange { .. })
));
}
#[test]
fn latitude_non_finite() {
assert!(matches!(
Latitude::new(f32::NEG_INFINITY),
Err(GeoError::NonFiniteCoordinate { .. })
));
}
#[test]
fn bbox_valid() {
let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0);
assert!(bb.is_ok());
}
#[test]
fn bbox_degenerate_x() {
assert!(matches!(
BoundingBox::new(5.0, -5.0, 5.0, 5.0),
Err(GeoError::DegenerateBbox { axis: "x", .. })
));
}
#[test]
fn bbox_degenerate_y() {
assert!(matches!(
BoundingBox::new(-5.0, 5.0, 5.0, 5.0),
Err(GeoError::DegenerateBbox { axis: "y", .. })
));
}
#[test]
fn bbox_non_finite_propagates() {
assert!(matches!(
BoundingBox::new(f32::NAN, 0.0, 10.0, 5.0),
Err(GeoError::NonFiniteCoordinate { .. })
));
}
#[test]
fn bbox_contains() {
let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
let inside_lon = Longitude::new(0.0).unwrap();
let inside_lat = Latitude::new(0.0).unwrap();
assert!(bb.contains(inside_lon, inside_lat));
let outside_lon = Longitude::new(15.0).unwrap();
assert!(!bb.contains(outside_lon, inside_lat));
}
#[test]
fn bbox_contains_on_boundary() {
let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
let edge_lon = Longitude::new(-10.0).unwrap();
let edge_lat = Latitude::new(5.0).unwrap();
assert!(bb.contains(edge_lon, edge_lat));
}
#[test]
fn bbox_intersects() {
let a = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
let b = BoundingBox::new(5.0, 0.0, 20.0, 10.0).unwrap();
assert!(a.intersects(&b));
assert!(b.intersects(&a));
}
#[test]
fn bbox_no_intersect() {
let a = BoundingBox::new(-10.0, -5.0, 0.0, 5.0).unwrap();
let b = BoundingBox::new(5.0, -5.0, 10.0, 5.0).unwrap();
assert!(!a.intersects(&b));
}
#[test]
fn bbox_getters() {
let bb = BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap();
assert!((bb.min_x().get() - (-10.0)).abs() < f32::EPSILON);
assert!((bb.min_y().get() - (-5.0)).abs() < f32::EPSILON);
assert!((bb.max_x().get() - 10.0).abs() < f32::EPSILON);
assert!((bb.max_y().get() - 5.0).abs() < f32::EPSILON);
}
#[test]
fn wkb_valid() {
let geom = WkbGeometry::new(vec![0x01, 0x02, 0x03]);
assert!(geom.is_ok());
}
#[test]
fn wkb_empty_rejected() {
assert!(matches!(WkbGeometry::new(vec![]), Err(GeoError::EmptyGeometry)));
}
#[test]
fn wkb_as_bytes() {
let geom = WkbGeometry::new(vec![0xDE, 0xAD]).unwrap();
assert_eq!(geom.as_bytes(), &[0xDE, 0xAD]);
}
#[test]
fn wkb_into_bytes() {
let raw = vec![0xBE, 0xEF];
let geom = WkbGeometry::new(raw.clone()).unwrap();
assert_eq!(geom.into_bytes(), raw);
}
#[test]
fn bbox_reversed_x_fails_with_degenerate_bbox() {
assert!(matches!(
BoundingBox::new(10.0, -5.0, -10.0, 5.0),
Err(GeoError::DegenerateBbox { axis: "x", .. })
));
}
#[test]
fn bbox_longitude_out_of_range_propagates() {
assert!(matches!(
BoundingBox::new(-200.0, -5.0, 10.0, 5.0),
Err(GeoError::LongitudeOutOfRange { .. })
));
}
#[test]
fn bbox_near_antimeridian_succeeds() {
assert!(BoundingBox::new(179.0, -5.0, 180.0, 5.0).is_ok());
}
#[test]
fn wkb_clone_produces_equal_value() {
let geom = WkbGeometry::new(vec![0x01, 0x02, 0x03]).unwrap();
let cloned = geom.clone();
assert_eq!(geom, cloned);
}
#[test]
fn bbox_edge_touching_intersects() {
let a = BoundingBox::new(-10.0, -5.0, 0.0, 5.0).unwrap();
let b = BoundingBox::new(0.0, -5.0, 10.0, 5.0).unwrap();
assert!(a.intersects(&b));
assert!(b.intersects(&a));
}
}