qualia_core_db/domains/geospatial/adapters/
osm_adapter.rs1use crate::domains::geospatial::adapters::AdapterHttpRequest;
2use crate::net::disclosure::NetworkDisclosureRegistry;
3
4pub struct OsmAdapter {
6 pub id: &'static str,
7 pub overpass_endpoint: String,
8 pub tile_endpoint: String,
9}
10
11impl OsmAdapter {
12 pub fn new(id: &'static str, overpass_endpoint: &str, tile_endpoint: &str) -> Self {
13 Self {
14 id,
15 overpass_endpoint: overpass_endpoint.to_string(),
16 tile_endpoint: tile_endpoint.to_string(),
17 }
18 }
19}
20
21impl OsmAdapter {
22 pub fn adapter_id(&self) -> &'static str {
23 self.id
24 }
25
26 pub fn build_fetch_request(
27 &self,
28 bbox: (f64, f64, f64, f64),
29 time_range: (u64, u64),
30 registry: &NetworkDisclosureRegistry,
31 ) -> Result<AdapterHttpRequest, String> {
32 let primary = &self.overpass_endpoint;
33 if !registry.check_egress_consent(self.adapter_id(), primary) {
34 return Err(format!(
35 "Consent denied or unregistered for endpoint {} by adapter {}",
36 primary,
37 self.adapter_id()
38 ));
39 }
40
41 let date_filter = if time_range.1 > 0 {
42 format!("[date:\"{}\"]", time_range.1) } else {
44 String::new()
45 };
46
47 let query = format!(
49 "{date_filter}[out:json];(node({s},{w},{n},{e});way({s},{w},{n},{e});relation({s},{w},{n},{e}););out body;",
50 date_filter = date_filter,
51 s = bbox.1,
52 w = bbox.0,
53 n = bbox.3,
54 e = bbox.2
55 );
56
57 Ok(AdapterHttpRequest::post_form(
58 primary.clone(),
59 query,
60 "OSM Overpass",
61 ))
62 }
63
64 pub fn fetch_region(
65 &self,
66 bbox: (f64, f64, f64, f64),
67 time_range: (u64, u64),
68 registry: &NetworkDisclosureRegistry,
69 ) -> Result<(), String> {
70 let request = self.build_fetch_request(bbox, time_range, registry)?;
71 super::execute_http_request_status(&request)?;
72 Ok(())
74 }
75
76 pub fn parse_features(&self, body: &str) -> Result<Vec<crate::NQuin>, String> {
84 use crate::lexicon::generate_60bit_token;
85
86 let title = generate_60bit_token(b"http://purl.org/dc/terms/title");
88 let ty = generate_60bit_token(b"http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
89 let source = generate_60bit_token(b"http://purl.org/dc/terms/source");
90 let lat_p = generate_60bit_token(b"http://www.w3.org/2003/01/geo/wgs84_pos#lat");
91 let long_p = generate_60bit_token(b"http://www.w3.org/2003/01/geo/wgs84_pos#long");
92
93 fn quin(s: u64, p: u64, o: u64) -> crate::NQuin {
94 crate::NQuin {
95 subject: s,
96 predicate: p,
97 object: o,
98 context: 0,
99 metadata: 0,
100 parity: s ^ p ^ o,
101 }
102 }
103
104 let json: serde_json::Value = serde_json::from_str(body).map_err(|e| e.to_string())?;
105
106 let mut quins = Vec::new();
107 let elements = match json.get("elements").and_then(|e| e.as_array()) {
108 Some(arr) => arr,
109 None => return Ok(quins),
110 };
111
112 for el in elements {
113 let el_type = match el.get("type").and_then(|t| t.as_str()) {
115 Some(t) => t,
116 None => continue,
117 };
118 let id = match el.get("id").and_then(|i| i.as_u64()) {
119 Some(i) => i,
120 None => continue,
121 };
122
123 let el_iri = format!("https://www.openstreetmap.org/{el_type}/{id}");
124 let subject = generate_60bit_token(el_iri.as_bytes());
125 let kind =
126 generate_60bit_token(format!("https://www.openstreetmap.org/{el_type}").as_bytes());
127
128 quins.push(quin(subject, ty, kind));
130
131 quins.push(quin(
133 subject,
134 source,
135 generate_60bit_token(el_iri.as_bytes()),
136 ));
137
138 if let Some(lat) = el.get("lat").and_then(|v| v.as_f64()) {
140 quins.push(quin(subject, lat_p, lat.to_bits()));
141 }
142 if let Some(lon) = el.get("lon").and_then(|v| v.as_f64()) {
143 quins.push(quin(subject, long_p, lon.to_bits()));
144 }
145
146 if let Some(tags) = el.get("tags").and_then(|t| t.as_object()) {
148 for (k, v) in tags {
149 let val = match v.as_str() {
150 Some(s) => s,
151 None => continue,
152 };
153 if k == "name" {
154 quins.push(quin(subject, title, generate_60bit_token(val.as_bytes())));
155 } else {
156 let pred = generate_60bit_token(
157 format!("https://wiki.openstreetmap.org/wiki/Key:{k}").as_bytes(),
158 );
159 quins.push(quin(subject, pred, generate_60bit_token(val.as_bytes())));
160 }
161 }
162 }
163 }
164
165 Ok(quins)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_osm_adapter_egress() {
175 let adapter = OsmAdapter::new(
176 "osm_adapter",
177 "https://overpass-api.de/api/interpreter",
178 "https://tiles.example.com/osm",
179 );
180 let registry = NetworkDisclosureRegistry::new();
181
182 let res1 = adapter.fetch_region((0.0, 0.0, 1.0, 1.0), (0, 0), ®istry);
183 assert!(res1.is_err());
184
185 let mut registry = registry;
186 registry.register_egress(
187 adapter.adapter_id(),
188 "https://overpass-api.de/api/interpreter",
189 "Fetch OSM features via Overpass",
190 "User views map layer",
191 );
192
193 let res2 = adapter.fetch_region((0.0, 0.0, 1.0, 1.0), (0, 0), ®istry);
194 if let Err(e) = res2 {
195 assert!(
196 !e.contains("Consent denied"),
197 "Failed on consent when it should have been granted: {}",
198 e
199 );
200 }
201 }
202
203 #[test]
204 fn test_osm_parse_features() {
205 use crate::lexicon::generate_60bit_token;
206
207 let adapter = OsmAdapter::new(
208 "osm_adapter",
209 "https://overpass-api.de/api/interpreter",
210 "https://tiles.example.com/osm",
211 );
212
213 let body = r#"{
216 "version": 0.6,
217 "generator": "Overpass API",
218 "elements": [
219 {
220 "type": "node",
221 "id": 1,
222 "lat": -37.8183,
223 "lon": 144.9671,
224 "tags": {
225 "name": "Flinders Street Station",
226 "amenity": "station"
227 }
228 },
229 {
230 "type": "way",
231 "id": 2,
232 "nodes": [1, 2, 3],
233 "tags": {
234 "name": "Yarra River",
235 "waterway": "river"
236 }
237 }
238 ]
239 }"#;
240
241 let quins = adapter
242 .parse_features(body)
243 .expect("valid JSON should parse");
244
245 assert_eq!(quins.len(), 10, "unexpected quin count: {}", quins.len());
248
249 let title = generate_60bit_token(b"http://purl.org/dc/terms/title");
251 let node_subject = generate_60bit_token(b"https://www.openstreetmap.org/node/1");
252 let node_name = generate_60bit_token(b"Flinders Street Station");
253
254 assert!(
255 quins.iter().any(|q| {
256 q.subject == node_subject && q.predicate == title && q.object == node_name
257 }),
258 "node TITLE quin missing from parsed output"
259 );
260
261 let empty = adapter.parse_features(r#"{"version":0.6}"#).expect("valid");
263 assert!(empty.is_empty());
264
265 assert!(adapter.parse_features("not json").is_err());
267 }
268}