qualia_core_db/domains/geospatial/
triggers.rs1use std::collections::HashMap;
2
3use crate::modalities::spatio_temporal::{self, Rcc8Relation};
4use crate::q_hash;
5use crate::NQuin;
6
7pub struct LocationTriggerRegistry {
11 pub region_rules: HashMap<u64, Vec<NQuin>>,
12}
13
14impl LocationTriggerRegistry {
15 pub fn new() -> Self {
16 Self {
17 region_rules: HashMap::new(),
18 }
19 }
20
21 pub fn register_trigger(&mut self, region_hash: u64, rule_quin: NQuin) {
22 self.region_rules
23 .entry(region_hash)
24 .or_default()
25 .push(rule_quin);
26 }
27
28 pub fn evaluate_triggers_at(&self, location_hash: u64) -> Vec<NQuin> {
29 let mut triggers = Vec::new();
30 if let Some(rules) = self.region_rules.get(&location_hash) {
31 triggers.extend(rules.iter().copied());
32 }
33 triggers
34 }
35}
36
37impl Default for LocationTriggerRegistry {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43pub struct LocationTriggerEngine {
46 registry: LocationTriggerRegistry,
47}
48
49impl LocationTriggerEngine {
50 pub fn new() -> Self {
51 Self {
52 registry: LocationTriggerRegistry::new(),
53 }
54 }
55
56 pub fn register_trigger(&mut self, region_hash: u64, rule_quin: NQuin) {
57 self.registry.register_trigger(region_hash, rule_quin);
58 }
59
60 pub fn on_enter(&self, location_hash: u64, arena_quins: &[NQuin]) -> Vec<NQuin> {
63 self.fire_triggers_at(location_hash, arena_quins)
64 }
65
66 pub fn fire_triggers_at(&self, location_hash: u64, arena_quins: &[NQuin]) -> Vec<NQuin> {
68 let mut triggers = self.registry.evaluate_triggers_at(location_hash);
69
70 let boundary = q_hash("spatial:boundary");
71 let location_point = q_hash("q42:locationPoint");
72
73 let loc_point = arena_quins.iter().find_map(|quin| {
74 if quin.subject == location_hash && quin.predicate == location_point {
75 Some(spatio_temporal::unpack_point(quin.object))
76 } else {
77 None
78 }
79 });
80
81 if let Some(point) = loc_point {
82 let point_slice = [point];
83 for (region_hash, rules) in &self.registry.region_rules {
84 if *region_hash == location_hash {
85 continue;
86 }
87 let region_poly = collect_boundary_points(*region_hash, arena_quins, boundary);
88 if region_poly.len() < 3 {
89 continue;
90 }
91 let relation = spatio_temporal::evaluate_rcc8_points(
92 location_hash,
93 &point_slice,
94 *region_hash,
95 ®ion_poly,
96 );
97 if is_containment(relation) {
98 for rule in rules {
99 if !triggers.iter().any(|t| t == rule) {
100 triggers.push(*rule);
101 }
102 }
103 }
104 }
105 }
106
107 triggers
108 }
109}
110
111impl Default for LocationTriggerEngine {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117fn is_containment(relation: Rcc8Relation) -> bool {
118 matches!(
119 relation,
120 Rcc8Relation::NonTangentialProperPart
121 | Rcc8Relation::TangentiallyProperPart
122 | Rcc8Relation::Equal
123 )
124}
125
126fn collect_boundary_points(
127 region_id: u64,
128 arena_quins: &[NQuin],
129 boundary_pred: u64,
130) -> Vec<(f64, f64)> {
131 let mut indexed: Vec<(u32, (f64, f64))> = Vec::new();
132 for quin in arena_quins {
133 if quin.subject == region_id && quin.predicate == boundary_pred {
134 let idx = quin.metadata as u32;
135 indexed.push((idx, spatio_temporal::unpack_point(quin.object)));
136 }
137 }
138 indexed.sort_by_key(|(idx, _)| *idx);
139 indexed.into_iter().map(|(_, pt)| pt).collect()
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_register_and_evaluate_trigger() {
148 let mut registry = LocationTriggerRegistry::new();
149 let region = 0x123456789;
150 let rule = NQuin {
151 subject: 1,
152 predicate: 2,
153 object: 3,
154 context: 0,
155 metadata: 0,
156 parity: 0,
157 };
158 registry.register_trigger(region, rule);
159
160 let triggers = registry.evaluate_triggers_at(region);
161 assert_eq!(triggers.len(), 1);
162 assert_eq!(triggers[0].subject, 1);
163
164 let no_triggers = registry.evaluate_triggers_at(0x999);
165 assert_eq!(no_triggers.len(), 0);
166 }
167
168 #[test]
169 fn test_location_trigger_engine_rcc8_containment() {
170 let mut engine = LocationTriggerEngine::new();
171 let region = 0xDEAD_BEEF;
172 let location = 0xCAFE_BABE;
173
174 let rule = NQuin {
175 subject: 10,
176 predicate: 20,
177 object: 30,
178 context: 0,
179 metadata: 0,
180 parity: 0,
181 };
182 engine.register_trigger(region, rule);
183
184 let boundary = q_hash("spatial:boundary");
185 let location_point = q_hash("q42:locationPoint");
186
187 let region_quins = [
189 boundary_quin(region, boundary, 0, (0.0, 0.0)),
190 boundary_quin(region, boundary, 1, (2.0, 0.0)),
191 boundary_quin(region, boundary, 2, (2.0, 2.0)),
192 boundary_quin(region, boundary, 3, (0.0, 2.0)),
193 ];
194
195 let mut loc_quin = NQuin {
196 subject: location,
197 predicate: location_point,
198 object: spatio_temporal::pack_point(0.5, 0.5),
199 context: 0,
200 metadata: 0,
201 parity: 0,
202 };
203 loc_quin.parity =
204 loc_quin.subject ^ loc_quin.predicate ^ loc_quin.object ^ loc_quin.context;
205
206 let mut arena: Vec<NQuin> = region_quins.to_vec();
207 arena.push(loc_quin);
208
209 let triggers = engine.fire_triggers_at(location, &arena);
210 assert_eq!(triggers.len(), 1);
211 assert_eq!(triggers[0].subject, 10);
212 }
213
214 fn boundary_quin(region: u64, pred: u64, seq: u32, pt: (f64, f64)) -> NQuin {
215 let mut q = NQuin {
216 subject: region,
217 predicate: pred,
218 object: spatio_temporal::pack_point(pt.0, pt.1),
219 context: 0,
220 metadata: seq as u64,
221 parity: 0,
222 };
223 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
224 q
225 }
226}