Skip to main content

oxigdal_streaming/tile/
provider.rs

1//! Tile provider implementations.
2
3use super::cache::TileCache;
4use super::protocol::{TileProtocol, TileRequest, TileResponse};
5use crate::error::{Result, StreamingError};
6use async_trait::async_trait;
7use std::sync::Arc;
8use tracing::{debug, info};
9
10/// Tile provider trait.
11#[async_trait]
12pub trait TileProvider: Send + Sync {
13    /// Get a tile.
14    async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse>;
15
16    /// Prefetch multiple tiles.
17    async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>>;
18}
19
20/// Tile source configuration.
21#[derive(Debug, Clone)]
22pub enum TileSource {
23    /// HTTP/HTTPS URL template
24    Http {
25        /// URL template with `{z}`, `{x}`, `{y}` placeholders
26        url_template: String,
27        /// Minimum supported zoom level
28        min_zoom: u8,
29        /// Maximum supported zoom level
30        max_zoom: u8,
31    },
32
33    /// Local file system
34    FileSystem {
35        /// Root directory containing tile files
36        base_path: std::path::PathBuf,
37        /// Tile image format
38        format: super::protocol::TileFormat,
39    },
40
41    /// In-memory tile generator
42    Generator {
43        /// Minimum supported zoom level
44        min_zoom: u8,
45        /// Maximum supported zoom level
46        max_zoom: u8,
47    },
48}
49
50/// Standard tile provider with caching.
51pub struct StandardTileProvider {
52    /// Source configuration (retained for introspection / future routing)
53    #[allow(dead_code)]
54    source: TileSource,
55    cache: Option<Arc<TileCache>>,
56    protocol: Arc<dyn TileProtocol>,
57}
58
59impl StandardTileProvider {
60    /// Create a new tile provider.
61    pub fn new(source: TileSource, protocol: Arc<dyn TileProtocol>) -> Self {
62        Self {
63            source,
64            cache: None,
65            protocol,
66        }
67    }
68
69    /// Enable caching.
70    pub fn with_cache(mut self, cache: Arc<TileCache>) -> Self {
71        self.cache = Some(cache);
72        self
73    }
74
75    /// Fetch a tile from the source.
76    async fn fetch_tile(&self, request: &TileRequest) -> Result<TileResponse> {
77        self.protocol.get_tile(request).await
78    }
79}
80
81#[async_trait]
82impl TileProvider for StandardTileProvider {
83    async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
84        // Check cache first
85        if let Some(cache) = &self.cache
86            && let Some(response) = cache.get(&request.coord).await
87        {
88            debug!("Cache hit for tile {}", request.coord);
89            return Ok(response);
90        }
91
92        // Fetch from source
93        let response = self.fetch_tile(request).await?;
94
95        // Store in cache
96        if let Some(cache) = &self.cache {
97            cache.put(response.clone()).await.ok();
98        }
99
100        Ok(response)
101    }
102
103    async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>> {
104        let mut responses = Vec::with_capacity(requests.len());
105
106        for request in requests {
107            match self.get_tile(&request).await {
108                Ok(response) => responses.push(response),
109                Err(e) => {
110                    debug!("Failed to prefetch tile {}: {}", request.coord, e);
111                }
112            }
113        }
114
115        Ok(responses)
116    }
117}
118
119/// Multi-source tile provider with fallback.
120pub struct MultiSourceTileProvider {
121    providers: Vec<Arc<dyn TileProvider>>,
122}
123
124impl MultiSourceTileProvider {
125    /// Create a new multi-source provider.
126    pub fn new(providers: Vec<Arc<dyn TileProvider>>) -> Self {
127        Self { providers }
128    }
129
130    /// Add a provider.
131    pub fn add_provider(&mut self, provider: Arc<dyn TileProvider>) {
132        self.providers.push(provider);
133    }
134}
135
136#[async_trait]
137impl TileProvider for MultiSourceTileProvider {
138    async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
139        for (i, provider) in self.providers.iter().enumerate() {
140            match provider.get_tile(request).await {
141                Ok(response) => {
142                    if i > 0 {
143                        info!("Fallback to provider {} for tile {}", i, request.coord);
144                    }
145                    return Ok(response);
146                }
147                Err(e) => {
148                    debug!("Provider {} failed for tile {}: {}", i, request.coord, e);
149                    continue;
150                }
151            }
152        }
153
154        Err(StreamingError::Other(format!(
155            "All providers failed for tile {}",
156            request.coord
157        )))
158    }
159
160    async fn prefetch_tiles(&self, requests: Vec<TileRequest>) -> Result<Vec<TileResponse>> {
161        // Use first provider for prefetch
162        if let Some(provider) = self.providers.first() {
163            provider.prefetch_tiles(requests).await
164        } else {
165            Ok(Vec::new())
166        }
167    }
168}
169
170#[cfg(test)]
171#[allow(clippy::panic)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_tile_source() {
177        let source = TileSource::Http {
178            url_template: "https://tile.openstreetmap.org/{z}/{x}/{y}.png".to_string(),
179            min_zoom: 0,
180            max_zoom: 18,
181        };
182
183        match source {
184            TileSource::Http {
185                min_zoom, max_zoom, ..
186            } => {
187                assert_eq!(min_zoom, 0);
188                assert_eq!(max_zoom, 18);
189            }
190            _ => panic!("Wrong variant"),
191        }
192    }
193}