1use crate::coord::GeoCoord;
45use crate::tile::{tile_to_geo, tile_xy_to_geo, TileId};
46
47#[derive(Debug, Clone, PartialEq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct ElevationGrid {
58 pub width: u32,
60 pub height: u32,
62 pub min_elev: f32,
64 pub max_elev: f32,
66 pub data: Vec<f32>,
71 pub tile: TileId,
73}
74
75impl ElevationGrid {
76 pub fn flat(tile: TileId, width: u32, height: u32) -> Self {
81 Self {
82 width,
83 height,
84 min_elev: 0.0,
85 max_elev: 0.0,
86 data: vec![0.0; (width * height) as usize],
87 tile,
88 }
89 }
90
91 pub fn from_data(tile: TileId, width: u32, height: u32, mut data: Vec<f32>) -> Option<Self> {
96 if data.len() != (width * height) as usize {
97 return None;
98 }
99 let mut min_elev = f32::MAX;
100 let mut max_elev = f32::MIN;
101 for v in data.iter_mut() {
102 *v = v.clamp(-500.0, 10_000.0);
103 if *v < min_elev {
104 min_elev = *v;
105 }
106 if *v > max_elev {
107 max_elev = *v;
108 }
109 }
110 Some(Self {
111 width,
112 height,
113 min_elev,
114 max_elev,
115 data,
116 tile,
117 })
118 }
119
120 #[inline]
126 pub fn elevation_range(&self) -> f32 {
127 self.max_elev - self.min_elev
128 }
129
130 pub fn sample(&self, u: f64, v: f64) -> Option<f32> {
141 if self.data.is_empty() || self.width == 0 || self.height == 0 {
142 return None;
143 }
144
145 let u = u.clamp(0.0, 1.0);
146 let v = v.clamp(0.0, 1.0);
147
148 let fx = u * (self.width - 1) as f64;
149 let fy = v * (self.height - 1) as f64;
150
151 let x0 = (fx.floor() as u32).min(self.width - 1);
152 let y0 = (fy.floor() as u32).min(self.height - 1);
153 let x1 = (x0 + 1).min(self.width - 1);
154 let y1 = (y0 + 1).min(self.height - 1);
155
156 let sx = (fx - x0 as f64) as f32;
157 let sy = (fy - y0 as f64) as f32;
158
159 let v00 = self.data[(y0 * self.width + x0) as usize];
160 let v10 = self.data[(y0 * self.width + x1) as usize];
161 let v01 = self.data[(y1 * self.width + x0) as usize];
162 let v11 = self.data[(y1 * self.width + x1) as usize];
163
164 let top = v00 * (1.0 - sx) + v10 * sx;
165 let bot = v01 * (1.0 - sx) + v11 * sx;
166 Some(top * (1.0 - sy) + bot * sy)
167 }
168
169 pub fn sample_geo(&self, coord: &GeoCoord) -> Option<f32> {
181 let nw = tile_to_geo(&self.tile);
183 let se = tile_xy_to_geo(
184 self.tile.zoom,
185 self.tile.x as f64 + 1.0,
186 self.tile.y as f64 + 1.0,
187 );
188
189 let lon_range = se.lon - nw.lon;
190 let lat_range = nw.lat - se.lat;
191
192 if lon_range.abs() < 1e-12 || lat_range.abs() < 1e-12 {
193 return None;
194 }
195
196 let u = (coord.lon - nw.lon) / lon_range;
197 let v = (nw.lat - coord.lat) / lat_range;
199
200 self.sample(u, v)
201 }
202}
203
204#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
215 fn flat_grid() {
216 let grid = ElevationGrid::flat(TileId::new(0, 0, 0), 3, 3);
217 assert_eq!(grid.data.len(), 9);
218 assert_eq!(grid.min_elev, 0.0);
219 assert_eq!(grid.max_elev, 0.0);
220 assert_eq!(grid.sample(0.5, 0.5), Some(0.0));
221 }
222
223 #[test]
224 fn from_data_wrong_size() {
225 assert!(ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, vec![0.0]).is_none());
226 }
227
228 #[test]
229 fn min_max_elev() {
230 let data = vec![-100.0, 50.0, 200.0, 0.0];
231 let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
232 assert!((grid.min_elev - (-100.0)).abs() < 1e-6);
233 assert!((grid.max_elev - 200.0).abs() < 1e-6);
234 }
235
236 #[test]
237 fn from_data_clamps_extreme_elevations() {
238 let data = vec![-32_768.0, -600.0, 10_500.0, 25.0];
239 let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
240 assert!((grid.min_elev - (-500.0)).abs() < 1e-6);
241 assert!((grid.max_elev - 10_000.0).abs() < 1e-6);
242 assert_eq!(grid.data, vec![-500.0, -500.0, 10_000.0, 25.0]);
243 }
244
245 #[test]
246 fn elevation_range() {
247 let data = vec![-100.0, 50.0, 200.0, 0.0];
248 let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
249 assert!((grid.elevation_range() - 300.0).abs() < 1e-6);
250 }
251
252 #[test]
253 fn elevation_range_flat() {
254 let grid = ElevationGrid::flat(TileId::new(0, 0, 0), 4, 4);
255 assert!((grid.elevation_range()).abs() < 1e-6);
256 }
257
258 #[test]
261 fn sample_corners() {
262 let data = vec![0.0, 10.0, 20.0, 30.0];
263 let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
264 assert!((grid.sample(0.0, 0.0).unwrap() - 0.0).abs() < 1e-6);
265 assert!((grid.sample(1.0, 0.0).unwrap() - 10.0).abs() < 1e-6);
266 assert!((grid.sample(0.0, 1.0).unwrap() - 20.0).abs() < 1e-6);
267 assert!((grid.sample(1.0, 1.0).unwrap() - 30.0).abs() < 1e-6);
268 }
269
270 #[test]
271 fn bilinear_midpoint() {
272 let data = vec![0.0, 10.0, 20.0, 30.0];
273 let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
274 let mid = grid.sample(0.5, 0.5).unwrap();
275 assert!((mid - 15.0).abs() < 1e-6);
277 }
278
279 #[test]
280 fn sample_1x1_grid() {
281 let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 1, 1, vec![42.0]).unwrap();
282 assert!((grid.sample(0.0, 0.0).unwrap() - 42.0).abs() < 1e-6);
284 assert!((grid.sample(0.5, 0.5).unwrap() - 42.0).abs() < 1e-6);
285 assert!((grid.sample(1.0, 1.0).unwrap() - 42.0).abs() < 1e-6);
286 }
287
288 #[test]
289 fn sample_clamps_out_of_range_uv() {
290 let data = vec![0.0, 10.0, 20.0, 30.0];
291 let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
292 assert!((grid.sample(-1.0, -1.0).unwrap() - 0.0).abs() < 1e-6);
294 assert!((grid.sample(2.0, 2.0).unwrap() - 30.0).abs() < 1e-6);
296 }
297
298 #[test]
299 fn sample_empty_grid_returns_none() {
300 let grid = ElevationGrid {
301 width: 0,
302 height: 0,
303 min_elev: 0.0,
304 max_elev: 0.0,
305 data: vec![],
306 tile: TileId::new(0, 0, 0),
307 };
308 assert!(grid.sample(0.5, 0.5).is_none());
309 }
310
311 #[test]
314 fn sample_geo_tile_center() {
315 let data = vec![100.0, 200.0, 300.0, 400.0];
317 let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
318
319 let center = GeoCoord::from_lat_lon(0.0, 0.0);
321 let elev = grid.sample_geo(¢er).unwrap();
322 assert!((elev - 250.0).abs() < 1.0);
324 }
325
326 #[test]
327 fn sample_geo_nw_corner() {
328 let data = vec![10.0, 20.0, 30.0, 40.0];
330 let grid = ElevationGrid::from_data(TileId::new(1, 0, 0), 2, 2, data).unwrap();
331
332 let nw = tile_to_geo(&TileId::new(1, 0, 0));
334 let elev = grid.sample_geo(&nw).unwrap();
335 assert!((elev - 10.0).abs() < 1e-3);
336 }
337
338 #[test]
341 fn partial_eq() {
342 let a = ElevationGrid::flat(TileId::new(5, 10, 10), 4, 4);
343 let b = ElevationGrid::flat(TileId::new(5, 10, 10), 4, 4);
344 assert_eq!(a, b);
345 }
346}