1use super::cache::TileCache;
4use super::protocol::{
5 FileSystemTileProtocol, TileCoordinate, TileProtocol, TileRequest, TileResponse,
6};
7use crate::error::{Result, StreamingError};
8use async_trait::async_trait;
9use std::sync::Arc;
10use tracing::{debug, info};
11
12#[async_trait]
14pub trait TileProvider: Send + Sync {
15 async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse>;
17
18 async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>>;
20}
21
22#[async_trait]
29pub trait TileGenerator: Send + Sync {
30 async fn generate(&self, coord: &TileCoordinate) -> Result<TileResponse>;
32}
33
34#[derive(Debug, Clone)]
36pub enum TileSource {
37 Http {
39 url_template: String,
41 min_zoom: u8,
43 max_zoom: u8,
45 },
46
47 FileSystem {
49 base_path: std::path::PathBuf,
51 format: super::protocol::TileFormat,
53 },
54
55 Generator {
57 min_zoom: u8,
59 max_zoom: u8,
61 },
62}
63
64pub struct StandardTileProvider {
74 source: TileSource,
76 cache: Option<Arc<TileCache>>,
77 protocol: Arc<dyn TileProtocol>,
79 fs_protocol: Option<FileSystemTileProtocol>,
81 generator: Option<Arc<dyn TileGenerator>>,
83}
84
85impl StandardTileProvider {
86 pub fn new(source: TileSource, protocol: Arc<dyn TileProtocol>) -> Self {
93 let fs_protocol = match &source {
94 TileSource::FileSystem { base_path, format } => {
95 Some(FileSystemTileProtocol::new(base_path.clone(), *format))
96 }
97 _ => None,
98 };
99 Self {
100 source,
101 cache: None,
102 protocol,
103 fs_protocol,
104 generator: None,
105 }
106 }
107
108 pub fn with_cache(mut self, cache: Arc<TileCache>) -> Self {
110 self.cache = Some(cache);
111 self
112 }
113
114 pub fn with_generator(mut self, generator: Arc<dyn TileGenerator>) -> Self {
117 self.generator = Some(generator);
118 self
119 }
120
121 async fn fetch_tile(&self, request: &TileRequest) -> Result<TileResponse> {
123 match &self.source {
124 TileSource::Http {
125 min_zoom, max_zoom, ..
126 } => {
127 if request.coord.z < *min_zoom || request.coord.z > *max_zoom {
128 return Err(StreamingError::InvalidOperation(format!(
129 "Zoom level {} out of range [{}, {}]",
130 request.coord.z, min_zoom, max_zoom
131 )));
132 }
133 self.protocol.get_tile(request).await
134 }
135 TileSource::FileSystem { .. } => {
136 let fs = self.fs_protocol.as_ref().ok_or_else(|| {
137 StreamingError::InvalidState(
138 "FileSystem source without an initialized disk protocol".to_string(),
139 )
140 })?;
141 fs.get_tile(request).await
142 }
143 TileSource::Generator { min_zoom, max_zoom } => {
144 if request.coord.z < *min_zoom || request.coord.z > *max_zoom {
145 return Err(StreamingError::InvalidOperation(format!(
146 "Zoom level {} out of range [{}, {}]",
147 request.coord.z, min_zoom, max_zoom
148 )));
149 }
150 match &self.generator {
151 Some(generator) => generator.generate(&request.coord).await,
152 None => Err(StreamingError::InvalidOperation(
153 "Generator source requires a generator; register one via \
154 StandardTileProvider::with_generator"
155 .to_string(),
156 )),
157 }
158 }
159 }
160 }
161}
162
163#[async_trait]
164impl TileProvider for StandardTileProvider {
165 async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
166 if let Some(cache) = &self.cache
168 && let Some(response) = cache.get(&request.coord).await
169 {
170 debug!("Cache hit for tile {}", request.coord);
171 return Ok(response);
172 }
173
174 let response = self.fetch_tile(request).await?;
176
177 if let Some(cache) = &self.cache {
179 cache.put(response.clone()).await.ok();
180 }
181
182 Ok(response)
183 }
184
185 async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>> {
186 let mut responses = Vec::with_capacity(requests.len());
187
188 for request in requests {
189 match self.get_tile(&request).await {
190 Ok(response) => responses.push(response),
191 Err(e) => {
192 debug!("Failed to prefetch tile {}: {}", request.coord, e);
193 }
194 }
195 }
196
197 Ok(responses)
198 }
199}
200
201pub struct MultiSourceTileProvider {
203 providers: Vec<Arc<dyn TileProvider>>,
204}
205
206impl MultiSourceTileProvider {
207 pub fn new(providers: Vec<Arc<dyn TileProvider>>) -> Self {
209 Self { providers }
210 }
211
212 pub fn add_provider(&mut self, provider: Arc<dyn TileProvider>) {
214 self.providers.push(provider);
215 }
216}
217
218#[async_trait]
219impl TileProvider for MultiSourceTileProvider {
220 async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
221 for (i, provider) in self.providers.iter().enumerate() {
222 match provider.get_tile(request).await {
223 Ok(response) => {
224 if i > 0 {
225 info!("Fallback to provider {} for tile {}", i, request.coord);
226 }
227 return Ok(response);
228 }
229 Err(e) => {
230 debug!("Provider {} failed for tile {}: {}", i, request.coord, e);
231 continue;
232 }
233 }
234 }
235
236 Err(StreamingError::Other(format!(
237 "All providers failed for tile {}",
238 request.coord
239 )))
240 }
241
242 async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>> {
243 if let Some(provider) = self.providers.first() {
245 provider.prefetch_tiles(requests).await
246 } else {
247 Ok(Vec::new())
248 }
249 }
250}
251
252#[cfg(test)]
253#[allow(clippy::panic)]
254mod tests {
255 use super::super::protocol::{TileCoordinate, TileFormat, TileMetadata};
256 use super::*;
257 use bytes::Bytes;
258
259 struct MarkerProtocol;
262
263 #[async_trait]
264 impl TileProtocol for MarkerProtocol {
265 async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
266 Ok(TileResponse::new(
267 request.coord,
268 Bytes::from_static(b"FROM_HTTP_PROTOCOL"),
269 "image/png".to_string(),
270 ))
271 }
272 async fn has_tile(&self, _coord: &TileCoordinate) -> Result<bool> {
273 Ok(true)
274 }
275 async fn get_tile_metadata(&self, coord: &TileCoordinate) -> Result<TileMetadata> {
276 Ok(TileMetadata {
277 coord: *coord,
278 size_bytes: 0,
279 format: TileFormat::Png,
280 created_at: None,
281 modified_at: None,
282 bbox: None,
283 metadata: std::collections::HashMap::new(),
284 })
285 }
286 fn zoom_levels(&self) -> (u8, u8) {
287 (0, 31)
288 }
289 fn tile_size(&self) -> (u32, u32) {
290 (256, 256)
291 }
292 }
293
294 struct SolidGenerator;
295
296 #[async_trait]
297 impl TileGenerator for SolidGenerator {
298 async fn generate(&self, coord: &TileCoordinate) -> Result<TileResponse> {
299 Ok(TileResponse::new(
300 *coord,
301 Bytes::from(format!("GEN:{}/{}/{}", coord.z, coord.x, coord.y).into_bytes()),
302 "image/png".to_string(),
303 ))
304 }
305 }
306
307 #[tokio::test]
308 async fn test_filesystem_source_reads_disk_not_protocol() {
309 let dir = tempfile::tempdir().expect("temp dir");
310 let coord = TileCoordinate::new(4, 2, 3);
311 let tile_file = dir.path().join("4/2/3.png");
312 tokio::fs::create_dir_all(tile_file.parent().expect("parent"))
313 .await
314 .expect("mkdir");
315 tokio::fs::write(&tile_file, b"FROM_DISK")
316 .await
317 .expect("write");
318
319 let source = TileSource::FileSystem {
320 base_path: dir.path().to_path_buf(),
321 format: TileFormat::Png,
322 };
323 let provider = StandardTileProvider::new(source, Arc::new(MarkerProtocol));
325 let request = TileRequest::new(coord, TileFormat::Png);
326 let resp = provider.get_tile(&request).await.expect("disk tile");
327 assert_eq!(&resp.data[..], b"FROM_DISK");
328 }
329
330 #[tokio::test]
331 async fn test_generator_source_uses_generator() {
332 let source = TileSource::Generator {
333 min_zoom: 0,
334 max_zoom: 20,
335 };
336 let coord = TileCoordinate::new(6, 1, 2);
337 let provider = StandardTileProvider::new(source, Arc::new(MarkerProtocol))
338 .with_generator(Arc::new(SolidGenerator));
339 let request = TileRequest::new(coord, TileFormat::Png);
340 let resp = provider.get_tile(&request).await.expect("generated tile");
341 assert_eq!(&resp.data[..], b"GEN:6/1/2");
342 }
343
344 #[tokio::test]
345 async fn test_generator_source_without_generator_errors() {
346 let source = TileSource::Generator {
347 min_zoom: 0,
348 max_zoom: 20,
349 };
350 let provider = StandardTileProvider::new(source, Arc::new(MarkerProtocol));
351 let request = TileRequest::new(TileCoordinate::new(6, 1, 2), TileFormat::Png);
352 let err = provider
353 .get_tile(&request)
354 .await
355 .expect_err("should error without generator");
356 assert!(matches!(err, StreamingError::InvalidOperation(_)));
357 }
358
359 #[tokio::test]
360 async fn test_http_source_uses_protocol() {
361 let source = TileSource::Http {
362 url_template: "https://example.com/{z}/{x}/{y}.png".to_string(),
363 min_zoom: 0,
364 max_zoom: 18,
365 };
366 let provider = StandardTileProvider::new(source, Arc::new(MarkerProtocol));
367 let request = TileRequest::new(TileCoordinate::new(5, 1, 1), TileFormat::Png);
368 let resp = provider.get_tile(&request).await.expect("http tile");
369 assert_eq!(&resp.data[..], b"FROM_HTTP_PROTOCOL");
370 }
371
372 #[test]
373 fn test_tile_source() {
374 let source = TileSource::Http {
375 url_template: "https://tile.openstreetmap.org/{z}/{x}/{y}.png".to_string(),
376 min_zoom: 0,
377 max_zoom: 18,
378 };
379
380 match source {
381 TileSource::Http {
382 min_zoom, max_zoom, ..
383 } => {
384 assert_eq!(min_zoom, 0);
385 assert_eq!(max_zoom, 18);
386 }
387 _ => panic!("Wrong variant"),
388 }
389 }
390}