use crate::geo::{haversine, LonLat};
#[derive(Debug, Clone)]
pub struct PolygonFence {
pub name: String,
pub vertices: Vec<LonLat>,
}
impl PolygonFence {
pub fn new(name: impl Into<String>, vertices: Vec<LonLat>) -> Self {
PolygonFence {
name: name.into(),
vertices,
}
}
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
}
}
#[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
}
}
#[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),
}
}
}
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"]);
}
}