use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct Coordinate {
pub x: f64,
pub y: f64,
}
impl Coordinate {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct BoundingBox {
pub min_x: f64,
pub min_y: f64,
pub max_x: f64,
pub max_y: f64,
}
impl BoundingBox {
pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
Self {
min_x,
min_y,
max_x,
max_y,
}
}
pub fn width(&self) -> f64 {
(self.max_x - self.min_x).max(0.0)
}
pub fn height(&self) -> f64 {
(self.max_y - self.min_y).max(0.0)
}
pub fn contains(&self, point: &Coordinate) -> bool {
point.x >= self.min_x
&& point.x <= self.max_x
&& point.y >= self.min_y
&& point.y <= self.max_y
}
pub fn intersects(&self, other: &Self) -> bool {
!(self.max_x < other.min_x
|| self.min_x > other.max_x
|| self.max_y < other.min_y
|| self.min_y > other.max_y)
}
fn from_points(points: &[Coordinate]) -> Option<Self> {
let first = points.first()?;
let mut min_x = first.x;
let mut min_y = first.y;
let mut max_x = first.x;
let mut max_y = first.y;
for point in points.iter().skip(1) {
min_x = min_x.min(point.x);
min_y = min_y.min(point.y);
max_x = max_x.max(point.x);
max_y = max_y.max(point.y);
}
Some(Self::new(min_x, min_y, max_x, max_y))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Geometry {
Point(Coordinate),
LineString(Vec<Coordinate>),
Polygon {
exterior: Vec<Coordinate>,
holes: Vec<Vec<Coordinate>>,
},
}
impl Geometry {
pub fn bounding_box(&self) -> Option<BoundingBox> {
match self {
Geometry::Point(point) => Some(BoundingBox::new(point.x, point.y, point.x, point.y)),
Geometry::LineString(points) => BoundingBox::from_points(points),
Geometry::Polygon { exterior, .. } => BoundingBox::from_points(exterior),
}
}
pub fn vertex_count(&self) -> usize {
match self {
Geometry::Point(_) => 1,
Geometry::LineString(points) => points.len(),
Geometry::Polygon { exterior, holes } => {
exterior.len() + holes.iter().map(Vec::len).sum::<usize>()
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TopologyConstraint {
MustNotSelfIntersect,
MustBeClosedRing,
MustBeWithinDatasetExtent,
MustNotOverlapSameLayer,
}
pub trait SpatialMetric {
fn distance(&self, left: &Geometry, right: &Geometry) -> Option<f64>;
fn area(&self, geometry: &Geometry) -> Option<f64>;
fn intersects_bbox(&self, geometry: &Geometry, bbox: &BoundingBox) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn polygon_bounding_box_is_computed_from_exterior_ring() {
let polygon = Geometry::Polygon {
exterior: vec![
Coordinate::new(-1.0, 0.0),
Coordinate::new(3.0, 0.0),
Coordinate::new(3.0, 5.0),
Coordinate::new(-1.0, 5.0),
Coordinate::new(-1.0, 0.0),
],
holes: Vec::new(),
};
let bbox = polygon.bounding_box().expect("bbox must exist");
assert_eq!(bbox, BoundingBox::new(-1.0, 0.0, 3.0, 5.0));
assert_eq!(bbox.width(), 4.0);
assert_eq!(bbox.height(), 5.0);
}
#[test]
fn bbox_intersection_detects_overlap() {
let left = BoundingBox::new(0.0, 0.0, 4.0, 4.0);
let right = BoundingBox::new(3.0, 3.0, 8.0, 8.0);
let far = BoundingBox::new(10.0, 10.0, 12.0, 12.0);
assert!(left.intersects(&right));
assert!(!left.intersects(&far));
}
#[test]
fn geometry_round_trip_serialization() {
let geometry = Geometry::LineString(vec![
Coordinate::new(0.0, 0.0),
Coordinate::new(2.0, 2.0),
Coordinate::new(4.0, 1.0),
]);
let json = serde_json::to_string(&geometry).expect("serialize geometry");
let restored: Geometry = serde_json::from_str(&json).expect("deserialize geometry");
assert_eq!(restored, geometry);
}
}