faultline_geometry 0.1.0

Geometry abstractions and spatial metric contracts for FaultLine.
Documentation
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);
    }
}