maps-engine-rust 0.1.0

Zero-dependency map engine core in Rust — Web Mercator projection, tiles, geofencing, clustering, navigation math.
Documentation
//! Geofencing — point-in-polygon and circular fences. Port of `geofence.ts`.

use crate::geo::{haversine, LonLat};

/// A polygonal geofence.
#[derive(Debug, Clone)]
pub struct PolygonFence {
    pub name: String,
    /// Vertices as (lon, lat), ordered either clockwise or counter-clockwise.
    pub vertices: Vec<LonLat>,
}

impl PolygonFence {
    pub fn new(name: impl Into<String>, vertices: Vec<LonLat>) -> Self {
        PolygonFence {
            name: name.into(),
            vertices,
        }
    }

    /// Ray-casting point-in-polygon test.
    pub fn contains(&self, p: LonLat) -> bool {
        let n = self.vertices.len();
        if n < 3 {
            return false;
        }
        let mut inside = false;
        let mut j = n - 1;
        for i in 0..n {
            let vi = self.vertices[i];
            let vj = self.vertices[j];
            if (vi.lat > p.lat) != (vj.lat > p.lat)
                && p.lon < (vj.lon - vi.lon) * (p.lat - vi.lat) / (vj.lat - vi.lat) + vi.lon
            {
                inside = !inside;
            }
            j = i;
        }
        inside
    }
}

/// A circular geofence defined by center + radius in meters.
#[derive(Debug, Clone)]
pub struct CircleFence {
    pub name: String,
    pub center: LonLat,
    pub radius_m: f64,
}

impl CircleFence {
    pub fn new(name: impl Into<String>, center: LonLat, radius_m: f64) -> Self {
        CircleFence {
            name: name.into(),
            center,
            radius_m,
        }
    }

    pub fn contains(&self, p: LonLat) -> bool {
        haversine(self.center, p) <= self.radius_m
    }
}

/// A named geofence of either shape.
#[derive(Debug, Clone)]
pub enum Geofence {
    Polygon(PolygonFence),
    Circle(CircleFence),
}

impl Geofence {
    pub fn name(&self) -> &str {
        match self {
            Geofence::Polygon(f) => &f.name,
            Geofence::Circle(f) => &f.name,
        }
    }

    pub fn contains(&self, p: LonLat) -> bool {
        match self {
            Geofence::Polygon(f) => f.contains(p),
            Geofence::Circle(f) => f.contains(p),
        }
    }
}

/// Return names of all fences containing the point.
pub fn fences_at(fences: &[Geofence], p: LonLat) -> Vec<&str> {
    fences
        .iter()
        .filter(|f| f.contains(p))
        .map(|f| f.name())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn square() -> PolygonFence {
        PolygonFence::new(
            "sq",
            vec![
                LonLat::new(0.0, 0.0),
                LonLat::new(1.0, 0.0),
                LonLat::new(1.0, 1.0),
                LonLat::new(0.0, 1.0),
            ],
        )
    }

    #[test]
    fn point_inside_square() {
        assert!(square().contains(LonLat::new(0.5, 0.5)));
    }

    #[test]
    fn point_outside_square() {
        assert!(!square().contains(LonLat::new(2.0, 0.5)));
        assert!(!square().contains(LonLat::new(0.5, -1.0)));
    }

    #[test]
    fn degenerate_polygon_never_contains() {
        let f = PolygonFence::new("line", vec![LonLat::new(0.0, 0.0), LonLat::new(1.0, 1.0)]);
        assert!(!f.contains(LonLat::new(0.5, 0.5)));
    }

    #[test]
    fn circle_fence_radius() {
        let c = LonLat::new(105.85, 21.02);
        let f = CircleFence::new("home", c, 500.0);
        assert!(f.contains(c));
        assert!(!f.contains(LonLat::new(105.90, 21.02)));
    }

    #[test]
    fn fences_at_lists_matches() {
        let fences = vec![
            Geofence::Polygon(square()),
            Geofence::Circle(CircleFence::new("far", LonLat::new(10.0, 10.0), 100.0)),
        ];
        let hits = fences_at(&fences, LonLat::new(0.5, 0.5));
        assert_eq!(hits, vec!["sq"]);
    }
}