use crate::source::TileKey;
use std::path::{Path, PathBuf};
pub trait TileCache: Send + Sync {
fn get(&self, key: TileKey) -> Option<slint::Image>;
fn put(&self, key: TileKey, bytes: &[u8]) -> Result<(), CacheError>;
fn contains(&self, key: TileKey) -> bool {
self.get(key).is_some()
}
fn get_bytes(&self, _key: TileKey) -> Option<Vec<u8>> {
None
}
}
#[derive(Debug)]
pub enum CacheError {
Io(std::io::Error),
Other(String),
}
impl std::fmt::Display for CacheError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CacheError::Io(e) => write!(f, "tile cache I/O error: {e}"),
CacheError::Other(s) => write!(f, "tile cache error: {s}"),
}
}
}
impl std::error::Error for CacheError {}
impl From<std::io::Error> for CacheError {
fn from(e: std::io::Error) -> Self {
CacheError::Io(e)
}
}
pub struct FileTileCache {
root: PathBuf,
extension: String,
}
impl FileTileCache {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
extension: "png".to_string(),
}
}
pub fn with_extension(mut self, ext: impl Into<String>) -> Self {
self.extension = ext.into();
self
}
pub fn path_for(&self, key: TileKey) -> PathBuf {
self.root
.join(key.z.to_string())
.join(key.x.to_string())
.join(format!("{}.{}", key.y, self.extension))
}
pub fn root(&self) -> &Path {
&self.root
}
}
impl TileCache for FileTileCache {
fn get(&self, key: TileKey) -> Option<slint::Image> {
let path = self.path_for(key);
if !path.exists() {
return None;
}
slint::Image::load_from_path(&path).ok()
}
fn put(&self, key: TileKey, bytes: &[u8]) -> Result<(), CacheError> {
let path = self.path_for(key);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, bytes)?;
Ok(())
}
fn contains(&self, key: TileKey) -> bool {
self.path_for(key).exists()
}
fn get_bytes(&self, key: TileKey) -> Option<Vec<u8>> {
let path = self.path_for(key);
if !path.exists() {
return None;
}
std::fs::read(&path).ok()
}
}
pub struct LayeredTileCache {
layers: Vec<Box<dyn TileCache>>,
}
impl LayeredTileCache {
pub fn new(writable: Box<dyn TileCache>, fallbacks: Vec<Box<dyn TileCache>>) -> Self {
let mut layers = Vec::with_capacity(1 + fallbacks.len());
layers.push(writable);
layers.extend(fallbacks);
Self { layers }
}
}
impl TileCache for LayeredTileCache {
fn get(&self, key: TileKey) -> Option<slint::Image> {
self.layers.iter().find_map(|l| l.get(key))
}
fn put(&self, key: TileKey, bytes: &[u8]) -> Result<(), CacheError> {
self.layers[0].put(key, bytes)
}
fn contains(&self, key: TileKey) -> bool {
self.layers.iter().any(|l| l.contains(key))
}
fn get_bytes(&self, key: TileKey) -> Option<Vec<u8>> {
self.layers.iter().find_map(|l| l.get_bytes(key))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn temp_root(name: &str) -> PathBuf {
let tid = format!("{:?}", std::thread::current().id());
let p = std::env::temp_dir().join(format!(
"slint-mapping-cache-test-{}-{}-{}",
std::process::id(),
name,
tid.replace([' ', '(', ')'], "_"),
));
let _ = fs::remove_dir_all(&p);
fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn put_then_get_returns_the_same_bytes_via_image() {
let root = temp_root("put_then_get");
let cache = FileTileCache::new(&root);
let key = TileKey { x: 1, y: 2, z: 3 };
let png = include_bytes!("../sample-tiles/0/0/0.png"); cache.put(key, png).unwrap();
assert!(cache.contains(key));
assert!(cache.get(key).is_some(), "round-trip should decode");
fs::remove_dir_all(&root).ok();
}
#[test]
fn missing_key_returns_none() {
let root = temp_root("missing");
let cache = FileTileCache::new(&root);
assert!(!cache.contains(TileKey { x: 9, y: 9, z: 9 }));
assert!(cache.get(TileKey { x: 9, y: 9, z: 9 }).is_none());
fs::remove_dir_all(&root).ok();
}
#[test]
fn put_creates_intermediate_directories() {
let root = temp_root("intermediate_dirs");
let cache = FileTileCache::new(&root);
let key = TileKey {
x: 1234,
y: 5678,
z: 12,
};
let png = include_bytes!("../sample-tiles/0/0/0.png");
cache.put(key, png).unwrap();
assert!(cache.path_for(key).exists());
fs::remove_dir_all(&root).ok();
}
}