1use 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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
12pub struct TileCoord {
13 pub z: u32,
15
16 pub x: u32,
18
19 pub y: u32,
21}
22
23impl TileCoord {
24 pub fn new(z: u32, x: u32, y: u32) -> Self {
26 Self { z, x, y }
27 }
28
29 pub fn max_coord(z: u32) -> u32 {
31 (1 << z) - 1
32 }
33
34 pub fn is_valid(&self) -> bool {
36 let max = Self::max_coord(self.z);
37 self.x <= max && self.y <= max
38 }
39
40 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 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 pub fn to_url(&self, base_url: &str) -> String {
73 format!("{}/{}/{}/{}", base_url, self.z, self.x, self.y)
74 }
75}
76
77#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
79pub struct BoundingBox {
80 pub min_lon: f64,
82
83 pub min_lat: f64,
85
86 pub max_lon: f64,
88
89 pub max_lat: f64,
91}
92
93impl BoundingBox {
94 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 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 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 pub fn width(&self) -> f64 {
125 self.max_lon - self.min_lon
126 }
127
128 pub fn height(&self) -> f64 {
130 self.max_lat - self.min_lat
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct GeospatialCacheConfig {
137 pub tile_cache_name: String,
139
140 pub vector_cache_name: String,
142
143 pub raster_cache_name: String,
145
146 pub max_zoom: u32,
148
149 pub min_zoom: u32,
151
152 pub tile_strategy: StrategyType,
154
155 pub vector_strategy: StrategyType,
157
158 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
177pub struct GeospatialCache {
179 config: GeospatialCacheConfig,
180 tile_cache: CacheManager,
181 vector_cache: CacheManager,
182 raster_cache: CacheManager,
183}
184
185impl GeospatialCache {
186 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 pub fn with_defaults() -> Self {
202 Self::new(GeospatialCacheConfig::default())
203 }
204
205 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 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 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 pub fn get_tiles_in_bbox(&self, bbox: &BoundingBox, zoom: u32) -> Vec<TileCoord> {
253 let mut tiles = Vec::new();
254
255 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 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 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 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 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 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 pub async fn clear_tiles(&self) -> Result<()> {
307 self.tile_cache.clear().await
308 }
309
310 pub async fn clear_vector(&self) -> Result<()> {
312 self.vector_cache.clear().await
313 }
314
315 pub async fn clear_raster(&self) -> Result<()> {
317 self.raster_cache.clear().await
318 }
319
320 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 fn is_zoom_cacheable(&self, zoom: u32) -> bool {
330 zoom >= self.config.min_zoom && zoom <= self.config.max_zoom
331 }
332
333 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 fn create_tile_request(&self, url: &str) -> Result<Request> {
341 self.create_request(url)
342 }
343
344 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 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}