use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
use moka::sync::Cache;
use crate::error::{Result, SrtmError};
use crate::filename::{coords_to_filename, filename_to_lat_lon};
use crate::tile::{SrtmTile, VOID_VALUE};
#[cfg(feature = "download")]
use crate::download::{DownloadConfig, Downloader};
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub entry_count: u64,
pub hit_count: u64,
pub miss_count: u64,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.hit_count + self.miss_count;
if total == 0 {
0.0
} else {
self.hit_count as f64 / total as f64
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct BoundingBox {
pub min_lat: f64,
pub min_lon: f64,
pub max_lat: f64,
pub max_lon: f64,
}
impl BoundingBox {
pub fn new(min_lat: f64, min_lon: f64, max_lat: f64, max_lon: f64) -> Self {
Self {
min_lat,
min_lon,
max_lat,
max_lon,
}
}
pub fn overlaps_tile(&self, tile_lat: i32, tile_lon: i32) -> bool {
let tile_max_lat = tile_lat + 1;
let tile_max_lon = tile_lon + 1;
self.min_lat < tile_max_lat as f64
&& self.max_lat > tile_lat as f64
&& self.min_lon < tile_max_lon as f64
&& self.max_lon > tile_lon as f64
}
}
#[derive(Debug, Clone, Default)]
pub struct PreloadStats {
pub tiles_loaded: u64,
pub tiles_already_cached: u64,
pub tiles_failed: u64,
pub tiles_matched: u64,
pub elapsed_ms: u64,
}
pub struct SrtmService {
data_dir: PathBuf,
tile_cache: Cache<(i32, i32), Arc<SrtmTile>>,
hit_count: AtomicU64,
miss_count: AtomicU64,
#[cfg(feature = "download")]
downloader: Option<Downloader>,
}
impl SrtmService {
pub fn new<P: AsRef<Path>>(data_dir: P, cache_size: u64) -> Self {
Self {
data_dir: data_dir.as_ref().to_path_buf(),
tile_cache: Cache::builder().max_capacity(cache_size).build(),
hit_count: AtomicU64::new(0),
miss_count: AtomicU64::new(0),
#[cfg(feature = "download")]
downloader: None,
}
}
pub fn builder<P: AsRef<Path>>(data_dir: P) -> SrtmServiceBuilder {
SrtmServiceBuilder::new(data_dir)
}
pub fn get_elevation(&self, lat: f64, lon: f64) -> Result<Option<i16>> {
match self.load_tile_for_coords(lat, lon) {
Ok(tile) => {
let v = tile.get_elevation(lat, lon)?;
Ok(if v == VOID_VALUE { None } else { Some(v) })
}
Err(SrtmError::FileNotFound { .. }) | Err(SrtmError::TileNotAvailable { .. }) => {
Ok(None)
}
Err(e) => Err(e),
}
}
pub fn get_elevation_floor(&self, lat: f64, lon: f64) -> Result<Option<i16>> {
match self.load_tile_for_coords(lat, lon) {
Ok(tile) => {
let v = tile.get_elevation_floor(lat, lon)?;
Ok(if v == VOID_VALUE { None } else { Some(v) })
}
Err(SrtmError::FileNotFound { .. }) | Err(SrtmError::TileNotAvailable { .. }) => {
Ok(None)
}
Err(e) => Err(e),
}
}
pub fn get_elevation_interpolated(&self, lat: f64, lon: f64) -> Result<Option<f64>> {
match self.load_tile_for_coords(lat, lon) {
Ok(tile) => tile.get_elevation_interpolated(lat, lon),
Err(SrtmError::FileNotFound { .. }) | Err(SrtmError::TileNotAvailable { .. }) => {
Ok(None)
}
Err(e) => Err(e),
}
}
pub fn get_elevations_batch(&self, coords: &[(f64, f64)], default: i16) -> Vec<i16> {
self.batch_with_tile_grouping(coords, default, |tile, lat, lon| {
match tile.get_elevation(lat, lon) {
Ok(v) if v != VOID_VALUE => Some(v),
_ => None,
}
})
}
pub fn get_elevations_batch_floor(&self, coords: &[(f64, f64)], default: i16) -> Vec<i16> {
self.batch_with_tile_grouping(coords, default, |tile, lat, lon| {
match tile.get_elevation_floor(lat, lon) {
Ok(v) if v != VOID_VALUE => Some(v),
_ => None,
}
})
}
pub fn get_elevations_batch_interpolated(
&self,
coords: &[(f64, f64)],
default: f64,
) -> Vec<f64> {
self.batch_with_tile_grouping(coords, default, |tile, lat, lon| {
tile.get_elevation_interpolated(lat, lon).ok().flatten()
})
}
fn batch_with_tile_grouping<T: Copy>(
&self,
coords: &[(f64, f64)],
default: T,
elevation_fn: impl Fn(&SrtmTile, f64, f64) -> Option<T>,
) -> Vec<T> {
let mut results = vec![default; coords.len()];
let mut common_key: Option<(i32, i32)> = None;
let mut all_same_tile = true;
for &(lat, lon) in coords {
if !(-60.0..=60.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
continue;
}
let key = (lat.floor() as i32, lon.floor() as i32);
match common_key {
None => common_key = Some(key),
Some(k) if k == key => {}
_ => {
all_same_tile = false;
break;
}
}
}
if all_same_tile {
if let Some(key) = common_key {
if let Ok(tile) = self.load_tile(key) {
for (i, &(lat, lon)) in coords.iter().enumerate() {
if !(-60.0..=60.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
continue;
}
if let Some(v) = elevation_fn(&tile, lat, lon) {
results[i] = v;
}
}
}
}
return results;
}
let mut groups: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
for (i, &(lat, lon)) in coords.iter().enumerate() {
if !(-60.0..=60.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
continue;
}
let key = (lat.floor() as i32, lon.floor() as i32);
groups.entry(key).or_default().push(i);
}
for (key, indices) in &groups {
let tile = match self.load_tile(*key) {
Ok(t) => t,
Err(_) => continue, };
for &i in indices {
let (lat, lon) = coords[i];
if let Some(v) = elevation_fn(&tile, lat, lon) {
results[i] = v;
}
}
}
results
}
fn load_tile_for_coords(&self, lat: f64, lon: f64) -> Result<Arc<SrtmTile>> {
if !(-60.0..=60.0).contains(&lat) {
return Err(SrtmError::OutOfBounds { lat, lon });
}
if !(-180.0..=180.0).contains(&lon) {
return Err(SrtmError::OutOfBounds { lat, lon });
}
let key = (lat.floor() as i32, lon.floor() as i32);
self.load_tile(key)
}
fn load_tile(&self, key: (i32, i32)) -> Result<Arc<SrtmTile>> {
if let Some(tile) = self.tile_cache.get(&key) {
self.hit_count.fetch_add(1, Ordering::Relaxed);
return Ok(tile);
}
self.miss_count.fetch_add(1, Ordering::Relaxed);
let filename = coords_to_filename(key.0, key.1);
let path = self.data_dir.join(&filename);
if !path.exists() {
let zip_path = self.data_dir.join(format!("{}.zip", filename));
if zip_path.exists() {
self.extract_hgt_from_zip(&zip_path, &filename)?;
} else {
#[cfg(feature = "download")]
{
if let Some(ref downloader) = self.downloader {
downloader.download_tile_by_name(&filename, &self.data_dir)?;
} else {
return Err(SrtmError::TileNotAvailable { filename });
}
}
#[cfg(not(feature = "download"))]
{
return Err(SrtmError::FileNotFound { path });
}
}
}
let tile = Arc::new(SrtmTile::from_file_with_coords(&path, key.0, key.1)?);
self.tile_cache.insert(key, tile.clone());
Ok(tile)
}
fn extract_hgt_from_zip(&self, zip_path: &Path, filename: &str) -> Result<()> {
let file = std::fs::File::open(zip_path).map_err(SrtmError::Io)?;
let mut archive = zip::ZipArchive::new(file)
.map_err(|e| SrtmError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
let mut found = false;
for i in 0..archive.len() {
let mut entry = archive.by_index(i).map_err(|e| {
SrtmError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
})?;
let entry_name = entry.name().to_string();
if entry_name.ends_with(".hgt") || entry_name == filename {
let out_path = self.data_dir.join(filename);
let mut out_file = std::fs::File::create(&out_path)?;
std::io::copy(&mut entry, &mut out_file)?;
found = true;
break;
}
}
if !found {
return Err(SrtmError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("No .hgt file found in {}", zip_path.display()),
)));
}
Ok(())
}
#[cfg(feature = "download")]
pub fn has_auto_download(&self) -> bool {
self.downloader.is_some()
}
pub fn cache_stats(&self) -> CacheStats {
CacheStats {
entry_count: self.tile_cache.entry_count(),
hit_count: self.hit_count.load(Ordering::Relaxed),
miss_count: self.miss_count.load(Ordering::Relaxed),
}
}
pub fn data_dir(&self) -> &Path {
&self.data_dir
}
pub fn cache_capacity(&self) -> u64 {
self.tile_cache.policy().max_capacity().unwrap_or(0)
}
pub fn invalidate_tile(&self, filename: &str) {
if let Some(key) = filename_to_lat_lon(filename) {
self.tile_cache.invalidate(&key);
}
}
pub fn clear_cache(&self) {
self.tile_cache.invalidate_all();
}
pub fn scan_tile_files(&self) -> Vec<String> {
let mut filenames = HashSet::new();
let entries = match std::fs::read_dir(&self.data_dir) {
Ok(entries) => entries,
Err(_) => return Vec::new(),
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.ends_with(".hgt.zip") {
let hgt_name = name.strip_suffix(".zip").unwrap();
filenames.insert(hgt_name.to_string());
} else if name.ends_with(".hgt") {
filenames.insert(name.to_string());
}
}
let mut result: Vec<String> = filenames.into_iter().collect();
result.sort();
result
}
pub fn preload(&self, bounds: Option<&[BoundingBox]>) -> PreloadStats {
let start = Instant::now();
let mut stats = PreloadStats::default();
let filenames = self.scan_tile_files();
for filename in &filenames {
let key = match filename_to_lat_lon(filename) {
Some(k) => k,
None => continue, };
if let Some(boxes) = bounds {
if !boxes.iter().any(|b| b.overlaps_tile(key.0, key.1)) {
continue;
}
}
stats.tiles_matched += 1;
if self.tile_cache.get(&key).is_some() {
stats.tiles_already_cached += 1;
continue;
}
match self.load_tile(key) {
Ok(_) => stats.tiles_loaded += 1,
Err(_) => stats.tiles_failed += 1,
}
}
stats.elapsed_ms = start.elapsed().as_millis() as u64;
stats
}
}
pub struct SrtmServiceBuilder {
data_dir: PathBuf,
cache_size: u64,
#[cfg(feature = "download")]
download_config: Option<DownloadConfig>,
}
impl SrtmServiceBuilder {
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
Self {
data_dir: data_dir.as_ref().to_path_buf(),
cache_size: 100, #[cfg(feature = "download")]
download_config: None,
}
}
pub fn from_env() -> Result<Self> {
let data_dir = std::env::var("HTG_DATA_DIR").map_err(|_| {
SrtmError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"HTG_DATA_DIR environment variable not set",
))
})?;
let cache_size: u64 = std::env::var("HTG_CACHE_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(100);
#[cfg(feature = "download")]
let download_config = {
if let Ok(source) = std::env::var("HTG_DOWNLOAD_SOURCE") {
match source.to_lowercase().as_str() {
"ardupilot" | "ardupilot-srtm1" => Some(DownloadConfig::ardupilot_srtm1()),
"ardupilot-srtm3" => Some(DownloadConfig::ardupilot_srtm3()),
_ => {
None
}
}
} else {
None
}
.or_else(|| {
match std::env::var("HTG_DOWNLOAD_URL") {
Ok(url_template) => {
if let Ok(gzip_setting) = std::env::var("HTG_DOWNLOAD_GZIP") {
let is_gzipped =
gzip_setting.eq_ignore_ascii_case("true") || gzip_setting == "1";
let compression = if is_gzipped {
crate::download::Compression::Gzip
} else {
crate::download::Compression::None
};
Some(DownloadConfig::with_url_template_and_compression(
url_template,
compression,
))
} else {
Some(DownloadConfig::with_url_template(url_template))
}
}
Err(_) => None,
}
})
};
Ok(Self {
data_dir: PathBuf::from(data_dir),
cache_size,
#[cfg(feature = "download")]
download_config,
})
}
pub fn data_dir<P: AsRef<Path>>(mut self, path: P) -> Self {
self.data_dir = path.as_ref().to_path_buf();
self
}
pub fn cache_size(mut self, size: u64) -> Self {
self.cache_size = size;
self
}
#[cfg(feature = "download")]
pub fn auto_download(mut self, config: DownloadConfig) -> Self {
self.download_config = Some(config);
self
}
#[cfg(feature = "download")]
pub fn build(self) -> Result<SrtmService> {
let downloader = match self.download_config {
Some(config) => Some(Downloader::new(config)?),
None => None,
};
Ok(SrtmService {
data_dir: self.data_dir,
tile_cache: Cache::builder().max_capacity(self.cache_size).build(),
hit_count: AtomicU64::new(0),
miss_count: AtomicU64::new(0),
downloader,
})
}
#[cfg(not(feature = "download"))]
pub fn build(self) -> SrtmService {
SrtmService {
data_dir: self.data_dir,
tile_cache: Cache::builder().max_capacity(self.cache_size).build(),
hit_count: AtomicU64::new(0),
miss_count: AtomicU64::new(0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::TempDir;
const SRTM3_SIZE: usize = 1201 * 1201 * 2;
const SRTM3_SAMPLES: usize = 1201;
fn create_test_tile(dir: &Path, filename: &str, center_elevation: i16) {
let mut data = vec![0u8; SRTM3_SIZE];
let center_offset = (600 * SRTM3_SAMPLES + 600) * 2;
let bytes = center_elevation.to_be_bytes();
data[center_offset] = bytes[0];
data[center_offset + 1] = bytes[1];
let path = dir.join(filename);
let mut file = fs::File::create(path).unwrap();
file.write_all(&data).unwrap();
}
#[test]
fn test_service_basic() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let elevation = service.get_elevation(35.5, 138.5).unwrap();
assert_eq!(elevation, Some(500));
}
#[test]
fn test_cache_hit() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let _ = service.get_elevation(35.5, 138.5).unwrap();
let stats1 = service.cache_stats();
assert_eq!(stats1.miss_count, 1);
assert_eq!(stats1.hit_count, 0);
let _ = service.get_elevation(35.6, 138.6).unwrap();
let stats2 = service.cache_stats();
assert_eq!(stats2.miss_count, 1);
assert_eq!(stats2.hit_count, 1);
}
#[test]
fn test_multiple_tiles() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
create_test_tile(temp_dir.path(), "N36E138.hgt", 1000);
let service = SrtmService::new(temp_dir.path(), 10);
let elev1 = service.get_elevation(35.5, 138.5).unwrap();
let elev2 = service.get_elevation(36.5, 138.5).unwrap();
assert_eq!(elev1, Some(500));
assert_eq!(elev2, Some(1000));
let stats = service.cache_stats();
assert_eq!(stats.miss_count, 2);
}
#[test]
fn test_invalid_coordinates() {
let temp_dir = TempDir::new().unwrap();
let service = SrtmService::new(temp_dir.path(), 10);
assert!(service.get_elevation(70.0, 0.0).is_err());
assert!(service.get_elevation(-70.0, 0.0).is_err());
assert!(service.get_elevation(0.0, 200.0).is_err());
assert!(service.get_elevation(0.0, -200.0).is_err());
}
#[test]
fn test_missing_file() {
let temp_dir = TempDir::new().unwrap();
let service = SrtmService::new(temp_dir.path(), 10);
let result = service.get_elevation(50.0, 50.0).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_missing_file_interpolated() {
let temp_dir = TempDir::new().unwrap();
let service = SrtmService::new(temp_dir.path(), 10);
let result = service.get_elevation_interpolated(50.0, 50.0).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_void_data_returns_none() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", VOID_VALUE);
let service = SrtmService::new(temp_dir.path(), 10);
let result = service.get_elevation(35.5, 138.5).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_get_elevations_batch() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let coords = vec![
(35.5, 138.5), (50.0, 50.0), (35.1, 138.1), ];
let results = service.get_elevations_batch(&coords, -1);
assert_eq!(results[0], 500);
assert_eq!(results[1], -1); assert_eq!(results[2], 0);
}
#[test]
fn test_get_elevations_batch_interpolated() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let coords = vec![
(35.5, 138.5), (50.0, 50.0), ];
let results = service.get_elevations_batch_interpolated(&coords, -1.0);
assert_eq!(results.len(), 2);
assert!((results[0] - 500.0).abs() < 1.0); assert_eq!(results[1], -1.0); }
#[test]
fn test_hgt_zip_extraction() {
let temp_dir = TempDir::new().unwrap();
let hgt_data = vec![0u8; SRTM3_SIZE];
let zip_path = temp_dir.path().join("N40E010.hgt.zip");
let file = fs::File::create(&zip_path).unwrap();
let mut zip_writer = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
zip_writer.start_file("N40E010.hgt", options).unwrap();
zip_writer.write_all(&hgt_data).unwrap();
zip_writer.finish().unwrap();
let service = SrtmService::new(temp_dir.path(), 10);
let result = service.get_elevation(40.5, 10.5).unwrap();
assert_eq!(result, Some(0));
assert!(temp_dir.path().join("N40E010.hgt").exists());
}
#[test]
fn test_cache_stats() {
let stats = CacheStats {
entry_count: 5,
hit_count: 80,
miss_count: 20,
};
assert_eq!(stats.hit_rate(), 0.8);
let empty_stats = CacheStats::default();
assert_eq!(empty_stats.hit_rate(), 0.0);
}
#[test]
fn test_clear_cache() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let _ = service.get_elevation(35.5, 138.5).unwrap();
assert_eq!(service.cache_stats().miss_count, 1);
service.clear_cache();
let _ = service.get_elevation(35.5, 138.5).unwrap();
assert_eq!(service.cache_stats().miss_count, 2);
}
#[test]
fn test_cache_capacity() {
let temp_dir = TempDir::new().unwrap();
let service = SrtmService::new(temp_dir.path(), 100);
assert_eq!(service.cache_capacity(), 100);
}
#[test]
fn test_get_elevation_floor() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let elev = service.get_elevation_floor(35.5, 138.5).unwrap();
assert_eq!(elev, Some(500));
}
#[test]
fn test_get_elevation_floor_missing_tile() {
let temp_dir = TempDir::new().unwrap();
let service = SrtmService::new(temp_dir.path(), 10);
let result = service.get_elevation_floor(50.0, 50.0).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_get_elevations_batch_floor() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let coords = vec![
(35.5, 138.5), (50.0, 50.0), ];
let results = service.get_elevations_batch_floor(&coords, -1);
assert_eq!(results[0], 500);
assert_eq!(results[1], -1); }
#[test]
fn test_from_env_missing_data_dir() {
let original = std::env::var("HTG_DATA_DIR").ok();
std::env::remove_var("HTG_DATA_DIR");
let result = SrtmServiceBuilder::from_env();
assert!(result.is_err());
if let Some(val) = original {
std::env::set_var("HTG_DATA_DIR", val);
}
}
#[test]
fn test_from_env_with_values() {
let temp_dir = TempDir::new().unwrap();
let orig_dir = std::env::var("HTG_DATA_DIR").ok();
let orig_size = std::env::var("HTG_CACHE_SIZE").ok();
std::env::set_var("HTG_DATA_DIR", temp_dir.path());
std::env::set_var("HTG_CACHE_SIZE", "50");
let builder = SrtmServiceBuilder::from_env().unwrap();
assert_eq!(builder.data_dir, temp_dir.path());
assert_eq!(builder.cache_size, 50);
match orig_dir {
Some(v) => std::env::set_var("HTG_DATA_DIR", v),
None => std::env::remove_var("HTG_DATA_DIR"),
}
match orig_size {
Some(v) => std::env::set_var("HTG_CACHE_SIZE", v),
None => std::env::remove_var("HTG_CACHE_SIZE"),
}
}
#[test]
fn test_from_env_default_cache_size() {
let temp_dir = TempDir::new().unwrap();
let orig_dir = std::env::var("HTG_DATA_DIR").ok();
let orig_size = std::env::var("HTG_CACHE_SIZE").ok();
std::env::set_var("HTG_DATA_DIR", temp_dir.path());
std::env::remove_var("HTG_CACHE_SIZE");
let builder = SrtmServiceBuilder::from_env().unwrap();
assert_eq!(builder.cache_size, 100);
match orig_dir {
Some(v) => std::env::set_var("HTG_DATA_DIR", v),
None => std::env::remove_var("HTG_DATA_DIR"),
}
match orig_size {
Some(v) => std::env::set_var("HTG_CACHE_SIZE", v),
None => std::env::remove_var("HTG_CACHE_SIZE"),
}
}
#[test]
fn test_preload_all_tiles() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
create_test_tile(temp_dir.path(), "N36E139.hgt", 1000);
let service = SrtmService::new(temp_dir.path(), 10);
let stats = service.preload(None);
assert_eq!(stats.tiles_matched, 2);
assert_eq!(stats.tiles_loaded, 2);
assert_eq!(stats.tiles_already_cached, 0);
assert_eq!(stats.tiles_failed, 0);
}
#[test]
fn test_preload_with_bounding_box() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
create_test_tile(temp_dir.path(), "N50E010.hgt", 1000);
let service = SrtmService::new(temp_dir.path(), 10);
let bbox = BoundingBox::new(34.0, 137.0, 37.0, 140.0);
let stats = service.preload(Some(&[bbox]));
assert_eq!(stats.tiles_matched, 1);
assert_eq!(stats.tiles_loaded, 1);
let elev = service.get_elevation(35.5, 138.5).unwrap();
assert_eq!(elev, Some(500));
}
#[test]
fn test_preload_multiple_bounding_boxes() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
create_test_tile(temp_dir.path(), "N50E010.hgt", 1000);
create_test_tile(temp_dir.path(), "N20E020.hgt", 200);
let service = SrtmService::new(temp_dir.path(), 10);
let japan = BoundingBox::new(34.0, 137.0, 37.0, 140.0);
let europe = BoundingBox::new(49.0, 9.0, 52.0, 12.0);
let stats = service.preload(Some(&[japan, europe]));
assert_eq!(stats.tiles_matched, 2); assert_eq!(stats.tiles_loaded, 2);
}
#[test]
fn test_preload_already_cached() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
create_test_tile(temp_dir.path(), "N36E139.hgt", 1000);
let service = SrtmService::new(temp_dir.path(), 10);
let stats1 = service.preload(None);
assert_eq!(stats1.tiles_loaded, 2);
assert_eq!(stats1.tiles_already_cached, 0);
let stats2 = service.preload(None);
assert_eq!(stats2.tiles_loaded, 0);
assert_eq!(stats2.tiles_already_cached, 2);
}
#[test]
fn test_preload_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let service = SrtmService::new(temp_dir.path(), 10);
let stats = service.preload(None);
assert_eq!(stats.tiles_matched, 0);
assert_eq!(stats.tiles_loaded, 0);
}
#[test]
fn test_preload_with_zip_files() {
let temp_dir = TempDir::new().unwrap();
let hgt_data = vec![0u8; SRTM3_SIZE];
let zip_path = temp_dir.path().join("N40E010.hgt.zip");
let file = fs::File::create(&zip_path).unwrap();
let mut zip_writer = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
zip_writer.start_file("N40E010.hgt", options).unwrap();
zip_writer.write_all(&hgt_data).unwrap();
zip_writer.finish().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let stats = service.preload(None);
assert_eq!(stats.tiles_matched, 2);
assert_eq!(stats.tiles_loaded, 2);
assert_eq!(stats.tiles_failed, 0);
}
#[test]
fn test_bounding_box_overlaps_tile() {
let bbox = BoundingBox::new(35.5, 138.5, 36.5, 139.5);
assert!(bbox.overlaps_tile(35, 138));
let bbox = BoundingBox::new(40.0, 140.0, 41.0, 141.0);
assert!(!bbox.overlaps_tile(35, 138));
let bbox = BoundingBox::new(36.0, 139.0, 37.0, 140.0);
assert!(!bbox.overlaps_tile(35, 138));
let bbox = BoundingBox::new(34.0, 137.0, 37.0, 140.0);
assert!(bbox.overlaps_tile(35, 138));
let bbox = BoundingBox::new(35.2, 138.2, 35.8, 138.8);
assert!(bbox.overlaps_tile(35, 138));
let bbox = BoundingBox::new(-13.5, -78.5, -11.5, -76.5);
assert!(bbox.overlaps_tile(-13, -78));
assert!(bbox.overlaps_tile(-12, -78));
assert!(bbox.overlaps_tile(-13, -77));
}
#[test]
fn test_preload_no_match() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let service = SrtmService::new(temp_dir.path(), 10);
let bbox = BoundingBox::new(-50.0, -50.0, -49.0, -49.0);
let stats = service.preload(Some(&[bbox]));
assert_eq!(stats.tiles_matched, 0);
assert_eq!(stats.tiles_loaded, 0);
}
#[test]
fn test_scan_tile_files() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
create_test_tile(temp_dir.path(), "N36E139.hgt", 1000);
fs::write(temp_dir.path().join("readme.txt"), "not a tile").unwrap();
let service = SrtmService::new(temp_dir.path(), 10);
let files = service.scan_tile_files();
assert_eq!(files.len(), 2);
assert_eq!(files[0], "N35E138.hgt");
assert_eq!(files[1], "N36E139.hgt");
}
#[test]
fn test_scan_tile_files_deduplicates_zip() {
let temp_dir = TempDir::new().unwrap();
create_test_tile(temp_dir.path(), "N35E138.hgt", 500);
let hgt_data = vec![0u8; SRTM3_SIZE];
let zip_path = temp_dir.path().join("N35E138.hgt.zip");
let file = fs::File::create(&zip_path).unwrap();
let mut zip_writer = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
zip_writer.start_file("N35E138.hgt", options).unwrap();
zip_writer.write_all(&hgt_data).unwrap();
zip_writer.finish().unwrap();
let service = SrtmService::new(temp_dir.path(), 10);
let files = service.scan_tile_files();
assert_eq!(files.len(), 1);
assert_eq!(files[0], "N35E138.hgt");
}
}