use super::cache::TileCache;
use super::protocol::{
FileSystemTileProtocol, TileCoordinate, TileProtocol, TileRequest, TileResponse,
};
use crate::error::{Result, StreamingError};
use async_trait::async_trait;
use std::sync::Arc;
use tracing::{debug, info};
#[async_trait]
pub trait TileProvider: Send + Sync {
async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse>;
async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>>;
}
#[async_trait]
pub trait TileGenerator: Send + Sync {
async fn generate(&self, coord: &TileCoordinate) -> Result<TileResponse>;
}
#[derive(Debug, Clone)]
pub enum TileSource {
Http {
url_template: String,
min_zoom: u8,
max_zoom: u8,
},
FileSystem {
base_path: std::path::PathBuf,
format: super::protocol::TileFormat,
},
Generator {
min_zoom: u8,
max_zoom: u8,
},
}
pub struct StandardTileProvider {
source: TileSource,
cache: Option<Arc<TileCache>>,
protocol: Arc<dyn TileProtocol>,
fs_protocol: Option<FileSystemTileProtocol>,
generator: Option<Arc<dyn TileGenerator>>,
}
impl StandardTileProvider {
pub fn new(source: TileSource, protocol: Arc<dyn TileProtocol>) -> Self {
let fs_protocol = match &source {
TileSource::FileSystem { base_path, format } => {
Some(FileSystemTileProtocol::new(base_path.clone(), *format))
}
_ => None,
};
Self {
source,
cache: None,
protocol,
fs_protocol,
generator: None,
}
}
pub fn with_cache(mut self, cache: Arc<TileCache>) -> Self {
self.cache = Some(cache);
self
}
pub fn with_generator(mut self, generator: Arc<dyn TileGenerator>) -> Self {
self.generator = Some(generator);
self
}
async fn fetch_tile(&self, request: &TileRequest) -> Result<TileResponse> {
match &self.source {
TileSource::Http {
min_zoom, max_zoom, ..
} => {
if request.coord.z < *min_zoom || request.coord.z > *max_zoom {
return Err(StreamingError::InvalidOperation(format!(
"Zoom level {} out of range [{}, {}]",
request.coord.z, min_zoom, max_zoom
)));
}
self.protocol.get_tile(request).await
}
TileSource::FileSystem { .. } => {
let fs = self.fs_protocol.as_ref().ok_or_else(|| {
StreamingError::InvalidState(
"FileSystem source without an initialized disk protocol".to_string(),
)
})?;
fs.get_tile(request).await
}
TileSource::Generator { min_zoom, max_zoom } => {
if request.coord.z < *min_zoom || request.coord.z > *max_zoom {
return Err(StreamingError::InvalidOperation(format!(
"Zoom level {} out of range [{}, {}]",
request.coord.z, min_zoom, max_zoom
)));
}
match &self.generator {
Some(generator) => generator.generate(&request.coord).await,
None => Err(StreamingError::InvalidOperation(
"Generator source requires a generator; register one via \
StandardTileProvider::with_generator"
.to_string(),
)),
}
}
}
}
}
#[async_trait]
impl TileProvider for StandardTileProvider {
async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
if let Some(cache) = &self.cache
&& let Some(response) = cache.get(&request.coord).await
{
debug!("Cache hit for tile {}", request.coord);
return Ok(response);
}
let response = self.fetch_tile(request).await?;
if let Some(cache) = &self.cache {
cache.put(response.clone()).await.ok();
}
Ok(response)
}
async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>> {
let mut responses = Vec::with_capacity(requests.len());
for request in requests {
match self.get_tile(&request).await {
Ok(response) => responses.push(response),
Err(e) => {
debug!("Failed to prefetch tile {}: {}", request.coord, e);
}
}
}
Ok(responses)
}
}
pub struct MultiSourceTileProvider {
providers: Vec<Arc<dyn TileProvider>>,
}
impl MultiSourceTileProvider {
pub fn new(providers: Vec<Arc<dyn TileProvider>>) -> Self {
Self { providers }
}
pub fn add_provider(&mut self, provider: Arc<dyn TileProvider>) {
self.providers.push(provider);
}
}
#[async_trait]
impl TileProvider for MultiSourceTileProvider {
async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
for (i, provider) in self.providers.iter().enumerate() {
match provider.get_tile(request).await {
Ok(response) => {
if i > 0 {
info!("Fallback to provider {} for tile {}", i, request.coord);
}
return Ok(response);
}
Err(e) => {
debug!("Provider {} failed for tile {}: {}", i, request.coord, e);
continue;
}
}
}
Err(StreamingError::Other(format!(
"All providers failed for tile {}",
request.coord
)))
}
async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>> {
if let Some(provider) = self.providers.first() {
provider.prefetch_tiles(requests).await
} else {
Ok(Vec::new())
}
}
}
#[cfg(test)]
#[allow(clippy::panic)]
mod tests {
use super::super::protocol::{TileCoordinate, TileFormat, TileMetadata};
use super::*;
use bytes::Bytes;
struct MarkerProtocol;
#[async_trait]
impl TileProtocol for MarkerProtocol {
async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
Ok(TileResponse::new(
request.coord,
Bytes::from_static(b"FROM_HTTP_PROTOCOL"),
"image/png".to_string(),
))
}
async fn has_tile(&self, _coord: &TileCoordinate) -> Result<bool> {
Ok(true)
}
async fn get_tile_metadata(&self, coord: &TileCoordinate) -> Result<TileMetadata> {
Ok(TileMetadata {
coord: *coord,
size_bytes: 0,
format: TileFormat::Png,
created_at: None,
modified_at: None,
bbox: None,
metadata: std::collections::HashMap::new(),
})
}
fn zoom_levels(&self) -> (u8, u8) {
(0, 31)
}
fn tile_size(&self) -> (u32, u32) {
(256, 256)
}
}
struct SolidGenerator;
#[async_trait]
impl TileGenerator for SolidGenerator {
async fn generate(&self, coord: &TileCoordinate) -> Result<TileResponse> {
Ok(TileResponse::new(
*coord,
Bytes::from(format!("GEN:{}/{}/{}", coord.z, coord.x, coord.y).into_bytes()),
"image/png".to_string(),
))
}
}
#[tokio::test]
async fn test_filesystem_source_reads_disk_not_protocol() {
let dir = tempfile::tempdir().expect("temp dir");
let coord = TileCoordinate::new(4, 2, 3);
let tile_file = dir.path().join("4/2/3.png");
tokio::fs::create_dir_all(tile_file.parent().expect("parent"))
.await
.expect("mkdir");
tokio::fs::write(&tile_file, b"FROM_DISK")
.await
.expect("write");
let source = TileSource::FileSystem {
base_path: dir.path().to_path_buf(),
format: TileFormat::Png,
};
let provider = StandardTileProvider::new(source, Arc::new(MarkerProtocol));
let request = TileRequest::new(coord, TileFormat::Png);
let resp = provider.get_tile(&request).await.expect("disk tile");
assert_eq!(&resp.data[..], b"FROM_DISK");
}
#[tokio::test]
async fn test_generator_source_uses_generator() {
let source = TileSource::Generator {
min_zoom: 0,
max_zoom: 20,
};
let coord = TileCoordinate::new(6, 1, 2);
let provider = StandardTileProvider::new(source, Arc::new(MarkerProtocol))
.with_generator(Arc::new(SolidGenerator));
let request = TileRequest::new(coord, TileFormat::Png);
let resp = provider.get_tile(&request).await.expect("generated tile");
assert_eq!(&resp.data[..], b"GEN:6/1/2");
}
#[tokio::test]
async fn test_generator_source_without_generator_errors() {
let source = TileSource::Generator {
min_zoom: 0,
max_zoom: 20,
};
let provider = StandardTileProvider::new(source, Arc::new(MarkerProtocol));
let request = TileRequest::new(TileCoordinate::new(6, 1, 2), TileFormat::Png);
let err = provider
.get_tile(&request)
.await
.expect_err("should error without generator");
assert!(matches!(err, StreamingError::InvalidOperation(_)));
}
#[tokio::test]
async fn test_http_source_uses_protocol() {
let source = TileSource::Http {
url_template: "https://example.com/{z}/{x}/{y}.png".to_string(),
min_zoom: 0,
max_zoom: 18,
};
let provider = StandardTileProvider::new(source, Arc::new(MarkerProtocol));
let request = TileRequest::new(TileCoordinate::new(5, 1, 1), TileFormat::Png);
let resp = provider.get_tile(&request).await.expect("http tile");
assert_eq!(&resp.data[..], b"FROM_HTTP_PROTOCOL");
}
#[test]
fn test_tile_source() {
let source = TileSource::Http {
url_template: "https://tile.openstreetmap.org/{z}/{x}/{y}.png".to_string(),
min_zoom: 0,
max_zoom: 18,
};
match source {
TileSource::Http {
min_zoom, max_zoom, ..
} => {
assert_eq!(min_zoom, 0);
assert_eq!(max_zoom, 18);
}
_ => panic!("Wrong variant"),
}
}
}