use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TileFormat {
Mvt,
Png,
Jpeg,
Webp,
Json,
}
impl TileFormat {
#[must_use]
pub fn extension(&self) -> &'static str {
match self {
TileFormat::Mvt => "mvt",
TileFormat::Png => "png",
TileFormat::Jpeg => "jpg",
TileFormat::Webp => "webp",
TileFormat::Json => "json",
}
}
#[must_use]
pub fn content_type(&self) -> &'static str {
match self {
TileFormat::Mvt => "application/vnd.mapbox-vector-tile",
TileFormat::Png => "image/png",
TileFormat::Jpeg => "image/jpeg",
TileFormat::Webp => "image/webp",
TileFormat::Json => "application/json",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TileKey {
pub z: u8,
pub x: u32,
pub y: u32,
pub layer: String,
pub format: TileFormat,
}
impl TileKey {
pub fn new(z: u8, x: u32, y: u32, layer: impl Into<String>, format: TileFormat) -> Self {
Self {
z,
x,
y,
layer: layer.into(),
format,
}
}
#[must_use]
pub fn path_string(&self) -> String {
format!(
"{}/{}/{}/{}.{}",
self.layer,
self.z,
self.x,
self.y,
self.format.extension()
)
}
#[must_use]
pub fn content_type(&self) -> &'static str {
self.format.content_type()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TileEncoding {
Identity,
Gzip,
Brotli,
}
#[derive(Debug, Clone)]
pub struct CachedTile {
pub key: TileKey,
pub data: Vec<u8>,
pub etag: String,
pub created_at: u64,
pub accessed_at: u64,
pub access_count: u64,
pub size_bytes: u64,
pub encoding: TileEncoding,
}
impl CachedTile {
pub fn new(key: TileKey, data: Vec<u8>, timestamp: u64) -> Self {
let etag = Self::compute_etag(&data);
let size_bytes = data.len() as u64;
Self {
key,
data,
etag,
created_at: timestamp,
accessed_at: timestamp,
access_count: 1,
size_bytes,
encoding: TileEncoding::Identity,
}
}
fn compute_etag(data: &[u8]) -> String {
const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
const FNV_PRIME: u64 = 1_099_511_628_211;
let mut hash = FNV_OFFSET;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
format!("\"{hash:016x}\"")
}
#[must_use]
pub fn is_stale(&self, max_age_secs: u64, now: u64) -> bool {
now >= self.created_at.saturating_add(max_age_secs)
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub entry_count: usize,
pub total_bytes: u64,
pub hit_count: u64,
pub miss_count: u64,
pub eviction_count: u64,
pub hit_rate: f64,
}
pub struct TileCache {
entries: HashMap<TileKey, CachedTile>,
access_order: VecDeque<TileKey>,
pub max_entries: usize,
pub max_bytes: u64,
pub current_bytes: u64,
pub hit_count: u64,
pub miss_count: u64,
pub eviction_count: u64,
}
impl TileCache {
pub fn new(max_entries: usize, max_bytes: u64) -> Self {
Self {
entries: HashMap::new(),
access_order: VecDeque::new(),
max_entries,
max_bytes,
current_bytes: 0,
hit_count: 0,
miss_count: 0,
eviction_count: 0,
}
}
pub fn get(&mut self, key: &TileKey, now: u64) -> Option<&CachedTile> {
if self.entries.contains_key(key) {
self.hit_count += 1;
if let Some(pos) = self.access_order.iter().position(|k| k == key) {
self.access_order.remove(pos);
}
self.access_order.push_back(key.clone());
if let Some(tile) = self.entries.get_mut(key) {
tile.accessed_at = now;
tile.access_count += 1;
}
self.entries.get(key)
} else {
self.miss_count += 1;
None
}
}
pub fn insert(&mut self, tile: CachedTile) {
if let Some(old) = self.entries.remove(&tile.key) {
self.current_bytes = self.current_bytes.saturating_sub(old.size_bytes);
if let Some(pos) = self.access_order.iter().position(|k| k == &old.key) {
self.access_order.remove(pos);
}
}
let key = tile.key.clone();
self.current_bytes += tile.size_bytes;
self.entries.insert(key.clone(), tile);
self.access_order.push_back(key);
while self.entries.len() > self.max_entries
|| (self.current_bytes > self.max_bytes && self.entries.len() > 1)
{
self.evict_lru();
}
}
pub fn invalidate(&mut self, key: &TileKey) -> bool {
if let Some(tile) = self.entries.remove(key) {
self.current_bytes = self.current_bytes.saturating_sub(tile.size_bytes);
if let Some(pos) = self.access_order.iter().position(|k| k == key) {
self.access_order.remove(pos);
}
true
} else {
false
}
}
pub fn invalidate_layer(&mut self, layer: &str) -> u64 {
let keys_to_remove: Vec<TileKey> = self
.entries
.keys()
.filter(|k| k.layer == layer)
.cloned()
.collect();
let count = keys_to_remove.len() as u64;
for key in keys_to_remove {
self.invalidate(&key);
}
count
}
pub fn invalidate_zoom_range(&mut self, min_z: u8, max_z: u8) -> u64 {
let keys_to_remove: Vec<TileKey> = self
.entries
.keys()
.filter(|k| k.z >= min_z && k.z <= max_z)
.cloned()
.collect();
let count = keys_to_remove.len() as u64;
for key in keys_to_remove {
self.invalidate(&key);
}
count
}
#[must_use]
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
}
}
#[must_use]
pub fn stats(&self) -> CacheStats {
CacheStats {
entry_count: self.entries.len(),
total_bytes: self.current_bytes,
hit_count: self.hit_count,
miss_count: self.miss_count,
eviction_count: self.eviction_count,
hit_rate: self.hit_rate(),
}
}
fn evict_lru(&mut self) {
if let Some(key) = self.access_order.pop_front() {
if let Some(tile) = self.entries.remove(&key) {
self.current_bytes = self.current_bytes.saturating_sub(tile.size_bytes);
self.eviction_count += 1;
}
}
}
}
pub struct TilePrefetcher {
pub radius: u8,
pub max_zoom_delta: u8,
}
impl TilePrefetcher {
pub fn new(radius: u8) -> Self {
Self {
radius,
max_zoom_delta: 1,
}
}
pub fn neighbors(&self, key: &TileKey) -> Vec<TileKey> {
let mut result: Vec<TileKey> = Vec::new();
let same_zoom_ring = self.ring_at_zoom(key, key.z, self.radius);
result.extend(same_zoom_ring);
for delta in 1..=self.max_zoom_delta {
if key.z >= delta {
let lower_zoom = key.z - delta;
let scaled_x = key.x >> delta;
let scaled_y = key.y >> delta;
let parent_key = TileKey::new(
lower_zoom,
scaled_x,
scaled_y,
key.layer.clone(),
key.format.clone(),
);
let ring = self.ring_at_zoom(&parent_key, lower_zoom, self.radius);
for t in ring {
if !result.iter().any(|r| r == &t) {
result.push(t);
}
}
}
let upper_zoom = key.z.saturating_add(delta);
if upper_zoom != key.z {
let scaled_x = key.x << delta;
let scaled_y = key.y << delta;
let child_key = TileKey::new(
upper_zoom,
scaled_x,
scaled_y,
key.layer.clone(),
key.format.clone(),
);
let ring = self.ring_at_zoom(&child_key, upper_zoom, self.radius);
for t in ring {
if !result.iter().any(|r| r == &t) {
result.push(t);
}
}
}
}
result.retain(|t| t != key);
result
}
pub fn ring_at_zoom(&self, key: &TileKey, zoom: u8, radius: u8) -> Vec<TileKey> {
let r = radius as i64;
let mut tiles = Vec::new();
for dx in -r..=r {
for dy in -r..=r {
let nx = if dx < 0 {
key.x.saturating_sub((-dx) as u32)
} else {
key.x.saturating_add(dx as u32)
};
let ny = if dy < 0 {
key.y.saturating_sub((-dy) as u32)
} else {
key.y.saturating_add(dy as u32)
};
tiles.push(TileKey::new(
zoom,
nx,
ny,
key.layer.clone(),
key.format.clone(),
));
}
}
tiles
}
}