Skip to main content

oxigdal_pwa/cache/
geospatial.rs

1//! Geospatial-specific caching strategies and tile caching.
2
3use crate::cache::{CacheManager, StrategyType};
4use crate::error::{PwaError, Result};
5use serde::{Deserialize, Serialize};
6use wasm_bindgen::JsCast;
7use wasm_bindgen_futures::JsFuture;
8use web_sys::{Request, Response};
9
10/// Tile coordinate for map tiles.
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
12pub struct TileCoord {
13    /// Zoom level
14    pub z: u32,
15
16    /// X coordinate
17    pub x: u32,
18
19    /// Y coordinate
20    pub y: u32,
21}
22
23impl TileCoord {
24    /// Create a new tile coordinate.
25    pub fn new(z: u32, x: u32, y: u32) -> Self {
26        Self { z, x, y }
27    }
28
29    /// Get the maximum tile coordinate for a zoom level.
30    pub fn max_coord(z: u32) -> u32 {
31        (1 << z) - 1
32    }
33
34    /// Check if this coordinate is valid for its zoom level.
35    pub fn is_valid(&self) -> bool {
36        let max = Self::max_coord(self.z);
37        self.x <= max && self.y <= max
38    }
39
40    /// Get parent tile at zoom level z-1.
41    pub fn parent(&self) -> Option<Self> {
42        if self.z == 0 {
43            None
44        } else {
45            Some(Self {
46                z: self.z - 1,
47                x: self.x / 2,
48                y: self.y / 2,
49            })
50        }
51    }
52
53    /// Get child tiles at zoom level z+1.
54    pub fn children(&self) -> [Self; 4] {
55        let z = self.z + 1;
56        let x = self.x * 2;
57        let y = self.y * 2;
58
59        [
60            Self { z, x, y },
61            Self { z, x: x + 1, y },
62            Self { z, x, y: y + 1 },
63            Self {
64                z,
65                x: x + 1,
66                y: y + 1,
67            },
68        ]
69    }
70
71    /// Convert to standard tile URL format.
72    pub fn to_url(&self, base_url: &str) -> String {
73        format!("{}/{}/{}/{}", base_url, self.z, self.x, self.y)
74    }
75}
76
77/// Bounding box for geospatial data.
78#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
79pub struct BoundingBox {
80    /// Minimum longitude
81    pub min_lon: f64,
82
83    /// Minimum latitude
84    pub min_lat: f64,
85
86    /// Maximum longitude
87    pub max_lon: f64,
88
89    /// Maximum latitude
90    pub max_lat: f64,
91}
92
93impl BoundingBox {
94    /// Create a new bounding box.
95    pub fn new(min_lon: f64, min_lat: f64, max_lon: f64, max_lat: f64) -> Result<Self> {
96        if min_lon >= max_lon || min_lat >= max_lat {
97            return Err(PwaError::ConfigurationError(
98                "Invalid bounding box coordinates".to_string(),
99            ));
100        }
101
102        Ok(Self {
103            min_lon,
104            min_lat,
105            max_lon,
106            max_lat,
107        })
108    }
109
110    /// Check if a point is inside the bounding box.
111    pub fn contains(&self, lon: f64, lat: f64) -> bool {
112        lon >= self.min_lon && lon <= self.max_lon && lat >= self.min_lat && lat <= self.max_lat
113    }
114
115    /// Get the center point of the bounding box.
116    pub fn center(&self) -> (f64, f64) {
117        (
118            (self.min_lon + self.max_lon) / 2.0,
119            (self.min_lat + self.max_lat) / 2.0,
120        )
121    }
122
123    /// Get the width of the bounding box.
124    pub fn width(&self) -> f64 {
125        self.max_lon - self.min_lon
126    }
127
128    /// Get the height of the bounding box.
129    pub fn height(&self) -> f64 {
130        self.max_lat - self.min_lat
131    }
132}
133
134/// Geospatial cache configuration.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct GeospatialCacheConfig {
137    /// Cache name for map tiles
138    pub tile_cache_name: String,
139
140    /// Cache name for vector data
141    pub vector_cache_name: String,
142
143    /// Cache name for raster data
144    pub raster_cache_name: String,
145
146    /// Maximum zoom level to cache
147    pub max_zoom: u32,
148
149    /// Minimum zoom level to cache
150    pub min_zoom: u32,
151
152    /// Tile cache strategy
153    pub tile_strategy: StrategyType,
154
155    /// Vector data cache strategy
156    pub vector_strategy: StrategyType,
157
158    /// Raster data cache strategy
159    pub raster_strategy: StrategyType,
160}
161
162impl Default for GeospatialCacheConfig {
163    fn default() -> Self {
164        Self {
165            tile_cache_name: "geo-tiles".to_string(),
166            vector_cache_name: "geo-vector".to_string(),
167            raster_cache_name: "geo-raster".to_string(),
168            max_zoom: 18,
169            min_zoom: 0,
170            tile_strategy: StrategyType::CacheFirst,
171            vector_strategy: StrategyType::NetworkFirst,
172            raster_strategy: StrategyType::CacheFirst,
173        }
174    }
175}
176
177/// Geospatial cache for map tiles and geospatial data.
178pub struct GeospatialCache {
179    config: GeospatialCacheConfig,
180    tile_cache: CacheManager,
181    vector_cache: CacheManager,
182    raster_cache: CacheManager,
183}
184
185impl GeospatialCache {
186    /// Create a new geospatial cache.
187    pub fn new(config: GeospatialCacheConfig) -> Self {
188        let tile_cache = CacheManager::new(&config.tile_cache_name);
189        let vector_cache = CacheManager::new(&config.vector_cache_name);
190        let raster_cache = CacheManager::new(&config.raster_cache_name);
191
192        Self {
193            config,
194            tile_cache,
195            vector_cache,
196            raster_cache,
197        }
198    }
199
200    /// Create a geospatial cache with default configuration.
201    pub fn with_defaults() -> Self {
202        Self::new(GeospatialCacheConfig::default())
203    }
204
205    /// Cache a map tile.
206    pub async fn cache_tile(&self, coord: &TileCoord, url: &str) -> Result<Response> {
207        if !self.is_zoom_cacheable(coord.z) {
208            return self.fetch_tile(url).await;
209        }
210
211        let request = self.create_tile_request(url)?;
212
213        // Use cache-first strategy for tiles
214        if let Some(response) = self.tile_cache.match_request(&request).await? {
215            return Ok(response);
216        }
217
218        let response = self.fetch_tile(url).await?;
219        self.tile_cache.put(&request, &response).await?;
220
221        Ok(response)
222    }
223
224    /// Prefetch tiles for a bounding box and zoom range.
225    pub async fn prefetch_tiles(
226        &self,
227        bbox: &BoundingBox,
228        zoom_range: std::ops::Range<u32>,
229        base_url: &str,
230    ) -> Result<Vec<TileCoord>> {
231        let mut cached_tiles = Vec::new();
232
233        for z in zoom_range {
234            if !self.is_zoom_cacheable(z) {
235                continue;
236            }
237
238            let tiles = self.get_tiles_in_bbox(bbox, z);
239
240            for coord in tiles {
241                let url = coord.to_url(base_url);
242                if self.cache_tile(&coord, &url).await.is_ok() {
243                    cached_tiles.push(coord);
244                }
245            }
246        }
247
248        Ok(cached_tiles)
249    }
250
251    /// Get all tile coordinates that intersect a bounding box at a given zoom.
252    pub fn get_tiles_in_bbox(&self, bbox: &BoundingBox, zoom: u32) -> Vec<TileCoord> {
253        let mut tiles = Vec::new();
254
255        // Convert lat/lon to tile coordinates
256        let min_tile = Self::lonlat_to_tile(bbox.min_lon, bbox.max_lat, zoom);
257        let max_tile = Self::lonlat_to_tile(bbox.max_lon, bbox.min_lat, zoom);
258
259        for x in min_tile.x..=max_tile.x {
260            for y in min_tile.y..=max_tile.y {
261                let coord = TileCoord::new(zoom, x, y);
262                if coord.is_valid() {
263                    tiles.push(coord);
264                }
265            }
266        }
267
268        tiles
269    }
270
271    /// Convert lon/lat to tile coordinates.
272    fn lonlat_to_tile(lon: f64, lat: f64, zoom: u32) -> TileCoord {
273        let n = 2_f64.powi(zoom as i32);
274        let x = ((lon + 180.0) / 360.0 * n) as u32;
275        let lat_rad = lat.to_radians();
276        let y = ((1.0 - lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0 * n) as u32;
277
278        TileCoord::new(zoom, x, y)
279    }
280
281    /// Cache vector data (GeoJSON, etc.).
282    pub async fn cache_vector_data(&self, url: &str, data: &Response) -> Result<()> {
283        let request = self.create_request(url)?;
284        self.vector_cache.put(&request, data).await
285    }
286
287    /// Get cached vector data.
288    pub async fn get_vector_data(&self, url: &str) -> Result<Option<Response>> {
289        let request = self.create_request(url)?;
290        self.vector_cache.match_request(&request).await
291    }
292
293    /// Cache raster data (COG, GeoTIFF, etc.).
294    pub async fn cache_raster_data(&self, url: &str, data: &Response) -> Result<()> {
295        let request = self.create_request(url)?;
296        self.raster_cache.put(&request, data).await
297    }
298
299    /// Get cached raster data.
300    pub async fn get_raster_data(&self, url: &str) -> Result<Option<Response>> {
301        let request = self.create_request(url)?;
302        self.raster_cache.match_request(&request).await
303    }
304
305    /// Clear tile cache.
306    pub async fn clear_tiles(&self) -> Result<()> {
307        self.tile_cache.clear().await
308    }
309
310    /// Clear vector cache.
311    pub async fn clear_vector(&self) -> Result<()> {
312        self.vector_cache.clear().await
313    }
314
315    /// Clear raster cache.
316    pub async fn clear_raster(&self) -> Result<()> {
317        self.raster_cache.clear().await
318    }
319
320    /// Clear all geospatial caches.
321    pub async fn clear_all(&self) -> Result<()> {
322        self.clear_tiles().await?;
323        self.clear_vector().await?;
324        self.clear_raster().await?;
325        Ok(())
326    }
327
328    /// Check if a zoom level is cacheable.
329    fn is_zoom_cacheable(&self, zoom: u32) -> bool {
330        zoom >= self.config.min_zoom && zoom <= self.config.max_zoom
331    }
332
333    /// Create a request for a URL.
334    fn create_request(&self, url: &str) -> Result<Request> {
335        Request::new_with_str(url)
336            .map_err(|e| PwaError::InvalidUrl(format!("Failed to create request: {:?}", e)))
337    }
338
339    /// Create a tile request.
340    fn create_tile_request(&self, url: &str) -> Result<Request> {
341        self.create_request(url)
342    }
343
344    /// Fetch a tile from the network.
345    async fn fetch_tile(&self, url: &str) -> Result<Response> {
346        let window = web_sys::window()
347            .ok_or_else(|| PwaError::InvalidState("No window available".to_string()))?;
348
349        let promise = window.fetch_with_str(url);
350        let result = JsFuture::from(promise)
351            .await
352            .map_err(|e| PwaError::FetchFailed(format!("Tile fetch failed: {:?}", e)))?;
353
354        result
355            .dyn_into::<Response>()
356            .map_err(|_| PwaError::FetchFailed("Invalid response object".to_string()))
357    }
358
359    /// Get the configuration.
360    pub fn config(&self) -> &GeospatialCacheConfig {
361        &self.config
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_tile_coord() {
371        let coord = TileCoord::new(10, 512, 512);
372        assert_eq!(coord.z, 10);
373        assert_eq!(coord.x, 512);
374        assert_eq!(coord.y, 512);
375        assert!(coord.is_valid());
376    }
377
378    #[test]
379    fn test_tile_parent() {
380        let coord = TileCoord::new(10, 512, 512);
381        let parent = coord
382            .parent()
383            .ok_or("")
384            .unwrap_or_else(|_| TileCoord::new(0, 0, 0));
385        assert_eq!(parent.z, 9);
386        assert_eq!(parent.x, 256);
387        assert_eq!(parent.y, 256);
388    }
389
390    #[test]
391    fn test_tile_children() {
392        let coord = TileCoord::new(5, 10, 10);
393        let children = coord.children();
394        assert_eq!(children.len(), 4);
395        assert_eq!(children[0], TileCoord::new(6, 20, 20));
396        assert_eq!(children[1], TileCoord::new(6, 21, 20));
397        assert_eq!(children[2], TileCoord::new(6, 20, 21));
398        assert_eq!(children[3], TileCoord::new(6, 21, 21));
399    }
400
401    #[test]
402    fn test_tile_url() {
403        let coord = TileCoord::new(10, 512, 512);
404        let url = coord.to_url("https://tiles.example.com");
405        assert_eq!(url, "https://tiles.example.com/10/512/512");
406    }
407
408    #[test]
409    fn test_bounding_box() -> Result<()> {
410        let bbox = BoundingBox::new(-180.0, -85.0, 180.0, 85.0)?;
411        assert_eq!(bbox.width(), 360.0);
412        assert_eq!(bbox.height(), 170.0);
413
414        let (center_lon, center_lat) = bbox.center();
415        assert_eq!(center_lon, 0.0);
416        assert_eq!(center_lat, 0.0);
417
418        assert!(bbox.contains(0.0, 0.0));
419        assert!(bbox.contains(-100.0, 50.0));
420        assert!(!bbox.contains(0.0, 90.0));
421
422        Ok(())
423    }
424
425    #[test]
426    fn test_lonlat_to_tile() {
427        let coord = GeospatialCache::lonlat_to_tile(0.0, 0.0, 0);
428        assert_eq!(coord.z, 0);
429
430        let coord = GeospatialCache::lonlat_to_tile(0.0, 0.0, 1);
431        assert_eq!(coord.z, 1);
432    }
433
434    #[test]
435    fn test_geospatial_cache_config() {
436        let config = GeospatialCacheConfig::default();
437        assert_eq!(config.tile_cache_name, "geo-tiles");
438        assert_eq!(config.max_zoom, 18);
439        assert_eq!(config.min_zoom, 0);
440    }
441}