use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
use wasm_bindgen::prelude::*;
use crate::error::{TileCacheError, WasmError, WasmResult};
pub const DEFAULT_MAX_CACHE_SIZE: usize = 100 * 1024 * 1024;
#[allow(dead_code)]
pub const DEFAULT_TILE_SIZE: u32 = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TileCoord {
pub level: u32,
pub x: u32,
pub y: u32,
}
impl TileCoord {
pub const fn new(level: u32, x: u32, y: u32) -> Self {
Self { level, x, y }
}
pub const fn parent(self) -> Option<Self> {
if self.level == 0 {
return None;
}
Some(Self {
level: self.level - 1,
x: self.x / 2,
y: self.y / 2,
})
}
pub const fn children(self) -> [Self; 4] {
let level = self.level + 1;
let x2 = self.x * 2;
let y2 = self.y * 2;
[
Self {
level,
x: x2,
y: y2,
},
Self {
level,
x: x2 + 1,
y: y2,
},
Self {
level,
x: x2,
y: y2 + 1,
},
Self {
level,
x: x2 + 1,
y: y2 + 1,
},
]
}
pub fn key(&self) -> String {
format!("{}/{}/{}", self.level, self.x, self.y)
}
pub fn from_key(key: &str) -> Option<Self> {
let parts: Vec<&str> = key.split('/').collect();
if parts.len() != 3 {
return None;
}
let level = parts[0].parse().ok()?;
let x = parts[1].parse().ok()?;
let y = parts[2].parse().ok()?;
Some(Self::new(level, x, y))
}
pub const fn is_valid(&self, max_level: u32, max_x: u32, max_y: u32) -> bool {
self.level <= max_level && self.x < max_x && self.y < max_y
}
pub const fn neighbors(&self) -> [Option<Self>; 8] {
let level = self.level;
let x = self.x;
let y = self.y;
[
if x > 0 && y > 0 {
Some(Self::new(level, x - 1, y - 1))
} else {
None
},
if y > 0 {
Some(Self::new(level, x, y - 1))
} else {
None
},
if y > 0 {
Some(Self::new(level, x + 1, y - 1))
} else {
None
},
if x > 0 {
Some(Self::new(level, x - 1, y))
} else {
None
},
Some(Self::new(level, x + 1, y)),
if x > 0 {
Some(Self::new(level, x - 1, y + 1))
} else {
None
},
Some(Self::new(level, x, y + 1)),
Some(Self::new(level, x + 1, y + 1)),
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileBounds {
pub min_x: u32,
pub min_y: u32,
pub max_x: u32,
pub max_y: u32,
}
impl TileBounds {
pub const fn new(min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> Self {
Self {
min_x,
min_y,
max_x,
max_y,
}
}
pub const fn contains(&self, coord: &TileCoord) -> bool {
coord.x >= self.min_x
&& coord.x < self.max_x
&& coord.y >= self.min_y
&& coord.y < self.max_y
}
pub const fn count(&self) -> u64 {
(self.max_x - self.min_x) as u64 * (self.max_y - self.min_y) as u64
}
pub fn iter(&self) -> TileBoundsIter {
TileBoundsIter {
bounds: *self,
current_x: self.min_x,
current_y: self.min_y,
}
}
}
pub struct TileBoundsIter {
bounds: TileBounds,
current_x: u32,
current_y: u32,
}
impl Iterator for TileBoundsIter {
type Item = (u32, u32);
fn next(&mut self) -> Option<Self::Item> {
if self.current_y >= self.bounds.max_y {
return None;
}
let result = (self.current_x, self.current_y);
self.current_x += 1;
if self.current_x >= self.bounds.max_x {
self.current_x = self.bounds.min_x;
self.current_y += 1;
}
Some(result)
}
}
#[derive(Debug, Clone)]
pub struct TilePyramid {
pub width: u64,
pub height: u64,
pub tile_width: u32,
pub tile_height: u32,
pub num_levels: u32,
pub tiles_per_level: Vec<(u32, u32)>,
}
impl TilePyramid {
pub fn new(width: u64, height: u64, tile_width: u32, tile_height: u32) -> Self {
let mut tiles_per_level = Vec::new();
let mut level_width = width;
let mut level_height = height;
let mut num_levels = 0;
while level_width > 0 && level_height > 0 {
let tiles_x = level_width.div_ceil(u64::from(tile_width)) as u32;
let tiles_y = level_height.div_ceil(u64::from(tile_height)) as u32;
tiles_per_level.push((tiles_x, tiles_y));
num_levels += 1;
if tiles_x == 1 && tiles_y == 1 {
break;
}
level_width /= 2;
level_height /= 2;
}
Self {
width,
height,
tile_width,
tile_height,
num_levels,
tiles_per_level,
}
}
pub fn tiles_at_level(&self, level: u32) -> Option<(u32, u32)> {
self.tiles_per_level.get(level as usize).copied()
}
pub fn bounds_at_level(&self, level: u32) -> Option<TileBounds> {
self.tiles_at_level(level)
.map(|(tiles_x, tiles_y)| TileBounds::new(0, 0, tiles_x, tiles_y))
}
pub fn is_valid_coord(&self, coord: &TileCoord) -> bool {
if coord.level >= self.num_levels {
return false;
}
if let Some((tiles_x, tiles_y)) = self.tiles_at_level(coord.level) {
coord.x < tiles_x && coord.y < tiles_y
} else {
false
}
}
pub fn total_tiles(&self) -> u64 {
self.tiles_per_level
.iter()
.map(|(x, y)| u64::from(*x) * u64::from(*y))
.sum()
}
}
#[derive(Debug, Clone)]
pub struct CachedTile {
pub coord: TileCoord,
pub data: Vec<u8>,
pub size: usize,
pub last_access: f64,
pub loaded_at: f64,
pub access_count: u64,
}
impl CachedTile {
pub fn new(coord: TileCoord, data: Vec<u8>, timestamp: f64) -> Self {
let size = data.len();
Self {
coord,
data,
size,
last_access: timestamp,
loaded_at: timestamp,
access_count: 1,
}
}
pub fn access(&mut self, timestamp: f64) {
self.last_access = timestamp;
self.access_count += 1;
}
}
pub struct TileCache {
cache: HashMap<String, CachedTile>,
lru_queue: VecDeque<String>,
current_size: usize,
max_size: usize,
hit_count: u64,
miss_count: u64,
}
impl TileCache {
pub fn new(max_size: usize) -> Self {
Self {
cache: HashMap::new(),
lru_queue: VecDeque::new(),
current_size: 0,
max_size,
hit_count: 0,
miss_count: 0,
}
}
pub fn with_default_size() -> Self {
Self::new(DEFAULT_MAX_CACHE_SIZE)
}
pub fn get(&mut self, coord: &TileCoord, timestamp: f64) -> Option<Vec<u8>> {
let key = coord.key();
if let Some(tile) = self.cache.get_mut(&key) {
tile.access(timestamp);
self.hit_count += 1;
if let Some(pos) = self.lru_queue.iter().position(|k| k == &key) {
self.lru_queue.remove(pos);
}
self.lru_queue.push_back(key);
Some(tile.data.clone())
} else {
self.miss_count += 1;
None
}
}
pub fn put(&mut self, coord: TileCoord, data: Vec<u8>, timestamp: f64) -> WasmResult<()> {
let key = coord.key();
let tile_size = data.len();
while !self.lru_queue.is_empty() && self.current_size + tile_size >= self.max_size {
self.evict_oldest()?;
}
if tile_size > self.max_size {
return Err(WasmError::TileCache(TileCacheError::Full {
current_size: self.current_size,
max_size: self.max_size,
}));
}
if let Some(old_tile) = self.cache.remove(&key) {
self.current_size -= old_tile.size;
if let Some(pos) = self.lru_queue.iter().position(|k| k == &key) {
self.lru_queue.remove(pos);
}
}
let tile = CachedTile::new(coord, data, timestamp);
self.current_size += tile_size;
self.cache.insert(key.clone(), tile);
self.lru_queue.push_back(key);
Ok(())
}
fn evict_oldest(&mut self) -> WasmResult<()> {
if let Some(key) = self.lru_queue.pop_front() {
if let Some(tile) = self.cache.remove(&key) {
self.current_size -= tile.size;
Ok(())
} else {
Err(WasmError::TileCache(TileCacheError::EvictionFailed {
message: format!("Tile {key} not found in cache"),
}))
}
} else {
Err(WasmError::TileCache(TileCacheError::EvictionFailed {
message: "No tiles to evict".to_string(),
}))
}
}
pub fn contains(&self, coord: &TileCoord) -> bool {
self.cache.contains_key(&coord.key())
}
pub fn remove(&mut self, coord: &TileCoord) -> Option<Vec<u8>> {
let key = coord.key();
if let Some(tile) = self.cache.remove(&key) {
self.current_size -= tile.size;
if let Some(pos) = self.lru_queue.iter().position(|k| k == &key) {
self.lru_queue.remove(pos);
}
Some(tile.data)
} else {
None
}
}
pub fn clear(&mut self) {
self.cache.clear();
self.lru_queue.clear();
self.current_size = 0;
}
pub fn stats(&self) -> CacheStats {
CacheStats {
current_size: self.current_size,
max_size: self.max_size,
entry_count: self.cache.len(),
hit_count: self.hit_count,
miss_count: self.miss_count,
}
}
pub fn hit_rate(&self) -> f64 {
let total = self.hit_count + self.miss_count;
if total == 0 {
0.0
} else {
self.hit_count as f64 / total as f64
}
}
pub fn prefetch_list(&self, coords: &[TileCoord]) -> Vec<TileCoord> {
coords
.iter()
.filter(|coord| !self.contains(coord))
.copied()
.collect()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct CacheStats {
pub current_size: usize,
pub max_size: usize,
pub entry_count: usize,
pub hit_count: u64,
pub miss_count: u64,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.hit_count + self.miss_count;
if total == 0 {
0.0
} else {
self.hit_count as f64 / total as f64
}
}
pub fn utilization(&self) -> f64 {
if self.max_size == 0 {
0.0
} else {
self.current_size as f64 / self.max_size as f64
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrefetchStrategy {
None,
Neighbors,
Radius(u32),
Pyramid,
Directional {
dx: i32,
dy: i32,
count: u32,
},
}
pub struct TilePrefetcher {
strategy: PrefetchStrategy,
pyramid: TilePyramid,
}
impl TilePrefetcher {
pub const fn new(strategy: PrefetchStrategy, pyramid: TilePyramid) -> Self {
Self { strategy, pyramid }
}
pub fn prefetch_for(&self, center: &TileCoord) -> Vec<TileCoord> {
match self.strategy {
PrefetchStrategy::None => vec![],
PrefetchStrategy::Neighbors => self.prefetch_neighbors(center),
PrefetchStrategy::Radius(radius) => self.prefetch_radius(center, radius),
PrefetchStrategy::Pyramid => self.prefetch_pyramid(center),
PrefetchStrategy::Directional { dx, dy, count } => {
self.prefetch_directional(center, dx, dy, count)
}
}
}
fn prefetch_neighbors(&self, center: &TileCoord) -> Vec<TileCoord> {
center
.neighbors()
.iter()
.filter_map(|&n| n)
.filter(|coord| self.pyramid.is_valid_coord(coord))
.collect()
}
fn prefetch_radius(&self, center: &TileCoord, radius: u32) -> Vec<TileCoord> {
let mut tiles = Vec::new();
let radius_i32 = radius as i32;
for dy in -radius_i32..=radius_i32 {
for dx in -radius_i32..=radius_i32 {
if dx == 0 && dy == 0 {
continue;
}
let dist_sq = (dx * dx + dy * dy) as f64;
if dist_sq > (radius as f64 * radius as f64) {
continue;
}
let x = center.x as i32 + dx;
let y = center.y as i32 + dy;
if x >= 0 && y >= 0 {
let coord = TileCoord::new(center.level, x as u32, y as u32);
if self.pyramid.is_valid_coord(&coord) {
tiles.push(coord);
}
}
}
}
tiles
}
fn prefetch_pyramid(&self, center: &TileCoord) -> Vec<TileCoord> {
let mut tiles = Vec::new();
if let Some(parent) = center.parent() {
if self.pyramid.is_valid_coord(&parent) {
tiles.push(parent);
}
}
for child in center.children() {
if self.pyramid.is_valid_coord(&child) {
tiles.push(child);
}
}
tiles
}
fn prefetch_directional(
&self,
center: &TileCoord,
dx: i32,
dy: i32,
count: u32,
) -> Vec<TileCoord> {
let mut tiles = Vec::new();
for i in 1..=count {
let x = center.x as i32 + dx * i as i32;
let y = center.y as i32 + dy * i as i32;
if x >= 0 && y >= 0 {
let coord = TileCoord::new(center.level, x as u32, y as u32);
if self.pyramid.is_valid_coord(&coord) {
tiles.push(coord);
} else {
break;
}
}
}
tiles
}
}
#[wasm_bindgen]
pub struct WasmTileCache {
cache: TileCache,
}
#[wasm_bindgen]
impl WasmTileCache {
#[wasm_bindgen(constructor)]
pub fn new(max_size_mb: usize) -> Self {
let max_size = max_size_mb * 1024 * 1024;
Self {
cache: TileCache::new(max_size),
}
}
#[wasm_bindgen(js_name = getStats)]
pub fn get_stats(&self) -> String {
serde_json::to_string(&self.cache.stats()).unwrap_or_default()
}
#[wasm_bindgen]
pub fn clear(&mut self) {
self.cache.clear();
}
#[wasm_bindgen(js_name = hitRate)]
pub fn hit_rate(&self) -> f64 {
self.cache.hit_rate()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tile_coord() {
let coord = TileCoord::new(5, 10, 20);
assert_eq!(coord.level, 5);
assert_eq!(coord.x, 10);
assert_eq!(coord.y, 20);
assert_eq!(coord.key(), "5/10/20");
}
#[test]
fn test_tile_coord_parent() {
let coord = TileCoord::new(5, 10, 20);
let parent = coord.parent().expect("Should have parent");
assert_eq!(parent.level, 4);
assert_eq!(parent.x, 5);
assert_eq!(parent.y, 10);
let root = TileCoord::new(0, 0, 0);
assert!(root.parent().is_none());
}
#[test]
fn test_tile_coord_children() {
let coord = TileCoord::new(5, 10, 20);
let children = coord.children();
assert_eq!(children.len(), 4);
assert_eq!(children[0], TileCoord::new(6, 20, 40));
assert_eq!(children[1], TileCoord::new(6, 21, 40));
assert_eq!(children[2], TileCoord::new(6, 20, 41));
assert_eq!(children[3], TileCoord::new(6, 21, 41));
}
#[test]
fn test_tile_pyramid() {
let pyramid = TilePyramid::new(4096, 2048, 256, 256);
assert_eq!(pyramid.tile_width, 256);
assert_eq!(pyramid.tile_height, 256);
let (tiles_x, tiles_y) = pyramid.tiles_at_level(0).expect("Level 0 exists");
assert_eq!(tiles_x, 16); assert_eq!(tiles_y, 8); }
#[test]
fn test_tile_cache() {
let mut cache = TileCache::new(1024);
let coord = TileCoord::new(0, 0, 0);
let data = vec![1, 2, 3, 4];
cache
.put(coord, data.clone(), 0.0)
.expect("Put should succeed");
let retrieved = cache.get(&coord, 0.0).expect("Should find tile");
assert_eq!(retrieved, data);
let stats = cache.stats();
assert_eq!(stats.entry_count, 1);
assert_eq!(stats.hit_count, 1);
}
#[test]
fn test_cache_eviction() {
let mut cache = TileCache::new(10); let coord1 = TileCoord::new(0, 0, 0);
let coord2 = TileCoord::new(0, 0, 1);
cache
.put(coord1, vec![1, 2, 3, 4, 5], 0.0)
.expect("Put should succeed");
cache
.put(coord2, vec![6, 7, 8, 9, 10], 1.0)
.expect("Put should succeed");
assert!(!cache.contains(&coord1));
assert!(cache.contains(&coord2));
}
#[test]
fn test_tile_bounds() {
let bounds = TileBounds::new(0, 0, 10, 10);
assert_eq!(bounds.count(), 100);
let coord_in = TileCoord::new(0, 5, 5);
let coord_out = TileCoord::new(0, 15, 15);
assert!(bounds.contains(&coord_in));
assert!(!bounds.contains(&coord_out));
}
#[test]
fn test_prefetch_neighbors() {
let pyramid = TilePyramid::new(1024, 1024, 256, 256);
let prefetcher = TilePrefetcher::new(PrefetchStrategy::Neighbors, pyramid);
let center = TileCoord::new(0, 1, 1);
let to_prefetch = prefetcher.prefetch_for(¢er);
assert!(!to_prefetch.is_empty());
assert!(to_prefetch.len() <= 8);
}
#[test]
fn test_from_key() {
let coord = TileCoord::new(5, 10, 20);
let key = coord.key();
let parsed = TileCoord::from_key(&key).expect("Should parse");
assert_eq!(parsed, coord);
assert!(TileCoord::from_key("invalid").is_none());
assert!(TileCoord::from_key("1/2").is_none());
}
}