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