//! Satellite tile fetcher — XYZ/TMS tile download with caching. use crate::cache::TileCache; use crate::coord; use crate::types::{GeoBBox, RasterTile, TileCoord}; use anyhow::Result; /// Tile provider (all free, no API keys). pub enum TileProvider { /// Sentinel-2 cloudless mosaic (EOX, 10m, CC-BY-4.0) Sentinel2Cloudless, /// ESRI World Imagery (sub-meter, free tier) EsriWorldImagery, /// OpenStreetMap (map tiles, not satellite) Osm, } impl TileProvider { pub fn url(&self, coord: &TileCoord) -> String { match self { Self::Sentinel2Cloudless => format!( "https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2021_3857/default/g/{}/{}/{}.jpg", coord.z, coord.y, coord.x ), Self::EsriWorldImagery => format!( "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{}/{}/{}", coord.z, coord.y, coord.x ), Self::Osm => format!( "https://tile.openstreetmap.org/{}/{}/{}.png", coord.z, coord.x, coord.y ), } } pub fn name(&self) -> &str { match self { Self::Sentinel2Cloudless => "sentinel2", Self::EsriWorldImagery => "esri", Self::Osm => "osm", } } } /// Fetch a single tile with caching. pub async fn fetch_tile( provider: &TileProvider, coord: &TileCoord, cache: &TileCache, ) -> Result { let cache_key = format!("tiles_{}_{}_{}.dat", coord.z, coord.x, coord.y); if let Some(data) = cache.get(&cache_key) { return Ok(RasterTile { coord: coord.clone(), data, bounds: coord::tile_bounds(coord), }); } let url = provider.url(coord); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .user_agent("RuView/0.1 (https://github.com/ruvnet/RuView)") .build()?; let resp = client.get(&url).send().await?; if !resp.status().is_success() { anyhow::bail!("Tile fetch failed: {} → {}", url, resp.status()); } let data = resp.bytes().await?.to_vec(); cache.put(&cache_key, &data)?; Ok(RasterTile { coord: coord.clone(), data, bounds: coord::tile_bounds(coord), }) } /// Fetch all tiles covering a bounding box. pub async fn fetch_area( provider: &TileProvider, bbox: &GeoBBox, zoom: u8, cache: &TileCache, ) -> Result> { let coords = coord::tiles_for_bbox(bbox, zoom); let mut tiles = Vec::with_capacity(coords.len()); for c in &coords { match fetch_tile(provider, c, cache).await { Ok(t) => tiles.push(t), Err(e) => eprintln!(" Tile {}/{}/{} failed: {}", c.z, c.x, c.y, e), } } Ok(tiles) }