1use bytes::Bytes;
4use std::time::{Duration, Instant};
5
6pub type CacheKey = String;
8
9#[derive(Debug, Clone)]
11pub struct CacheEntry {
12 pub data: Bytes,
14 pub size: usize,
16 pub access_count: u64,
18 pub last_access: Instant,
20 pub created_at: Instant,
22 pub expires_at: Option<Instant>,
24 pub compressed: bool,
26 pub spatial_info: Option<SpatialInfo>,
28}
29
30impl CacheEntry {
31 #[must_use]
33 pub fn new(data: Bytes, compressed: bool) -> Self {
34 let size = data.len();
35 let now = Instant::now();
36
37 Self {
38 data,
39 size,
40 access_count: 1,
41 last_access: now,
42 created_at: now,
43 expires_at: None,
44 compressed,
45 spatial_info: None,
46 }
47 }
48
49 #[must_use]
51 pub fn with_ttl(data: Bytes, compressed: bool, ttl: Duration) -> Self {
52 let mut entry = Self::new(data, compressed);
53 entry.expires_at = Some(Instant::now() + ttl);
54 entry
55 }
56
57 #[must_use]
59 pub fn with_spatial_info(data: Bytes, compressed: bool, spatial_info: SpatialInfo) -> Self {
60 let mut entry = Self::new(data, compressed);
61 entry.spatial_info = Some(spatial_info);
62 entry
63 }
64
65 pub fn record_access(&mut self) {
67 self.access_count += 1;
68 self.last_access = Instant::now();
69 }
70
71 #[must_use]
73 pub fn age(&self) -> Duration {
74 self.created_at.elapsed()
75 }
76
77 #[must_use]
79 pub fn is_expired(&self) -> bool {
80 if let Some(expires_at) = self.expires_at {
81 Instant::now() >= expires_at
82 } else {
83 false
84 }
85 }
86
87 #[must_use]
89 pub fn remaining_ttl(&self) -> Option<Duration> {
90 self.expires_at.map(|expires_at| {
91 let now = Instant::now();
92 if now >= expires_at {
93 Duration::ZERO
94 } else {
95 expires_at - now
96 }
97 })
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct SpatialInfo {
104 pub bounds: (f64, f64, f64, f64),
106 pub crs: Option<u32>,
108 pub resolution: Option<(f64, f64)>,
110 pub zoom_level: Option<u8>,
112}
113
114impl SpatialInfo {
115 #[must_use]
117 pub fn new(bounds: (f64, f64, f64, f64)) -> Self {
118 Self {
119 bounds,
120 crs: None,
121 resolution: None,
122 zoom_level: None,
123 }
124 }
125
126 #[must_use]
128 pub fn with_crs(mut self, crs: u32) -> Self {
129 self.crs = Some(crs);
130 self
131 }
132
133 #[must_use]
135 pub fn with_resolution(mut self, res_x: f64, res_y: f64) -> Self {
136 self.resolution = Some((res_x, res_y));
137 self
138 }
139
140 #[must_use]
142 pub fn with_zoom_level(mut self, zoom: u8) -> Self {
143 self.zoom_level = Some(zoom);
144 self
145 }
146
147 #[must_use]
149 pub fn intersects(&self, other: &SpatialInfo) -> bool {
150 let (min_x1, min_y1, max_x1, max_y1) = self.bounds;
151 let (min_x2, min_y2, max_x2, max_y2) = other.bounds;
152
153 min_x1 <= max_x2 && max_x1 >= min_x2 && min_y1 <= max_y2 && max_y1 >= min_y2
154 }
155
156 #[must_use]
158 pub fn contains_point(&self, x: f64, y: f64) -> bool {
159 let (min_x, min_y, max_x, max_y) = self.bounds;
160 x >= min_x && x <= max_x && y >= min_y && y <= max_y
161 }
162}
163
164#[derive(Debug, Clone, Hash, PartialEq, Eq)]
166pub struct TileCoord {
167 pub z: u8,
169 pub x: u32,
171 pub y: u32,
173}
174
175impl TileCoord {
176 #[must_use]
178 pub const fn new(z: u8, x: u32, y: u32) -> Self {
179 Self { z, x, y }
180 }
181
182 #[must_use]
184 pub fn to_cache_key(&self, prefix: &str) -> String {
185 format!("{prefix}/tiles/{}/{}/{}", self.z, self.x, self.y)
186 }
187
188 #[must_use]
190 pub fn parent(&self) -> Option<Self> {
191 if self.z == 0 {
192 return None;
193 }
194 Some(Self {
195 z: self.z - 1,
196 x: self.x / 2,
197 y: self.y / 2,
198 })
199 }
200
201 #[must_use]
203 pub fn children(&self) -> [Self; 4] {
204 let z = self.z + 1;
205 let x = self.x * 2;
206 let y = self.y * 2;
207 [
208 Self::new(z, x, y),
209 Self::new(z, x + 1, y),
210 Self::new(z, x, y + 1),
211 Self::new(z, x + 1, y + 1),
212 ]
213 }
214}
215
216#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
218pub struct DiskCacheMetadata {
219 pub path: String,
221 pub size: usize,
223 pub created_at_ms: u64,
225 pub expires_at_ms: Option<u64>,
227 pub access_count: u64,
229 pub compressed: bool,
231}
232
233#[derive(Debug, Default)]
235pub struct CacheStats {
236 pub hits: std::sync::atomic::AtomicU64,
238 pub misses: std::sync::atomic::AtomicU64,
240 pub writes: std::sync::atomic::AtomicU64,
242 pub evictions: std::sync::atomic::AtomicU64,
244}
245
246impl CacheStats {
247 #[must_use]
249 pub fn hit_ratio(&self) -> f64 {
250 use std::sync::atomic::Ordering;
251 let hits = self.hits.load(Ordering::Relaxed) as f64;
252 let misses = self.misses.load(Ordering::Relaxed) as f64;
253 if hits + misses == 0.0 {
254 0.0
255 } else {
256 hits / (hits + misses)
257 }
258 }
259
260 pub fn reset(&self) {
262 use std::sync::atomic::Ordering;
263 self.hits.store(0, Ordering::Relaxed);
264 self.misses.store(0, Ordering::Relaxed);
265 self.writes.store(0, Ordering::Relaxed);
266 self.evictions.store(0, Ordering::Relaxed);
267 }
268}
269
270#[derive(Debug, Default)]
272pub struct LevelStats {
273 pub tile_count: std::sync::atomic::AtomicUsize,
275 pub total_size: std::sync::atomic::AtomicUsize,
277 pub hits: std::sync::atomic::AtomicU64,
279}