oxigdal_services/tile_cache/
cache.rs1use std::collections::{HashMap, VecDeque};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum TileFormat {
13 Mvt,
15 Png,
17 Jpeg,
19 Webp,
21 Json,
23}
24
25impl TileFormat {
26 #[must_use]
28 pub fn extension(&self) -> &'static str {
29 match self {
30 TileFormat::Mvt => "mvt",
31 TileFormat::Png => "png",
32 TileFormat::Jpeg => "jpg",
33 TileFormat::Webp => "webp",
34 TileFormat::Json => "json",
35 }
36 }
37
38 #[must_use]
40 pub fn content_type(&self) -> &'static str {
41 match self {
42 TileFormat::Mvt => "application/vnd.mapbox-vector-tile",
43 TileFormat::Png => "image/png",
44 TileFormat::Jpeg => "image/jpeg",
45 TileFormat::Webp => "image/webp",
46 TileFormat::Json => "application/json",
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub struct TileKey {
56 pub z: u8,
58 pub x: u32,
60 pub y: u32,
62 pub layer: String,
64 pub format: TileFormat,
66}
67
68impl TileKey {
69 pub fn new(z: u8, x: u32, y: u32, layer: impl Into<String>, format: TileFormat) -> Self {
71 Self {
72 z,
73 x,
74 y,
75 layer: layer.into(),
76 format,
77 }
78 }
79
80 #[must_use]
82 pub fn path_string(&self) -> String {
83 format!(
84 "{}/{}/{}/{}.{}",
85 self.layer,
86 self.z,
87 self.x,
88 self.y,
89 self.format.extension()
90 )
91 }
92
93 #[must_use]
95 pub fn content_type(&self) -> &'static str {
96 self.format.content_type()
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum TileEncoding {
105 Identity,
107 Gzip,
109 Brotli,
111}
112
113#[derive(Debug, Clone)]
117pub struct CachedTile {
118 pub key: TileKey,
120 pub data: Vec<u8>,
122 pub etag: String,
124 pub created_at: u64,
126 pub accessed_at: u64,
128 pub access_count: u64,
130 pub size_bytes: u64,
132 pub encoding: TileEncoding,
134}
135
136impl CachedTile {
137 pub fn new(key: TileKey, data: Vec<u8>, timestamp: u64) -> Self {
139 let etag = Self::compute_etag(&data);
140 let size_bytes = data.len() as u64;
141 Self {
142 key,
143 data,
144 etag,
145 created_at: timestamp,
146 accessed_at: timestamp,
147 access_count: 1,
148 size_bytes,
149 encoding: TileEncoding::Identity,
150 }
151 }
152
153 fn compute_etag(data: &[u8]) -> String {
155 const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
156 const FNV_PRIME: u64 = 1_099_511_628_211;
157 let mut hash = FNV_OFFSET;
158 for &byte in data {
159 hash ^= u64::from(byte);
160 hash = hash.wrapping_mul(FNV_PRIME);
161 }
162 format!("\"{hash:016x}\"")
163 }
164
165 #[must_use]
167 pub fn is_stale(&self, max_age_secs: u64, now: u64) -> bool {
168 now >= self.created_at.saturating_add(max_age_secs)
169 }
170}
171
172#[derive(Debug, Clone)]
176pub struct CacheStats {
177 pub entry_count: usize,
179 pub total_bytes: u64,
181 pub hit_count: u64,
183 pub miss_count: u64,
185 pub eviction_count: u64,
187 pub hit_rate: f64,
189}
190
191pub struct TileCache {
198 entries: HashMap<TileKey, CachedTile>,
199 access_order: VecDeque<TileKey>,
200 pub max_entries: usize,
202 pub max_bytes: u64,
204 pub current_bytes: u64,
206 pub hit_count: u64,
208 pub miss_count: u64,
210 pub eviction_count: u64,
212}
213
214impl TileCache {
215 pub fn new(max_entries: usize, max_bytes: u64) -> Self {
217 Self {
218 entries: HashMap::new(),
219 access_order: VecDeque::new(),
220 max_entries,
221 max_bytes,
222 current_bytes: 0,
223 hit_count: 0,
224 miss_count: 0,
225 eviction_count: 0,
226 }
227 }
228
229 pub fn get(&mut self, key: &TileKey, now: u64) -> Option<&CachedTile> {
232 if self.entries.contains_key(key) {
233 self.hit_count += 1;
234 if let Some(pos) = self.access_order.iter().position(|k| k == key) {
236 self.access_order.remove(pos);
237 }
238 self.access_order.push_back(key.clone());
239 if let Some(tile) = self.entries.get_mut(key) {
241 tile.accessed_at = now;
242 tile.access_count += 1;
243 }
244 self.entries.get(key)
245 } else {
246 self.miss_count += 1;
247 None
248 }
249 }
250
251 pub fn insert(&mut self, tile: CachedTile) {
253 if let Some(old) = self.entries.remove(&tile.key) {
255 self.current_bytes = self.current_bytes.saturating_sub(old.size_bytes);
256 if let Some(pos) = self.access_order.iter().position(|k| k == &old.key) {
257 self.access_order.remove(pos);
258 }
259 }
260
261 let key = tile.key.clone();
262 self.current_bytes += tile.size_bytes;
263 self.entries.insert(key.clone(), tile);
264 self.access_order.push_back(key);
265
266 while self.entries.len() > self.max_entries
268 || (self.current_bytes > self.max_bytes && self.entries.len() > 1)
269 {
270 self.evict_lru();
271 }
272 }
273
274 pub fn invalidate(&mut self, key: &TileKey) -> bool {
276 if let Some(tile) = self.entries.remove(key) {
277 self.current_bytes = self.current_bytes.saturating_sub(tile.size_bytes);
278 if let Some(pos) = self.access_order.iter().position(|k| k == key) {
279 self.access_order.remove(pos);
280 }
281 true
282 } else {
283 false
284 }
285 }
286
287 pub fn invalidate_layer(&mut self, layer: &str) -> u64 {
289 let keys_to_remove: Vec<TileKey> = self
290 .entries
291 .keys()
292 .filter(|k| k.layer == layer)
293 .cloned()
294 .collect();
295 let count = keys_to_remove.len() as u64;
296 for key in keys_to_remove {
297 self.invalidate(&key);
298 }
299 count
300 }
301
302 pub fn invalidate_zoom_range(&mut self, min_z: u8, max_z: u8) -> u64 {
305 let keys_to_remove: Vec<TileKey> = self
306 .entries
307 .keys()
308 .filter(|k| k.z >= min_z && k.z <= max_z)
309 .cloned()
310 .collect();
311 let count = keys_to_remove.len() as u64;
312 for key in keys_to_remove {
313 self.invalidate(&key);
314 }
315 count
316 }
317
318 #[must_use]
320 pub fn hit_rate(&self) -> f64 {
321 let total = self.hit_count + self.miss_count;
322 if total == 0 {
323 0.0
324 } else {
325 self.hit_count as f64 / total as f64
326 }
327 }
328
329 #[must_use]
331 pub fn stats(&self) -> CacheStats {
332 CacheStats {
333 entry_count: self.entries.len(),
334 total_bytes: self.current_bytes,
335 hit_count: self.hit_count,
336 miss_count: self.miss_count,
337 eviction_count: self.eviction_count,
338 hit_rate: self.hit_rate(),
339 }
340 }
341
342 fn evict_lru(&mut self) {
344 if let Some(key) = self.access_order.pop_front() {
345 if let Some(tile) = self.entries.remove(&key) {
346 self.current_bytes = self.current_bytes.saturating_sub(tile.size_bytes);
347 self.eviction_count += 1;
348 }
349 }
350 }
351}
352
353pub struct TilePrefetcher {
357 pub radius: u8,
359 pub max_zoom_delta: u8,
361}
362
363impl TilePrefetcher {
364 pub fn new(radius: u8) -> Self {
367 Self {
368 radius,
369 max_zoom_delta: 1,
370 }
371 }
372
373 pub fn neighbors(&self, key: &TileKey) -> Vec<TileKey> {
378 let mut result: Vec<TileKey> = Vec::new();
379
380 let same_zoom_ring = self.ring_at_zoom(key, key.z, self.radius);
382 result.extend(same_zoom_ring);
383
384 for delta in 1..=self.max_zoom_delta {
386 if key.z >= delta {
387 let lower_zoom = key.z - delta;
388 let scaled_x = key.x >> delta;
390 let scaled_y = key.y >> delta;
391 let parent_key = TileKey::new(
392 lower_zoom,
393 scaled_x,
394 scaled_y,
395 key.layer.clone(),
396 key.format.clone(),
397 );
398 let ring = self.ring_at_zoom(&parent_key, lower_zoom, self.radius);
399 for t in ring {
400 if !result.iter().any(|r| r == &t) {
401 result.push(t);
402 }
403 }
404 }
405 let upper_zoom = key.z.saturating_add(delta);
406 if upper_zoom != key.z {
407 let scaled_x = key.x << delta;
409 let scaled_y = key.y << delta;
410 let child_key = TileKey::new(
411 upper_zoom,
412 scaled_x,
413 scaled_y,
414 key.layer.clone(),
415 key.format.clone(),
416 );
417 let ring = self.ring_at_zoom(&child_key, upper_zoom, self.radius);
418 for t in ring {
419 if !result.iter().any(|r| r == &t) {
420 result.push(t);
421 }
422 }
423 }
424 }
425
426 result.retain(|t| t != key);
428 result
429 }
430
431 pub fn ring_at_zoom(&self, key: &TileKey, zoom: u8, radius: u8) -> Vec<TileKey> {
436 let r = radius as i64;
437 let mut tiles = Vec::new();
438 for dx in -r..=r {
439 for dy in -r..=r {
440 let nx = if dx < 0 {
441 key.x.saturating_sub((-dx) as u32)
442 } else {
443 key.x.saturating_add(dx as u32)
444 };
445 let ny = if dy < 0 {
446 key.y.saturating_sub((-dy) as u32)
447 } else {
448 key.y.saturating_add(dy as u32)
449 };
450 tiles.push(TileKey::new(
451 zoom,
452 nx,
453 ny,
454 key.layer.clone(),
455 key.format.clone(),
456 ));
457 }
458 }
459 tiles
460 }
461}