Skip to main content

oxigdal_wasm/
tile.rs

1//! Tile management and caching system
2//!
3//! This module provides comprehensive tile coordinate systems, LRU caching,
4//! pyramid management, prefetching, and memory management for WASM geospatial applications.
5//!
6//! # Overview
7//!
8//! The tile module implements the core tile management infrastructure for oxigdal-wasm:
9//!
10//! - **Tile Coordinates**: XYZ tile coordinate system (level, x, y)
11//! - **Tile Pyramids**: Multi-resolution tile pyramids for efficient zooming
12//! - **LRU Caching**: Least Recently Used cache with configurable size limits
13//! - **Prefetching**: Intelligent prefetching strategies for smooth interaction
14//! - **Memory Management**: Automatic eviction when memory limits are reached
15//!
16//! # Tile Coordinate System
17//!
18//! Tiles are addressed using a standard XYZ coordinate system:
19//!
20//! ```text
21//! Level 0:     [0,0,0]                  (1 tile)
22//!
23//! Level 1:  [1,0,0] [1,1,0]             (4 tiles)
24//!           [1,0,1] [1,1,1]
25//!
26//! Level 2:  [2,0,0] [2,1,0] [2,2,0] [2,3,0]  (16 tiles)
27//!           [2,0,1] [2,1,1] [2,2,1] [2,3,1]
28//!           [2,0,2] [2,1,2] [2,2,2] [2,3,2]
29//!           [2,0,3] [2,1,3] [2,2,3] [2,3,3]
30//! ```
31//!
32//! Each level has 4^level tiles. The tree structure allows efficient
33//! parent/child relationships for multi-resolution rendering.
34//!
35//! # Tile Pyramid Structure
36//!
37//! A tile pyramid represents the complete multi-resolution structure:
38//!
39//! ```rust
40//! use oxigdal_wasm::TilePyramid;
41//!
42//! // Create pyramid for a 4096x2048 image with 256x256 tiles
43//! let pyramid = TilePyramid::new(4096, 2048, 256, 256);
44//!
45//! // Level 0: 16x8 tiles (full resolution)
46//! // Level 1: 8x4 tiles (2x downsampled)
47//! // Level 2: 4x2 tiles (4x downsampled)
48//! // Level 3: 2x1 tiles (8x downsampled)
49//! // Level 4: 1x1 tile (16x downsampled)
50//!
51//! println!("Levels: {}", pyramid.num_levels);
52//! println!("Total tiles: {}", pyramid.total_tiles());
53//! ```
54//!
55//! # LRU Cache Implementation
56//!
57//! The cache uses a Least Recently Used eviction policy:
58//!
59//! 1. Tiles are stored in a HashMap for O(1) lookups
60//! 2. Access order is tracked in a VecDeque
61//! 3. When cache is full, oldest tile is evicted
62//! 4. Cache hits update the access time
63//!
64//! ```rust
65//! use oxigdal_wasm::{TileCache, TileCoord};
66//!
67//! let mut cache = TileCache::new(100 * 1024 * 1024); // 100 MB
68//!
69//! let coord = TileCoord::new(0, 0, 0);
70//! let tile_data = vec![0u8; 256 * 256 * 4];
71//!
72//! // Cache the tile
73//! cache.put(coord, tile_data.clone(), 0.0).expect("Put failed");
74//!
75//! // Retrieve from cache
76//! let cached = cache.get(&coord, 1.0).expect("Cache miss");
77//!
78//! // Check statistics
79//! let stats = cache.stats();
80//! println!("Hit rate: {:.2}%", stats.hit_rate() * 100.0);
81//! ```
82//!
83//! # Prefetching Strategies
84//!
85//! Multiple prefetching strategies are supported:
86//!
87//! ## Neighbors
88//! Prefetches 8 immediately adjacent tiles:
89//! ```text
90//! [NW] [N]  [NE]
91//! [W]  [C]  [E]
92//! [SW] [S]  [SE]
93//! ```
94//!
95//! ## Radius
96//! Prefetches all tiles within a given radius (Euclidean distance).
97//!
98//! ## Pyramid
99//! Prefetches parent tile (for zoom out) and 4 child tiles (for zoom in).
100//!
101//! ## Directional
102//! Prefetches tiles in a specific direction (for panning).
103//!
104//! # Performance Characteristics
105//!
106//! - Cache lookup: O(1) average, O(n) worst case
107//! - Cache insertion: O(1) average, O(n) worst case
108//! - Cache eviction: O(1)
109//! - Pyramid traversal: O(log n) for parent/child lookup
110//!
111//! Memory overhead:
112//! - TileCoord: 12 bytes (3 u32 values)
113//! - CachedTile: ~48 bytes + data size
114//! - Cache entry: ~80 bytes + data size
115//!
116//! # Example: Complete Tile Loading
117//!
118//! ```rust
119//! use oxigdal_wasm::{TileCache, TileCoord, TilePrefetcher, PrefetchStrategy, TilePyramid};
120//!
121//! // Setup
122//! let mut cache = TileCache::new(50 * 1024 * 1024); // 50 MB cache
123//! let pyramid = TilePyramid::new(4096, 4096, 256, 256);
124//! let prefetcher = TilePrefetcher::new(
125//!     PrefetchStrategy::Neighbors,
126//!     pyramid.clone()
127//! );
128//!
129//! // Load center tile
130//! let center = TileCoord::new(0, 8, 8); // Center of level 0
131//! let tile_data = vec![0u8; 256 * 256 * 4]; // Simulated tile data
132//!
133//! cache.put(center, tile_data, 0.0).expect("Cache put failed");
134//!
135//! // Get tiles to prefetch
136//! let to_prefetch = prefetcher.prefetch_for(&center);
137//! println!("Prefetching {} tiles", to_prefetch.len());
138//!
139//! // Load prefetch tiles (in real code, fetch from network)
140//! for coord in to_prefetch {
141//!     if !cache.contains(&coord) {
142//!         // Fetch and cache tile
143//!         // let tile = fetch_tile(coord).await;
144//!         // cache.put(coord, tile, timestamp)?;
145//!     }
146//! }
147//! ```
148
149use serde::{Deserialize, Serialize};
150use std::collections::{HashMap, VecDeque};
151use std::hash::Hash;
152use wasm_bindgen::prelude::*;
153
154use crate::error::{TileCacheError, WasmError, WasmResult};
155
156/// Maximum cache size in bytes (default: 100MB)
157pub const DEFAULT_MAX_CACHE_SIZE: usize = 100 * 1024 * 1024;
158
159/// Default tile size in pixels
160#[allow(dead_code)]
161pub const DEFAULT_TILE_SIZE: u32 = 256;
162
163/// Tile coordinate in a pyramid
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
165pub struct TileCoord {
166    /// Zoom level (0 = most zoomed out)
167    pub level: u32,
168    /// Column index (x)
169    pub x: u32,
170    /// Row index (y)
171    pub y: u32,
172}
173
174impl TileCoord {
175    /// Creates a new tile coordinate
176    pub const fn new(level: u32, x: u32, y: u32) -> Self {
177        Self { level, x, y }
178    }
179
180    /// Returns the parent tile coordinate (one level up)
181    pub const fn parent(self) -> Option<Self> {
182        if self.level == 0 {
183            return None;
184        }
185
186        Some(Self {
187            level: self.level - 1,
188            x: self.x / 2,
189            y: self.y / 2,
190        })
191    }
192
193    /// Returns the four child tile coordinates (one level down)
194    pub const fn children(self) -> [Self; 4] {
195        let level = self.level + 1;
196        let x2 = self.x * 2;
197        let y2 = self.y * 2;
198
199        [
200            Self {
201                level,
202                x: x2,
203                y: y2,
204            },
205            Self {
206                level,
207                x: x2 + 1,
208                y: y2,
209            },
210            Self {
211                level,
212                x: x2,
213                y: y2 + 1,
214            },
215            Self {
216                level,
217                x: x2 + 1,
218                y: y2 + 1,
219            },
220        ]
221    }
222
223    /// Returns the tile key for caching
224    pub fn key(&self) -> String {
225        format!("{}/{}/{}", self.level, self.x, self.y)
226    }
227
228    /// Parses a tile key
229    pub fn from_key(key: &str) -> Option<Self> {
230        let parts: Vec<&str> = key.split('/').collect();
231        if parts.len() != 3 {
232            return None;
233        }
234
235        let level = parts[0].parse().ok()?;
236        let x = parts[1].parse().ok()?;
237        let y = parts[2].parse().ok()?;
238
239        Some(Self::new(level, x, y))
240    }
241
242    /// Checks if this tile is valid for the given bounds
243    pub const fn is_valid(&self, max_level: u32, max_x: u32, max_y: u32) -> bool {
244        self.level <= max_level && self.x < max_x && self.y < max_y
245    }
246
247    /// Returns neighboring tile coordinates
248    pub const fn neighbors(&self) -> [Option<Self>; 8] {
249        let level = self.level;
250        let x = self.x;
251        let y = self.y;
252
253        [
254            // Top-left
255            if x > 0 && y > 0 {
256                Some(Self::new(level, x - 1, y - 1))
257            } else {
258                None
259            },
260            // Top
261            if y > 0 {
262                Some(Self::new(level, x, y - 1))
263            } else {
264                None
265            },
266            // Top-right
267            if y > 0 {
268                Some(Self::new(level, x + 1, y - 1))
269            } else {
270                None
271            },
272            // Left
273            if x > 0 {
274                Some(Self::new(level, x - 1, y))
275            } else {
276                None
277            },
278            // Right
279            Some(Self::new(level, x + 1, y)),
280            // Bottom-left
281            if x > 0 {
282                Some(Self::new(level, x - 1, y + 1))
283            } else {
284                None
285            },
286            // Bottom
287            Some(Self::new(level, x, y + 1)),
288            // Bottom-right
289            Some(Self::new(level, x + 1, y + 1)),
290        ]
291    }
292}
293
294/// Tile bounds in the pyramid
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub struct TileBounds {
297    /// Minimum x coordinate
298    pub min_x: u32,
299    /// Minimum y coordinate
300    pub min_y: u32,
301    /// Maximum x coordinate (exclusive)
302    pub max_x: u32,
303    /// Maximum y coordinate (exclusive)
304    pub max_y: u32,
305}
306
307impl TileBounds {
308    /// Creates new tile bounds
309    pub const fn new(min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> Self {
310        Self {
311            min_x,
312            min_y,
313            max_x,
314            max_y,
315        }
316    }
317
318    /// Checks if a coordinate is within bounds
319    pub const fn contains(&self, coord: &TileCoord) -> bool {
320        coord.x >= self.min_x
321            && coord.x < self.max_x
322            && coord.y >= self.min_y
323            && coord.y < self.max_y
324    }
325
326    /// Returns the number of tiles in these bounds
327    pub const fn count(&self) -> u64 {
328        (self.max_x - self.min_x) as u64 * (self.max_y - self.min_y) as u64
329    }
330
331    /// Returns an iterator over tile coordinates in these bounds
332    pub fn iter(&self) -> TileBoundsIter {
333        TileBoundsIter {
334            bounds: *self,
335            current_x: self.min_x,
336            current_y: self.min_y,
337        }
338    }
339}
340
341/// Iterator over tile coordinates in bounds
342pub struct TileBoundsIter {
343    bounds: TileBounds,
344    current_x: u32,
345    current_y: u32,
346}
347
348impl Iterator for TileBoundsIter {
349    type Item = (u32, u32);
350
351    fn next(&mut self) -> Option<Self::Item> {
352        if self.current_y >= self.bounds.max_y {
353            return None;
354        }
355
356        let result = (self.current_x, self.current_y);
357
358        self.current_x += 1;
359        if self.current_x >= self.bounds.max_x {
360            self.current_x = self.bounds.min_x;
361            self.current_y += 1;
362        }
363
364        Some(result)
365    }
366}
367
368/// Tile pyramid metadata
369#[derive(Debug, Clone)]
370pub struct TilePyramid {
371    /// Image width in pixels
372    pub width: u64,
373    /// Image height in pixels
374    pub height: u64,
375    /// Tile width in pixels
376    pub tile_width: u32,
377    /// Tile height in pixels
378    pub tile_height: u32,
379    /// Number of pyramid levels
380    pub num_levels: u32,
381    /// Tiles per level (width, height)
382    pub tiles_per_level: Vec<(u32, u32)>,
383}
384
385impl TilePyramid {
386    /// Creates a new tile pyramid
387    pub fn new(width: u64, height: u64, tile_width: u32, tile_height: u32) -> Self {
388        let mut tiles_per_level = Vec::new();
389        let mut level_width = width;
390        let mut level_height = height;
391        let mut num_levels = 0;
392
393        while level_width > 0 && level_height > 0 {
394            let tiles_x = level_width.div_ceil(u64::from(tile_width)) as u32;
395            let tiles_y = level_height.div_ceil(u64::from(tile_height)) as u32;
396            tiles_per_level.push((tiles_x, tiles_y));
397            num_levels += 1;
398
399            // Stop when we reach a single tile (pyramid top)
400            if tiles_x == 1 && tiles_y == 1 {
401                break;
402            }
403
404            level_width /= 2;
405            level_height /= 2;
406        }
407
408        Self {
409            width,
410            height,
411            tile_width,
412            tile_height,
413            num_levels,
414            tiles_per_level,
415        }
416    }
417
418    /// Returns the number of tiles at a given level
419    pub fn tiles_at_level(&self, level: u32) -> Option<(u32, u32)> {
420        self.tiles_per_level.get(level as usize).copied()
421    }
422
423    /// Returns the tile bounds for a given level
424    pub fn bounds_at_level(&self, level: u32) -> Option<TileBounds> {
425        self.tiles_at_level(level)
426            .map(|(tiles_x, tiles_y)| TileBounds::new(0, 0, tiles_x, tiles_y))
427    }
428
429    /// Checks if a tile coordinate is valid
430    pub fn is_valid_coord(&self, coord: &TileCoord) -> bool {
431        if coord.level >= self.num_levels {
432            return false;
433        }
434
435        if let Some((tiles_x, tiles_y)) = self.tiles_at_level(coord.level) {
436            coord.x < tiles_x && coord.y < tiles_y
437        } else {
438            false
439        }
440    }
441
442    /// Returns the total number of tiles in the pyramid
443    pub fn total_tiles(&self) -> u64 {
444        self.tiles_per_level
445            .iter()
446            .map(|(x, y)| u64::from(*x) * u64::from(*y))
447            .sum()
448    }
449}
450
451/// Cached tile data
452#[derive(Debug, Clone)]
453pub struct CachedTile {
454    /// Tile coordinate
455    pub coord: TileCoord,
456    /// Tile data
457    pub data: Vec<u8>,
458    /// Size in bytes
459    pub size: usize,
460    /// Access timestamp (for LRU)
461    pub last_access: f64,
462    /// Load timestamp
463    pub loaded_at: f64,
464    /// Number of accesses
465    pub access_count: u64,
466}
467
468impl CachedTile {
469    /// Creates a new cached tile
470    pub fn new(coord: TileCoord, data: Vec<u8>, timestamp: f64) -> Self {
471        let size = data.len();
472        Self {
473            coord,
474            data,
475            size,
476            last_access: timestamp,
477            loaded_at: timestamp,
478            access_count: 1,
479        }
480    }
481
482    /// Updates access information
483    pub fn access(&mut self, timestamp: f64) {
484        self.last_access = timestamp;
485        self.access_count += 1;
486    }
487}
488
489/// LRU tile cache
490pub struct TileCache {
491    /// Cache entries
492    cache: HashMap<String, CachedTile>,
493    /// LRU queue (tile keys in access order)
494    lru_queue: VecDeque<String>,
495    /// Current cache size in bytes
496    current_size: usize,
497    /// Maximum cache size in bytes
498    max_size: usize,
499    /// Cache hit count
500    hit_count: u64,
501    /// Cache miss count
502    miss_count: u64,
503}
504
505impl TileCache {
506    /// Creates a new tile cache
507    pub fn new(max_size: usize) -> Self {
508        Self {
509            cache: HashMap::new(),
510            lru_queue: VecDeque::new(),
511            current_size: 0,
512            max_size,
513            hit_count: 0,
514            miss_count: 0,
515        }
516    }
517
518    /// Creates a new tile cache with default size
519    pub fn with_default_size() -> Self {
520        Self::new(DEFAULT_MAX_CACHE_SIZE)
521    }
522
523    /// Gets a tile from the cache
524    pub fn get(&mut self, coord: &TileCoord, timestamp: f64) -> Option<Vec<u8>> {
525        let key = coord.key();
526
527        if let Some(tile) = self.cache.get_mut(&key) {
528            tile.access(timestamp);
529            self.hit_count += 1;
530
531            // Move to end of LRU queue
532            if let Some(pos) = self.lru_queue.iter().position(|k| k == &key) {
533                self.lru_queue.remove(pos);
534            }
535            self.lru_queue.push_back(key);
536
537            Some(tile.data.clone())
538        } else {
539            self.miss_count += 1;
540            None
541        }
542    }
543
544    /// Puts a tile into the cache
545    pub fn put(&mut self, coord: TileCoord, data: Vec<u8>, timestamp: f64) -> WasmResult<()> {
546        let key = coord.key();
547        let tile_size = data.len();
548
549        // Evict tiles if necessary when adding would exceed or reach capacity
550        // Using >= because cache should maintain strict size limit
551        while !self.lru_queue.is_empty() && self.current_size + tile_size >= self.max_size {
552            self.evict_oldest()?;
553        }
554
555        // Check if we can fit the tile
556        if tile_size > self.max_size {
557            return Err(WasmError::TileCache(TileCacheError::Full {
558                current_size: self.current_size,
559                max_size: self.max_size,
560            }));
561        }
562
563        // Add or update tile
564        if let Some(old_tile) = self.cache.remove(&key) {
565            self.current_size -= old_tile.size;
566            if let Some(pos) = self.lru_queue.iter().position(|k| k == &key) {
567                self.lru_queue.remove(pos);
568            }
569        }
570
571        let tile = CachedTile::new(coord, data, timestamp);
572        self.current_size += tile_size;
573        self.cache.insert(key.clone(), tile);
574        self.lru_queue.push_back(key);
575
576        Ok(())
577    }
578
579    /// Evicts the oldest tile from the cache
580    fn evict_oldest(&mut self) -> WasmResult<()> {
581        if let Some(key) = self.lru_queue.pop_front() {
582            if let Some(tile) = self.cache.remove(&key) {
583                self.current_size -= tile.size;
584                Ok(())
585            } else {
586                Err(WasmError::TileCache(TileCacheError::EvictionFailed {
587                    message: format!("Tile {key} not found in cache"),
588                }))
589            }
590        } else {
591            Err(WasmError::TileCache(TileCacheError::EvictionFailed {
592                message: "No tiles to evict".to_string(),
593            }))
594        }
595    }
596
597    /// Checks if a tile is in the cache
598    pub fn contains(&self, coord: &TileCoord) -> bool {
599        self.cache.contains_key(&coord.key())
600    }
601
602    /// Removes a tile from the cache
603    pub fn remove(&mut self, coord: &TileCoord) -> Option<Vec<u8>> {
604        let key = coord.key();
605        if let Some(tile) = self.cache.remove(&key) {
606            self.current_size -= tile.size;
607            if let Some(pos) = self.lru_queue.iter().position(|k| k == &key) {
608                self.lru_queue.remove(pos);
609            }
610            Some(tile.data)
611        } else {
612            None
613        }
614    }
615
616    /// Clears the entire cache
617    pub fn clear(&mut self) {
618        self.cache.clear();
619        self.lru_queue.clear();
620        self.current_size = 0;
621    }
622
623    /// Returns cache statistics
624    pub fn stats(&self) -> CacheStats {
625        CacheStats {
626            current_size: self.current_size,
627            max_size: self.max_size,
628            entry_count: self.cache.len(),
629            hit_count: self.hit_count,
630            miss_count: self.miss_count,
631        }
632    }
633
634    /// Returns the cache hit rate
635    pub fn hit_rate(&self) -> f64 {
636        let total = self.hit_count + self.miss_count;
637        if total == 0 {
638            0.0
639        } else {
640            self.hit_count as f64 / total as f64
641        }
642    }
643
644    /// Prefetches tiles in the given coordinates
645    pub fn prefetch_list(&self, coords: &[TileCoord]) -> Vec<TileCoord> {
646        coords
647            .iter()
648            .filter(|coord| !self.contains(coord))
649            .copied()
650            .collect()
651    }
652}
653
654/// Cache statistics
655#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
656pub struct CacheStats {
657    /// Current cache size in bytes
658    pub current_size: usize,
659    /// Maximum cache size in bytes
660    pub max_size: usize,
661    /// Number of cached entries
662    pub entry_count: usize,
663    /// Number of cache hits
664    pub hit_count: u64,
665    /// Number of cache misses
666    pub miss_count: u64,
667}
668
669impl CacheStats {
670    /// Returns the cache hit rate
671    pub fn hit_rate(&self) -> f64 {
672        let total = self.hit_count + self.miss_count;
673        if total == 0 {
674            0.0
675        } else {
676            self.hit_count as f64 / total as f64
677        }
678    }
679
680    /// Returns the cache utilization as a fraction
681    pub fn utilization(&self) -> f64 {
682        if self.max_size == 0 {
683            0.0
684        } else {
685            self.current_size as f64 / self.max_size as f64
686        }
687    }
688}
689
690/// Prefetching strategy
691#[derive(Debug, Clone, Copy, PartialEq, Eq)]
692pub enum PrefetchStrategy {
693    /// No prefetching
694    None,
695    /// Prefetch immediate neighbors
696    Neighbors,
697    /// Prefetch in a radius
698    Radius(u32),
699    /// Prefetch pyramid (parent and children)
700    Pyramid,
701    /// Prefetch along a direction
702    Directional {
703        /// X direction delta
704        dx: i32,
705        /// Y direction delta
706        dy: i32,
707        /// Number of tiles to prefetch
708        count: u32,
709    },
710}
711
712/// Tile prefetcher
713pub struct TilePrefetcher {
714    /// Prefetch strategy
715    strategy: PrefetchStrategy,
716    /// Pyramid metadata
717    pyramid: TilePyramid,
718}
719
720impl TilePrefetcher {
721    /// Creates a new tile prefetcher
722    pub const fn new(strategy: PrefetchStrategy, pyramid: TilePyramid) -> Self {
723        Self { strategy, pyramid }
724    }
725
726    /// Returns tiles to prefetch for a given center tile
727    pub fn prefetch_for(&self, center: &TileCoord) -> Vec<TileCoord> {
728        match self.strategy {
729            PrefetchStrategy::None => vec![],
730            PrefetchStrategy::Neighbors => self.prefetch_neighbors(center),
731            PrefetchStrategy::Radius(radius) => self.prefetch_radius(center, radius),
732            PrefetchStrategy::Pyramid => self.prefetch_pyramid(center),
733            PrefetchStrategy::Directional { dx, dy, count } => {
734                self.prefetch_directional(center, dx, dy, count)
735            }
736        }
737    }
738
739    /// Prefetches immediate neighbors
740    fn prefetch_neighbors(&self, center: &TileCoord) -> Vec<TileCoord> {
741        center
742            .neighbors()
743            .iter()
744            .filter_map(|&n| n)
745            .filter(|coord| self.pyramid.is_valid_coord(coord))
746            .collect()
747    }
748
749    /// Prefetches tiles in a radius
750    fn prefetch_radius(&self, center: &TileCoord, radius: u32) -> Vec<TileCoord> {
751        let mut tiles = Vec::new();
752        let radius_i32 = radius as i32;
753
754        for dy in -radius_i32..=radius_i32 {
755            for dx in -radius_i32..=radius_i32 {
756                // Skip the center tile itself
757                if dx == 0 && dy == 0 {
758                    continue;
759                }
760
761                // Calculate Euclidean distance
762                let dist_sq = (dx * dx + dy * dy) as f64;
763                if dist_sq > (radius as f64 * radius as f64) {
764                    continue;
765                }
766
767                let x = center.x as i32 + dx;
768                let y = center.y as i32 + dy;
769
770                if x >= 0 && y >= 0 {
771                    let coord = TileCoord::new(center.level, x as u32, y as u32);
772                    if self.pyramid.is_valid_coord(&coord) {
773                        tiles.push(coord);
774                    }
775                }
776            }
777        }
778
779        tiles
780    }
781
782    /// Prefetches parent and children tiles
783    fn prefetch_pyramid(&self, center: &TileCoord) -> Vec<TileCoord> {
784        let mut tiles = Vec::new();
785
786        // Add parent
787        if let Some(parent) = center.parent() {
788            if self.pyramid.is_valid_coord(&parent) {
789                tiles.push(parent);
790            }
791        }
792
793        // Add children
794        for child in center.children() {
795            if self.pyramid.is_valid_coord(&child) {
796                tiles.push(child);
797            }
798        }
799
800        tiles
801    }
802
803    /// Prefetches tiles in a direction
804    fn prefetch_directional(
805        &self,
806        center: &TileCoord,
807        dx: i32,
808        dy: i32,
809        count: u32,
810    ) -> Vec<TileCoord> {
811        let mut tiles = Vec::new();
812
813        for i in 1..=count {
814            let x = center.x as i32 + dx * i as i32;
815            let y = center.y as i32 + dy * i as i32;
816
817            if x >= 0 && y >= 0 {
818                let coord = TileCoord::new(center.level, x as u32, y as u32);
819                if self.pyramid.is_valid_coord(&coord) {
820                    tiles.push(coord);
821                } else {
822                    break;
823                }
824            }
825        }
826
827        tiles
828    }
829}
830
831/// WASM bindings for tile management
832#[wasm_bindgen]
833pub struct WasmTileCache {
834    cache: TileCache,
835}
836
837#[wasm_bindgen]
838impl WasmTileCache {
839    /// Creates a new tile cache
840    #[wasm_bindgen(constructor)]
841    pub fn new(max_size_mb: usize) -> Self {
842        let max_size = max_size_mb * 1024 * 1024;
843        Self {
844            cache: TileCache::new(max_size),
845        }
846    }
847
848    /// Gets cache statistics as JSON
849    #[wasm_bindgen(js_name = getStats)]
850    pub fn get_stats(&self) -> String {
851        serde_json::to_string(&self.cache.stats()).unwrap_or_default()
852    }
853
854    /// Clears the cache
855    #[wasm_bindgen]
856    pub fn clear(&mut self) {
857        self.cache.clear();
858    }
859
860    /// Returns the cache hit rate
861    #[wasm_bindgen(js_name = hitRate)]
862    pub fn hit_rate(&self) -> f64 {
863        self.cache.hit_rate()
864    }
865}
866
867#[cfg(test)]
868mod tests {
869    use super::*;
870
871    #[test]
872    fn test_tile_coord() {
873        let coord = TileCoord::new(5, 10, 20);
874        assert_eq!(coord.level, 5);
875        assert_eq!(coord.x, 10);
876        assert_eq!(coord.y, 20);
877        assert_eq!(coord.key(), "5/10/20");
878    }
879
880    #[test]
881    fn test_tile_coord_parent() {
882        let coord = TileCoord::new(5, 10, 20);
883        let parent = coord.parent().expect("Should have parent");
884        assert_eq!(parent.level, 4);
885        assert_eq!(parent.x, 5);
886        assert_eq!(parent.y, 10);
887
888        let root = TileCoord::new(0, 0, 0);
889        assert!(root.parent().is_none());
890    }
891
892    #[test]
893    fn test_tile_coord_children() {
894        let coord = TileCoord::new(5, 10, 20);
895        let children = coord.children();
896        assert_eq!(children.len(), 4);
897        assert_eq!(children[0], TileCoord::new(6, 20, 40));
898        assert_eq!(children[1], TileCoord::new(6, 21, 40));
899        assert_eq!(children[2], TileCoord::new(6, 20, 41));
900        assert_eq!(children[3], TileCoord::new(6, 21, 41));
901    }
902
903    #[test]
904    fn test_tile_pyramid() {
905        let pyramid = TilePyramid::new(4096, 2048, 256, 256);
906        assert_eq!(pyramid.tile_width, 256);
907        assert_eq!(pyramid.tile_height, 256);
908
909        let (tiles_x, tiles_y) = pyramid.tiles_at_level(0).expect("Level 0 exists");
910        assert_eq!(tiles_x, 16); // 4096 / 256
911        assert_eq!(tiles_y, 8); // 2048 / 256
912    }
913
914    #[test]
915    fn test_tile_cache() {
916        let mut cache = TileCache::new(1024);
917        let coord = TileCoord::new(0, 0, 0);
918        let data = vec![1, 2, 3, 4];
919
920        // Put and get
921        cache
922            .put(coord, data.clone(), 0.0)
923            .expect("Put should succeed");
924        let retrieved = cache.get(&coord, 0.0).expect("Should find tile");
925        assert_eq!(retrieved, data);
926
927        // Check stats
928        let stats = cache.stats();
929        assert_eq!(stats.entry_count, 1);
930        assert_eq!(stats.hit_count, 1);
931    }
932
933    #[test]
934    fn test_cache_eviction() {
935        let mut cache = TileCache::new(10); // Very small cache
936        let coord1 = TileCoord::new(0, 0, 0);
937        let coord2 = TileCoord::new(0, 0, 1);
938
939        cache
940            .put(coord1, vec![1, 2, 3, 4, 5], 0.0)
941            .expect("Put should succeed");
942        cache
943            .put(coord2, vec![6, 7, 8, 9, 10], 1.0)
944            .expect("Put should succeed");
945
946        // First tile should be evicted
947        assert!(!cache.contains(&coord1));
948        assert!(cache.contains(&coord2));
949    }
950
951    #[test]
952    fn test_tile_bounds() {
953        let bounds = TileBounds::new(0, 0, 10, 10);
954        assert_eq!(bounds.count(), 100);
955
956        let coord_in = TileCoord::new(0, 5, 5);
957        let coord_out = TileCoord::new(0, 15, 15);
958
959        assert!(bounds.contains(&coord_in));
960        assert!(!bounds.contains(&coord_out));
961    }
962
963    #[test]
964    fn test_prefetch_neighbors() {
965        let pyramid = TilePyramid::new(1024, 1024, 256, 256);
966        let prefetcher = TilePrefetcher::new(PrefetchStrategy::Neighbors, pyramid);
967
968        let center = TileCoord::new(0, 1, 1);
969        let to_prefetch = prefetcher.prefetch_for(&center);
970
971        // Should prefetch up to 8 neighbors
972        assert!(!to_prefetch.is_empty());
973        assert!(to_prefetch.len() <= 8);
974    }
975
976    #[test]
977    fn test_from_key() {
978        let coord = TileCoord::new(5, 10, 20);
979        let key = coord.key();
980        let parsed = TileCoord::from_key(&key).expect("Should parse");
981        assert_eq!(parsed, coord);
982
983        assert!(TileCoord::from_key("invalid").is_none());
984        assert!(TileCoord::from_key("1/2").is_none());
985    }
986}