nodedb_cluster/distributed_spatial/
geofence.rs1use nodedb_types::geometry::point_in_polygon;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Geofence {
19 pub id: String,
21 pub name: String,
23 pub polygon: Vec<[f64; 2]>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct GeofenceEvent {
30 pub geofence_id: String,
32 pub entity_id: String,
34 pub event_type: GeofenceEventType,
36 pub point: [f64; 2],
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub enum GeofenceEventType {
42 Enter,
43 Exit,
44}
45
46pub struct GeofenceRegistry {
52 geofences: Vec<Geofence>,
53 entity_state: std::collections::HashMap<String, std::collections::HashSet<String>>,
55}
56
57impl GeofenceRegistry {
58 pub fn new() -> Self {
59 Self {
60 geofences: Vec::new(),
61 entity_state: std::collections::HashMap::new(),
62 }
63 }
64
65 pub fn register(&mut self, geofence: Geofence) {
67 self.geofences.retain(|g| g.id != geofence.id);
69 self.geofences.push(geofence);
70 }
71
72 pub fn unregister(&mut self, geofence_id: &str) {
74 self.geofences.retain(|g| g.id != geofence_id);
75 for state in self.entity_state.values_mut() {
77 state.remove(geofence_id);
78 }
79 }
80
81 pub fn evaluate_point(&mut self, entity_id: &str, lng: f64, lat: f64) -> Vec<GeofenceEvent> {
86 let point = [lng, lat];
87 let mut events = Vec::new();
88
89 let current_inside: std::collections::HashSet<String> = self
90 .geofences
91 .iter()
92 .filter(|g| point_in_polygon(lng, lat, &g.polygon))
93 .map(|g| g.id.clone())
94 .collect();
95
96 let previous = self.entity_state.entry(entity_id.to_string()).or_default();
97
98 for gid in ¤t_inside {
100 if !previous.contains(gid) {
101 events.push(GeofenceEvent {
102 geofence_id: gid.clone(),
103 entity_id: entity_id.to_string(),
104 event_type: GeofenceEventType::Enter,
105 point,
106 });
107 }
108 }
109
110 for gid in previous.iter() {
112 if !current_inside.contains(gid) {
113 events.push(GeofenceEvent {
114 geofence_id: gid.clone(),
115 entity_id: entity_id.to_string(),
116 event_type: GeofenceEventType::Exit,
117 point,
118 });
119 }
120 }
121
122 *previous = current_inside;
124 events
125 }
126
127 pub fn len(&self) -> usize {
129 self.geofences.len()
130 }
131
132 pub fn is_empty(&self) -> bool {
133 self.geofences.is_empty()
134 }
135
136 pub fn export(&self) -> Vec<u8> {
138 sonic_rs::to_vec(&self.geofences).expect("Geofence vec is always serializable")
140 }
141
142 pub fn import(&mut self, data: &[u8]) {
144 if let Ok(geofences) = sonic_rs::from_slice::<Vec<Geofence>>(data) {
145 self.geofences = geofences;
146 }
147 }
148}
149
150impl Default for GeofenceRegistry {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 fn square_geofence(id: &str, min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Geofence {
161 Geofence {
162 id: id.to_string(),
163 name: format!("zone_{id}"),
164 polygon: vec![
165 [min_x, min_y],
166 [max_x, min_y],
167 [max_x, max_y],
168 [min_x, max_y],
169 [min_x, min_y],
170 ],
171 }
172 }
173
174 #[test]
175 fn enter_event() {
176 let mut reg = GeofenceRegistry::new();
177 reg.register(square_geofence("zone1", 0.0, 0.0, 10.0, 10.0));
178
179 let events = reg.evaluate_point("vehicle1", -5.0, -5.0);
181 assert!(events.is_empty());
182
183 let events = reg.evaluate_point("vehicle1", 5.0, 5.0);
185 assert_eq!(events.len(), 1);
186 assert_eq!(events[0].event_type, GeofenceEventType::Enter);
187 assert_eq!(events[0].geofence_id, "zone1");
188 }
189
190 #[test]
191 fn exit_event() {
192 let mut reg = GeofenceRegistry::new();
193 reg.register(square_geofence("zone1", 0.0, 0.0, 10.0, 10.0));
194
195 reg.evaluate_point("v1", 5.0, 5.0);
197 let events = reg.evaluate_point("v1", 20.0, 20.0);
199 assert_eq!(events.len(), 1);
200 assert_eq!(events[0].event_type, GeofenceEventType::Exit);
201 }
202
203 #[test]
204 fn no_event_when_staying() {
205 let mut reg = GeofenceRegistry::new();
206 reg.register(square_geofence("zone1", 0.0, 0.0, 10.0, 10.0));
207
208 reg.evaluate_point("v1", 5.0, 5.0); let events = reg.evaluate_point("v1", 6.0, 6.0); assert!(events.is_empty());
211 }
212
213 #[test]
214 fn multiple_geofences() {
215 let mut reg = GeofenceRegistry::new();
216 reg.register(square_geofence("a", 0.0, 0.0, 10.0, 10.0));
217 reg.register(square_geofence("b", 5.0, 5.0, 15.0, 15.0));
218
219 let events = reg.evaluate_point("v1", 7.0, 7.0);
221 assert_eq!(events.len(), 2);
222 }
223
224 #[test]
225 fn unregister_geofence() {
226 let mut reg = GeofenceRegistry::new();
227 reg.register(square_geofence("zone1", 0.0, 0.0, 10.0, 10.0));
228 reg.unregister("zone1");
229 assert!(reg.is_empty());
230 }
231
232 #[test]
233 fn export_import_roundtrip() {
234 let mut reg = GeofenceRegistry::new();
235 reg.register(square_geofence("a", 0.0, 0.0, 10.0, 10.0));
236 reg.register(square_geofence("b", 20.0, 20.0, 30.0, 30.0));
237
238 let data = reg.export();
239 let mut reg2 = GeofenceRegistry::new();
240 reg2.import(&data);
241 assert_eq!(reg2.len(), 2);
242 }
243}