//! SRTM DEM parser — elevation data from NASA 1-arcsecond HGT files. use crate::cache::TileCache; use crate::types::{ElevationGrid, GeoPoint}; use anyhow::Result; /// Download and parse SRTM HGT for a location. pub async fn fetch_elevation(point: &GeoPoint, cache: &TileCache) -> Result { let lat_int = point.lat.floor() as i32; let lon_int = point.lon.floor() as i32; let ns = if lat_int >= 0 { 'N' } else { 'S' }; let ew = if lon_int >= 0 { 'E' } else { 'W' }; let filename = format!( "{}{:02}{}{:03}.hgt", ns, lat_int.unsigned_abs(), ew, lon_int.unsigned_abs() ); let cache_key = format!("srtm_{filename}"); if let Some(data) = cache.get(&cache_key) { return parse_hgt(&data, lat_int as f64, lon_int as f64); } let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build()?; // Primary: NASA SRTM public mirror (no auth required for .hgt) let nasa_url = format!("https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL1.003/2000.02.11/{filename}"); if let Ok(resp) = client.get(&nasa_url).send().await { if resp.status().is_success() { let data = resp.bytes().await?.to_vec(); cache.put(&cache_key, &data)?; return parse_hgt(&data, lat_int as f64, lon_int as f64); } } // Fallback: viewfinderpanoramas.org // Files are grouped by continent zip, but individual .hgt files can be // fetched directly when the server exposes them. let vfp_url = format!("http://viewfinderpanoramas.org/dem1/{filename}"); if let Ok(resp) = client.get(&vfp_url).send().await { if resp.status().is_success() { let data = resp.bytes().await?.to_vec(); cache.put(&cache_key, &data)?; return parse_hgt(&data, lat_int as f64, lon_int as f64); } } // Final fallback: flat terrain when all downloads fail Ok(ElevationGrid { origin_lat: lat_int as f64, origin_lon: lon_int as f64, cell_size_deg: 1.0 / 3600.0, cols: 100, rows: 100, heights: vec![0.0; 10000], }) } /// Parse SRTM HGT binary (3601x3601 big-endian i16). pub fn parse_hgt(data: &[u8], origin_lat: f64, origin_lon: f64) -> Result { let n_samples = data.len() / 2; let side = (n_samples as f64).sqrt() as usize; let heights: Vec = data .chunks_exact(2) .map(|c| { let v = i16::from_be_bytes([c[0], c[1]]); if v == -32768 { 0.0 } else { v as f32 } // -32768 = void }) .collect(); Ok(ElevationGrid { origin_lat, origin_lon, cell_size_deg: 1.0 / (side - 1) as f64, cols: side, rows: side, heights, }) } /// Get elevation at a specific point from a grid. pub fn elevation_at(grid: &ElevationGrid, point: &GeoPoint) -> f32 { grid.get(point.lat, point.lon).unwrap_or(0.0) } /// Extract a small subgrid around a point. pub fn extract_subgrid(grid: &ElevationGrid, center: &GeoPoint, radius_m: f64) -> ElevationGrid { let radius_deg = radius_m / 111_320.0; let min_row = ((grid.origin_lat + (grid.rows as f64 * grid.cell_size_deg) - center.lat - radius_deg) / grid.cell_size_deg) .max(0.0) as usize; let max_row = ((grid.origin_lat + (grid.rows as f64 * grid.cell_size_deg) - center.lat + radius_deg) / grid.cell_size_deg) .min(grid.rows as f64) as usize; let min_col = ((center.lon - radius_deg - grid.origin_lon) / grid.cell_size_deg).max(0.0) as usize; let max_col = ((center.lon + radius_deg - grid.origin_lon) / grid.cell_size_deg) .min(grid.cols as f64) as usize; let rows = max_row.saturating_sub(min_row); let cols = max_col.saturating_sub(min_col); let mut heights = Vec::with_capacity(rows * cols); for r in min_row..max_row { for c in min_col..max_col { heights.push(grid.heights.get(r * grid.cols + c).copied().unwrap_or(0.0)); } } ElevationGrid { origin_lat: grid.origin_lat + (grid.rows - max_row) as f64 * grid.cell_size_deg, origin_lon: grid.origin_lon + min_col as f64 * grid.cell_size_deg, cell_size_deg: grid.cell_size_deg, cols, rows, heights, } }