Skip to main content

maps_engine_rust/
geofence.rs

1//! Geofencing — point-in-polygon and circular fences. Port of `geofence.ts`.
2
3use crate::geo::{haversine, LonLat};
4
5/// A polygonal geofence.
6#[derive(Debug, Clone)]
7pub struct PolygonFence {
8    pub name: String,
9    /// Vertices as (lon, lat), ordered either clockwise or counter-clockwise.
10    pub vertices: Vec<LonLat>,
11}
12
13impl PolygonFence {
14    pub fn new(name: impl Into<String>, vertices: Vec<LonLat>) -> Self {
15        PolygonFence {
16            name: name.into(),
17            vertices,
18        }
19    }
20
21    /// Ray-casting point-in-polygon test.
22    pub fn contains(&self, p: LonLat) -> bool {
23        let n = self.vertices.len();
24        if n < 3 {
25            return false;
26        }
27        let mut inside = false;
28        let mut j = n - 1;
29        for i in 0..n {
30            let vi = self.vertices[i];
31            let vj = self.vertices[j];
32            if (vi.lat > p.lat) != (vj.lat > p.lat)
33                && p.lon < (vj.lon - vi.lon) * (p.lat - vi.lat) / (vj.lat - vi.lat) + vi.lon
34            {
35                inside = !inside;
36            }
37            j = i;
38        }
39        inside
40    }
41}
42
43/// A circular geofence defined by center + radius in meters.
44#[derive(Debug, Clone)]
45pub struct CircleFence {
46    pub name: String,
47    pub center: LonLat,
48    pub radius_m: f64,
49}
50
51impl CircleFence {
52    pub fn new(name: impl Into<String>, center: LonLat, radius_m: f64) -> Self {
53        CircleFence {
54            name: name.into(),
55            center,
56            radius_m,
57        }
58    }
59
60    pub fn contains(&self, p: LonLat) -> bool {
61        haversine(self.center, p) <= self.radius_m
62    }
63}
64
65/// A named geofence of either shape.
66#[derive(Debug, Clone)]
67pub enum Geofence {
68    Polygon(PolygonFence),
69    Circle(CircleFence),
70}
71
72impl Geofence {
73    pub fn name(&self) -> &str {
74        match self {
75            Geofence::Polygon(f) => &f.name,
76            Geofence::Circle(f) => &f.name,
77        }
78    }
79
80    pub fn contains(&self, p: LonLat) -> bool {
81        match self {
82            Geofence::Polygon(f) => f.contains(p),
83            Geofence::Circle(f) => f.contains(p),
84        }
85    }
86}
87
88/// Return names of all fences containing the point.
89pub fn fences_at(fences: &[Geofence], p: LonLat) -> Vec<&str> {
90    fences
91        .iter()
92        .filter(|f| f.contains(p))
93        .map(|f| f.name())
94        .collect()
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn square() -> PolygonFence {
102        PolygonFence::new(
103            "sq",
104            vec![
105                LonLat::new(0.0, 0.0),
106                LonLat::new(1.0, 0.0),
107                LonLat::new(1.0, 1.0),
108                LonLat::new(0.0, 1.0),
109            ],
110        )
111    }
112
113    #[test]
114    fn point_inside_square() {
115        assert!(square().contains(LonLat::new(0.5, 0.5)));
116    }
117
118    #[test]
119    fn point_outside_square() {
120        assert!(!square().contains(LonLat::new(2.0, 0.5)));
121        assert!(!square().contains(LonLat::new(0.5, -1.0)));
122    }
123
124    #[test]
125    fn degenerate_polygon_never_contains() {
126        let f = PolygonFence::new("line", vec![LonLat::new(0.0, 0.0), LonLat::new(1.0, 1.0)]);
127        assert!(!f.contains(LonLat::new(0.5, 0.5)));
128    }
129
130    #[test]
131    fn circle_fence_radius() {
132        let c = LonLat::new(105.85, 21.02);
133        let f = CircleFence::new("home", c, 500.0);
134        assert!(f.contains(c));
135        assert!(!f.contains(LonLat::new(105.90, 21.02)));
136    }
137
138    #[test]
139    fn fences_at_lists_matches() {
140        let fences = vec![
141            Geofence::Polygon(square()),
142            Geofence::Circle(CircleFence::new("far", LonLat::new(10.0, 10.0), 100.0)),
143        ];
144        let hits = fences_at(&fences, LonLat::new(0.5, 0.5));
145        assert_eq!(hits, vec!["sq"]);
146    }
147}