1pub mod astrometry_adapter;
4pub mod ckan_adapter;
5pub mod dem_adapter;
6pub mod gbif_adapter;
7pub mod ivoa_tap_adapter;
8pub mod ogc_3d_tiles;
9pub mod opendap_adapter;
10pub mod osm_adapter;
11pub mod sparql_adapter;
12pub mod stac_adapter;
13pub mod wms_adapter;
14
15pub mod canvas_defaults;
16
17use std::collections::HashMap;
18
19use crate::net::disclosure::NetworkDisclosureRegistry;
20
21pub use astrometry_adapter::AstrometryAdapter;
22pub use ckan_adapter::CkanAdapter;
23pub use dem_adapter::DemAdapter;
24pub use gbif_adapter::GbifAdapter;
25pub use ivoa_tap_adapter::IvoaTapAdapter;
26pub use ogc_3d_tiles::Ogc3dTilesAdapter;
27pub use opendap_adapter::OpendapAdapter;
28pub use osm_adapter::OsmAdapter;
29pub use sparql_adapter::SparqlAdapter;
30pub use stac_adapter::StacAdapter;
31pub use wms_adapter::WmsAdapter;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum AdapterHttpMethod {
35 Get,
36 Post,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct AdapterHttpRequest {
41 pub method: AdapterHttpMethod,
42 pub url: String,
43 pub body: Option<String>,
44 pub content_type: Option<&'static str>,
45 pub service_label: &'static str,
46}
47
48impl AdapterHttpRequest {
49 pub fn get(url: String, service_label: &'static str) -> Self {
50 Self {
51 method: AdapterHttpMethod::Get,
52 url,
53 body: None,
54 content_type: None,
55 service_label,
56 }
57 }
58
59 pub fn post_form(url: String, body: String, service_label: &'static str) -> Self {
60 Self {
61 method: AdapterHttpMethod::Post,
62 url,
63 body: Some(body),
64 content_type: Some("application/x-www-form-urlencoded"),
65 service_label,
66 }
67 }
68}
69
70#[cfg(not(target_arch = "wasm32"))]
71fn execute_http_request_text(request: &AdapterHttpRequest) -> Result<String, String> {
72 let client = reqwest::blocking::Client::new();
73 let mut builder = match request.method {
74 AdapterHttpMethod::Get => client.get(&request.url),
75 AdapterHttpMethod::Post => client.post(&request.url),
76 };
77 if let Some(content_type) = request.content_type {
78 builder = builder.header("Content-Type", content_type);
79 }
80 if let Some(body) = &request.body {
81 builder = builder.body(body.clone());
82 }
83
84 let resp = builder.send().map_err(|e| e.to_string())?;
85 if !resp.status().is_success() {
86 return Err(format!(
87 "{} API returned error: {}",
88 request.service_label,
89 resp.status()
90 ));
91 }
92 resp.text().map_err(|e| e.to_string())
93}
94
95#[cfg(not(target_arch = "wasm32"))]
96fn execute_http_request_status(request: &AdapterHttpRequest) -> Result<(), String> {
97 execute_http_request_text(request).map(|_| ())
98}
99
100#[cfg(target_arch = "wasm32")]
101async fn execute_http_request_text_async(request: &AdapterHttpRequest) -> Result<String, String> {
102 let client = reqwest::Client::new();
103 let mut builder = match request.method {
104 AdapterHttpMethod::Get => client.get(&request.url),
105 AdapterHttpMethod::Post => client.post(&request.url),
106 };
107 if let Some(content_type) = request.content_type {
108 builder = builder.header("Content-Type", content_type);
109 }
110 if let Some(body) = &request.body {
111 builder = builder.body(body.clone());
112 }
113
114 let resp = builder.send().await.map_err(|e| e.to_string())?;
115 let status = resp.status();
116 if !status.is_success() {
117 return Err(format!(
118 "{} API returned error: {status}",
119 request.service_label
120 ));
121 }
122 resp.text().await.map_err(|e| e.to_string())
123}
124
125#[cfg(target_arch = "wasm32")]
126fn execute_http_request_status(_request: &AdapterHttpRequest) -> Result<(), String> {
127 Err(
128 "Synchronous geospatial HTTP is unavailable on wasm32; call fetch_region_async instead"
129 .to_string(),
130 )
131}
132
133#[cfg(target_arch = "wasm32")]
134fn execute_http_request_text(_request: &AdapterHttpRequest) -> Result<String, String> {
135 Err(
136 "Synchronous geospatial HTTP is unavailable on wasm32; call fetch_region_async instead"
137 .to_string(),
138 )
139}
140
141#[cfg(target_arch = "wasm32")]
142pub async fn fetch_region_async(
143 adapter: &dyn DataAdapter,
144 bbox: (f64, f64, f64, f64),
145 time_range: (u64, u64),
146 registry: &NetworkDisclosureRegistry,
147) -> Result<(), String> {
148 let request = adapter.build_fetch_request(bbox, time_range, registry)?;
149 if adapter.needs_fetch_body() {
150 let body = execute_http_request_text_async(&request).await?;
151 adapter.handle_fetch_body(&body)
152 } else {
153 execute_http_request_text_async(&request).await.map(|_| ())
154 }
155}
156
157pub trait DataAdapter {
158 fn adapter_id(&self) -> &'static str;
160
161 fn build_fetch_request(
165 &self,
166 bbox: (f64, f64, f64, f64),
167 time_range: (u64, u64),
168 registry: &NetworkDisclosureRegistry,
169 ) -> Result<AdapterHttpRequest, String>;
170
171 fn fetch_region(
174 &self,
175 bbox: (f64, f64, f64, f64),
176 time_range: (u64, u64),
177 registry: &NetworkDisclosureRegistry,
178 ) -> Result<(), String> {
179 let request = self.build_fetch_request(bbox, time_range, registry)?;
180 if self.needs_fetch_body() {
181 let body = execute_http_request_text(&request)?;
182 self.handle_fetch_body(&body)
183 } else {
184 execute_http_request_status(&request)
185 }
186 }
187
188 fn needs_fetch_body(&self) -> bool {
191 false
192 }
193
194 fn handle_fetch_body(&self, _body: &str) -> Result<(), String> {
197 Ok(())
198 }
199
200 fn parse_response(&self, _body: &str) -> Result<Vec<crate::NQuin>, String> {
207 Ok(Vec::new())
208 }
209
210 fn fetch_region_features(
215 &self,
216 bbox: (f64, f64, f64, f64),
217 time_range: (u64, u64),
218 registry: &NetworkDisclosureRegistry,
219 ) -> Result<Vec<crate::NQuin>, String> {
220 let request = self.build_fetch_request(bbox, time_range, registry)?;
221 let body = execute_http_request_text(&request)?;
222 self.parse_response(&body)
223 }
224
225 fn primary_endpoint(&self) -> &str;
227
228 fn estimate_tile_count(&self, bbox: (f64, f64, f64, f64)) -> u32;
230}
231
232impl DataAdapter for DemAdapter {
233 fn adapter_id(&self) -> &'static str {
234 "dem_adapter"
235 }
236
237 fn build_fetch_request(
238 &self,
239 bbox: (f64, f64, f64, f64),
240 time_range: (u64, u64),
241 registry: &NetworkDisclosureRegistry,
242 ) -> Result<AdapterHttpRequest, String> {
243 DemAdapter::build_fetch_request(self, bbox, time_range, registry)
244 }
245
246 fn primary_endpoint(&self) -> &str {
247 &self.endpoint
248 }
249
250 fn estimate_tile_count(&self, bbox: (f64, f64, f64, f64)) -> u32 {
251 estimate_raster_tiles(bbox, 14)
252 }
253}
254
255impl DataAdapter for OsmAdapter {
256 fn adapter_id(&self) -> &'static str {
257 "osm_adapter"
258 }
259
260 fn build_fetch_request(
261 &self,
262 bbox: (f64, f64, f64, f64),
263 time_range: (u64, u64),
264 registry: &NetworkDisclosureRegistry,
265 ) -> Result<AdapterHttpRequest, String> {
266 OsmAdapter::build_fetch_request(self, bbox, time_range, registry)
267 }
268
269 fn primary_endpoint(&self) -> &str {
270 &self.overpass_endpoint
271 }
272
273 fn estimate_tile_count(&self, bbox: (f64, f64, f64, f64)) -> u32 {
274 1 + estimate_raster_tiles(bbox, 15)
276 }
277}
278
279impl DataAdapter for WmsAdapter {
280 fn adapter_id(&self) -> &'static str {
281 "wms_adapter"
282 }
283
284 fn build_fetch_request(
285 &self,
286 bbox: (f64, f64, f64, f64),
287 time_range: (u64, u64),
288 registry: &NetworkDisclosureRegistry,
289 ) -> Result<AdapterHttpRequest, String> {
290 WmsAdapter::build_fetch_request(self, bbox, time_range, registry)
291 }
292
293 fn primary_endpoint(&self) -> &str {
294 &self.endpoint
295 }
296
297 fn estimate_tile_count(&self, bbox: (f64, f64, f64, f64)) -> u32 {
298 estimate_raster_tiles(bbox, 12)
300 }
301}
302
303impl DataAdapter for GbifAdapter {
304 fn adapter_id(&self) -> &'static str {
305 "gbif_adapter"
306 }
307
308 fn build_fetch_request(
309 &self,
310 bbox: (f64, f64, f64, f64),
311 time_range: (u64, u64),
312 registry: &NetworkDisclosureRegistry,
313 ) -> Result<AdapterHttpRequest, String> {
314 GbifAdapter::build_fetch_request(self, bbox, time_range, registry)
315 }
316
317 fn primary_endpoint(&self) -> &str {
318 &self.occurrence_endpoint
319 }
320
321 fn estimate_tile_count(&self, bbox: (f64, f64, f64, f64)) -> u32 {
322 let area = (bbox.2 - bbox.0).abs() * (bbox.3 - bbox.1).abs();
324 let pages = (area * 10.0).ceil() as u32;
325 pages.max(1)
326 }
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331pub enum LayerFetchStatus {
332 ConsentDenied,
334 ReadyToFetch,
336 AdapterNotFound,
338}
339
340#[derive(Debug, Clone, PartialEq)]
342pub struct LayerFetchReport {
343 pub adapter_id: String,
344 pub endpoint: String,
345 pub bbox: (f64, f64, f64, f64),
346 pub time_range: (u64, u64),
347 pub estimated_tile_count: u32,
348 pub status: LayerFetchStatus,
349}
350
351pub struct AdapterRegistry {
353 adapters: HashMap<String, Box<dyn DataAdapter>>,
354 disclosure: NetworkDisclosureRegistry,
355}
356
357impl AdapterRegistry {
358 pub fn new() -> Self {
359 Self {
360 adapters: HashMap::new(),
361 disclosure: NetworkDisclosureRegistry::new(),
362 }
363 }
364
365 pub fn disclosure_registry(&self) -> &NetworkDisclosureRegistry {
366 &self.disclosure
367 }
368
369 pub fn disclosure_registry_mut(&mut self) -> &mut NetworkDisclosureRegistry {
370 &mut self.disclosure
371 }
372
373 pub fn register_adapter(&mut self, adapter: Box<dyn DataAdapter>) {
374 let id = adapter.adapter_id().to_string();
375 self.adapters.insert(id, adapter);
376 }
377
378 pub fn fetch_layer(
381 &self,
382 adapter_id: &str,
383 bbox: (f64, f64, f64, f64),
384 time_range: (u64, u64),
385 ) -> Result<LayerFetchReport, String> {
386 let adapter = self
387 .adapters
388 .get(adapter_id)
389 .ok_or_else(|| format!("Adapter '{}' not registered", adapter_id))?;
390
391 let endpoint = adapter.primary_endpoint().to_string();
392 let tile_count = adapter.estimate_tile_count(bbox);
393
394 let status = if self.disclosure.check_egress_consent(adapter_id, &endpoint) {
395 LayerFetchStatus::ReadyToFetch
396 } else {
397 LayerFetchStatus::ConsentDenied
398 };
399
400 Ok(LayerFetchReport {
401 adapter_id: adapter_id.to_string(),
402 endpoint,
403 bbox,
404 time_range,
405 estimated_tile_count: tile_count,
406 status,
407 })
408 }
409}
410
411impl Default for AdapterRegistry {
412 fn default() -> Self {
413 let mut registry = Self::new();
414 canvas_defaults::register_canvas_defaults(&mut registry);
415 registry
416 }
417}
418
419fn estimate_raster_tiles(bbox: (f64, f64, f64, f64), zoom: u8) -> u32 {
421 let (x1, y1, x2, y2) = bbox;
422 let n = 1u32 << zoom;
423 let tx1 = ((x1 + 180.0) / 360.0 * n as f64).floor() as u32;
424 let tx2 = ((x2 + 180.0) / 360.0 * n as f64).ceil() as u32;
425 let ty1 = ((1.0
426 - (y2.to_radians().tan() + 1.0 / y2.to_radians().cos()).ln() / std::f64::consts::PI)
427 / 2.0
428 * n as f64)
429 .floor() as u32;
430 let ty2 = ((1.0
431 - (y1.to_radians().tan() + 1.0 / y1.to_radians().cos()).ln() / std::f64::consts::PI)
432 / 2.0
433 * n as f64)
434 .ceil() as u32;
435 let tiles_x = tx2.saturating_sub(tx1).max(1);
436 let tiles_y = ty2.saturating_sub(ty1).max(1);
437 tiles_x.saturating_mul(tiles_y)
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_adapter_registry_consent_denied() {
446 let mut registry = AdapterRegistry::new();
447 registry.register_adapter(Box::new(DemAdapter::new(
448 "dem_adapter",
449 "https://elevation.example.com",
450 )));
451
452 let report = registry
453 .fetch_layer("dem_adapter", (0.0, 0.0, 1.0, 1.0), (0, 0))
454 .expect("report");
455
456 assert_eq!(report.status, LayerFetchStatus::ConsentDenied);
457 assert_eq!(report.endpoint, "https://elevation.example.com");
458 assert!(report.estimated_tile_count >= 1);
459 }
460
461 #[test]
462 fn test_adapter_registry_consent_granted() {
463 let mut registry = AdapterRegistry::new();
464 registry.register_adapter(Box::new(OsmAdapter::new(
465 "osm_adapter",
466 "https://overpass-api.de/api/interpreter",
467 "https://tiles.example.com/osm",
468 )));
469 registry.disclosure_registry_mut().register_egress(
470 "osm_adapter",
471 "https://overpass-api.de/api/interpreter",
472 "Fetch OSM features",
473 "User pans map",
474 );
475
476 let report = registry
477 .fetch_layer("osm_adapter", (-0.1, 51.4, 0.1, 51.6), (0, 0))
478 .expect("report");
479
480 assert_eq!(report.status, LayerFetchStatus::ReadyToFetch);
481 assert!(report.estimated_tile_count >= 1);
482 }
483
484 #[test]
485 fn test_adapter_registry_unknown_adapter() {
486 let registry = AdapterRegistry::new();
487 let err = registry
488 .fetch_layer("unknown_adapter", (0.0, 0.0, 1.0, 1.0), (0, 0))
489 .unwrap_err();
490 assert!(err.contains("not registered"));
491 }
492}