1use crate::source::TileKey;
15use std::path::{Path, PathBuf};
16
17pub trait TileCache: Send + Sync {
21 fn get(&self, key: TileKey) -> Option<slint::Image>;
25
26 fn put(&self, key: TileKey, bytes: &[u8]) -> Result<(), CacheError>;
31
32 fn contains(&self, key: TileKey) -> bool {
36 self.get(key).is_some()
37 }
38
39 fn get_bytes(&self, _key: TileKey) -> Option<Vec<u8>> {
44 None
45 }
46}
47
48#[derive(Debug)]
50pub enum CacheError {
51 Io(std::io::Error),
52 Other(String),
53}
54
55impl std::fmt::Display for CacheError {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 CacheError::Io(e) => write!(f, "tile cache I/O error: {e}"),
59 CacheError::Other(s) => write!(f, "tile cache error: {s}"),
60 }
61 }
62}
63
64impl std::error::Error for CacheError {}
65
66impl From<std::io::Error> for CacheError {
67 fn from(e: std::io::Error) -> Self {
68 CacheError::Io(e)
69 }
70}
71
72pub struct FileTileCache {
77 root: PathBuf,
78 extension: String,
79}
80
81impl FileTileCache {
82 pub fn new(root: impl Into<PathBuf>) -> Self {
83 Self {
84 root: root.into(),
85 extension: "png".to_string(),
86 }
87 }
88
89 pub fn with_extension(mut self, ext: impl Into<String>) -> Self {
91 self.extension = ext.into();
92 self
93 }
94
95 pub fn path_for(&self, key: TileKey) -> PathBuf {
97 self.root
98 .join(key.z.to_string())
99 .join(key.x.to_string())
100 .join(format!("{}.{}", key.y, self.extension))
101 }
102
103 pub fn root(&self) -> &Path {
105 &self.root
106 }
107}
108
109impl TileCache for FileTileCache {
110 fn get(&self, key: TileKey) -> Option<slint::Image> {
111 let path = self.path_for(key);
112 if !path.exists() {
113 return None;
114 }
115 slint::Image::load_from_path(&path).ok()
116 }
117
118 fn put(&self, key: TileKey, bytes: &[u8]) -> Result<(), CacheError> {
119 let path = self.path_for(key);
120 if let Some(parent) = path.parent() {
121 std::fs::create_dir_all(parent)?;
122 }
123 std::fs::write(&path, bytes)?;
124 Ok(())
125 }
126
127 fn contains(&self, key: TileKey) -> bool {
128 self.path_for(key).exists()
129 }
130
131 fn get_bytes(&self, key: TileKey) -> Option<Vec<u8>> {
132 let path = self.path_for(key);
133 if !path.exists() {
134 return None;
135 }
136 std::fs::read(&path).ok()
137 }
138}
139
140pub struct LayeredTileCache {
147 layers: Vec<Box<dyn TileCache>>,
150}
151
152impl LayeredTileCache {
153 pub fn new(writable: Box<dyn TileCache>, fallbacks: Vec<Box<dyn TileCache>>) -> Self {
156 let mut layers = Vec::with_capacity(1 + fallbacks.len());
157 layers.push(writable);
158 layers.extend(fallbacks);
159 Self { layers }
160 }
161}
162
163impl TileCache for LayeredTileCache {
164 fn get(&self, key: TileKey) -> Option<slint::Image> {
165 self.layers.iter().find_map(|l| l.get(key))
166 }
167 fn put(&self, key: TileKey, bytes: &[u8]) -> Result<(), CacheError> {
168 self.layers[0].put(key, bytes)
169 }
170 fn contains(&self, key: TileKey) -> bool {
171 self.layers.iter().any(|l| l.contains(key))
172 }
173 fn get_bytes(&self, key: TileKey) -> Option<Vec<u8>> {
174 self.layers.iter().find_map(|l| l.get_bytes(key))
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use std::fs;
182
183 fn temp_root(name: &str) -> PathBuf {
184 let tid = format!("{:?}", std::thread::current().id());
188 let p = std::env::temp_dir().join(format!(
189 "slint-mapping-cache-test-{}-{}-{}",
190 std::process::id(),
191 name,
192 tid.replace([' ', '(', ')'], "_"),
193 ));
194 let _ = fs::remove_dir_all(&p);
195 fs::create_dir_all(&p).unwrap();
196 p
197 }
198
199 #[test]
200 fn put_then_get_returns_the_same_bytes_via_image() {
201 let root = temp_root("put_then_get");
202 let cache = FileTileCache::new(&root);
203 let key = TileKey { x: 1, y: 2, z: 3 };
204 let png = include_bytes!("../sample-tiles/0/0/0.png"); cache.put(key, png).unwrap();
206 assert!(cache.contains(key));
207 assert!(cache.get(key).is_some(), "round-trip should decode");
208 fs::remove_dir_all(&root).ok();
209 }
210
211 #[test]
212 fn missing_key_returns_none() {
213 let root = temp_root("missing");
214 let cache = FileTileCache::new(&root);
215 assert!(!cache.contains(TileKey { x: 9, y: 9, z: 9 }));
216 assert!(cache.get(TileKey { x: 9, y: 9, z: 9 }).is_none());
217 fs::remove_dir_all(&root).ok();
218 }
219
220 #[test]
221 fn put_creates_intermediate_directories() {
222 let root = temp_root("intermediate_dirs");
223 let cache = FileTileCache::new(&root);
224 let key = TileKey {
225 x: 1234,
226 y: 5678,
227 z: 12,
228 };
229 let png = include_bytes!("../sample-tiles/0/0/0.png");
230 cache.put(key, png).unwrap();
231 assert!(cache.path_for(key).exists());
232 fs::remove_dir_all(&root).ok();
233 }
234}