use crate::coord::GeoCoord;
use crate::tile::{tile_to_geo, tile_xy_to_geo, TileId};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ElevationGrid {
pub width: u32,
pub height: u32,
pub min_elev: f32,
pub max_elev: f32,
pub data: Vec<f32>,
pub tile: TileId,
}
impl ElevationGrid {
pub fn flat(tile: TileId, width: u32, height: u32) -> Self {
Self {
width,
height,
min_elev: 0.0,
max_elev: 0.0,
data: vec![0.0; (width * height) as usize],
tile,
}
}
pub fn from_data(tile: TileId, width: u32, height: u32, mut data: Vec<f32>) -> Option<Self> {
if data.len() != (width * height) as usize {
return None;
}
let mut min_elev = f32::MAX;
let mut max_elev = f32::MIN;
for v in data.iter_mut() {
*v = v.clamp(-500.0, 10_000.0);
if *v < min_elev {
min_elev = *v;
}
if *v > max_elev {
max_elev = *v;
}
}
Some(Self {
width,
height,
min_elev,
max_elev,
data,
tile,
})
}
#[inline]
pub fn elevation_range(&self) -> f32 {
self.max_elev - self.min_elev
}
pub fn sample(&self, u: f64, v: f64) -> Option<f32> {
if self.data.is_empty() || self.width == 0 || self.height == 0 {
return None;
}
let u = u.clamp(0.0, 1.0);
let v = v.clamp(0.0, 1.0);
let fx = u * (self.width - 1) as f64;
let fy = v * (self.height - 1) as f64;
let x0 = (fx.floor() as u32).min(self.width - 1);
let y0 = (fy.floor() as u32).min(self.height - 1);
let x1 = (x0 + 1).min(self.width - 1);
let y1 = (y0 + 1).min(self.height - 1);
let sx = (fx - x0 as f64) as f32;
let sy = (fy - y0 as f64) as f32;
let v00 = self.data[(y0 * self.width + x0) as usize];
let v10 = self.data[(y0 * self.width + x1) as usize];
let v01 = self.data[(y1 * self.width + x0) as usize];
let v11 = self.data[(y1 * self.width + x1) as usize];
let top = v00 * (1.0 - sx) + v10 * sx;
let bot = v01 * (1.0 - sx) + v11 * sx;
Some(top * (1.0 - sy) + bot * sy)
}
pub fn sample_geo(&self, coord: &GeoCoord) -> Option<f32> {
let nw = tile_to_geo(&self.tile);
let se = tile_xy_to_geo(
self.tile.zoom,
self.tile.x as f64 + 1.0,
self.tile.y as f64 + 1.0,
);
let lon_range = se.lon - nw.lon;
let lat_range = nw.lat - se.lat;
if lon_range.abs() < 1e-12 || lat_range.abs() < 1e-12 {
return None;
}
let u = (coord.lon - nw.lon) / lon_range;
let v = (nw.lat - coord.lat) / lat_range;
self.sample(u, v)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flat_grid() {
let grid = ElevationGrid::flat(TileId::new(0, 0, 0), 3, 3);
assert_eq!(grid.data.len(), 9);
assert_eq!(grid.min_elev, 0.0);
assert_eq!(grid.max_elev, 0.0);
assert_eq!(grid.sample(0.5, 0.5), Some(0.0));
}
#[test]
fn from_data_wrong_size() {
assert!(ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, vec![0.0]).is_none());
}
#[test]
fn min_max_elev() {
let data = vec![-100.0, 50.0, 200.0, 0.0];
let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
assert!((grid.min_elev - (-100.0)).abs() < 1e-6);
assert!((grid.max_elev - 200.0).abs() < 1e-6);
}
#[test]
fn from_data_clamps_extreme_elevations() {
let data = vec![-32_768.0, -600.0, 10_500.0, 25.0];
let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
assert!((grid.min_elev - (-500.0)).abs() < 1e-6);
assert!((grid.max_elev - 10_000.0).abs() < 1e-6);
assert_eq!(grid.data, vec![-500.0, -500.0, 10_000.0, 25.0]);
}
#[test]
fn elevation_range() {
let data = vec![-100.0, 50.0, 200.0, 0.0];
let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
assert!((grid.elevation_range() - 300.0).abs() < 1e-6);
}
#[test]
fn elevation_range_flat() {
let grid = ElevationGrid::flat(TileId::new(0, 0, 0), 4, 4);
assert!((grid.elevation_range()).abs() < 1e-6);
}
#[test]
fn sample_corners() {
let data = vec![0.0, 10.0, 20.0, 30.0];
let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
assert!((grid.sample(0.0, 0.0).unwrap() - 0.0).abs() < 1e-6);
assert!((grid.sample(1.0, 0.0).unwrap() - 10.0).abs() < 1e-6);
assert!((grid.sample(0.0, 1.0).unwrap() - 20.0).abs() < 1e-6);
assert!((grid.sample(1.0, 1.0).unwrap() - 30.0).abs() < 1e-6);
}
#[test]
fn bilinear_midpoint() {
let data = vec![0.0, 10.0, 20.0, 30.0];
let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
let mid = grid.sample(0.5, 0.5).unwrap();
assert!((mid - 15.0).abs() < 1e-6);
}
#[test]
fn sample_1x1_grid() {
let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 1, 1, vec![42.0]).unwrap();
assert!((grid.sample(0.0, 0.0).unwrap() - 42.0).abs() < 1e-6);
assert!((grid.sample(0.5, 0.5).unwrap() - 42.0).abs() < 1e-6);
assert!((grid.sample(1.0, 1.0).unwrap() - 42.0).abs() < 1e-6);
}
#[test]
fn sample_clamps_out_of_range_uv() {
let data = vec![0.0, 10.0, 20.0, 30.0];
let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
assert!((grid.sample(-1.0, -1.0).unwrap() - 0.0).abs() < 1e-6);
assert!((grid.sample(2.0, 2.0).unwrap() - 30.0).abs() < 1e-6);
}
#[test]
fn sample_empty_grid_returns_none() {
let grid = ElevationGrid {
width: 0,
height: 0,
min_elev: 0.0,
max_elev: 0.0,
data: vec![],
tile: TileId::new(0, 0, 0),
};
assert!(grid.sample(0.5, 0.5).is_none());
}
#[test]
fn sample_geo_tile_center() {
let data = vec![100.0, 200.0, 300.0, 400.0];
let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
let center = GeoCoord::from_lat_lon(0.0, 0.0);
let elev = grid.sample_geo(¢er).unwrap();
assert!((elev - 250.0).abs() < 1.0);
}
#[test]
fn sample_geo_nw_corner() {
let data = vec![10.0, 20.0, 30.0, 40.0];
let grid = ElevationGrid::from_data(TileId::new(1, 0, 0), 2, 2, data).unwrap();
let nw = tile_to_geo(&TileId::new(1, 0, 0));
let elev = grid.sample_geo(&nw).unwrap();
assert!((elev - 10.0).abs() < 1e-3);
}
#[test]
fn partial_eq() {
let a = ElevationGrid::flat(TileId::new(5, 10, 10), 4, 4);
let b = ElevationGrid::flat(TileId::new(5, 10, 10), 4, 4);
assert_eq!(a, b);
}
}