Skip to main content

qualia_core_db/domains/geospatial/
triggers.rs

1use std::collections::HashMap;
2
3use crate::modalities::spatio_temporal::{self, Rcc8Relation};
4use crate::q_hash;
5use crate::NQuin;
6
7/// A registry mapping spatial regions (e.g., geohash or bounding box hash) to
8/// governed N3 trigger rules. When a user or entity crosses into a region,
9/// the corresponding rules are injected into the Webizen VM for evaluation.
10pub 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
43/// Engine that fires location triggers, optionally using RCC-8 containment when
44/// boundary quins are available in the arena.
45pub 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    /// Collect triggers that should fire when entering `location_hash`.
61    /// Evaluates RCC-8 containment when boundary quins and a location point are present.
62    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    /// Returns rule quins to inject for the given location.
67    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                    &region_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        // Square region containing the point (0.5, 0.5)
188        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}