use std::f64::consts::PI;
use quantized_mesh::TileBounds;
use crate::normals::BufferedElevations;
use crate::tile_coords::web_mercator;
#[derive(Debug, Clone)]
pub struct MercatorDem {
zoom: u8,
x0: u32,
y0: u32,
tiles_x: u32,
tiles_y: u32,
tile_size: u32,
elev: Vec<f32>,
}
impl MercatorDem {
pub fn new(
zoom: u8,
x0: u32,
y0: u32,
tiles_x: u32,
tiles_y: u32,
tile_size: u32,
elev: Vec<f32>,
) -> Self {
assert!(
tiles_x > 0 && tiles_y > 0 && tile_size > 0,
"tiles_x, tiles_y and tile_size must be non-zero"
);
let expected = (tiles_x * tile_size) as usize * (tiles_y * tile_size) as usize;
assert_eq!(
elev.len(),
expected,
"stitched elevation length mismatch: expected {expected}, got {}",
elev.len()
);
Self {
zoom,
x0,
y0,
tiles_x,
tiles_y,
tile_size,
elev,
}
}
pub fn from_tiles<F>(
zoom: u8,
x0: u32,
y0: u32,
tiles_x: u32,
tiles_y: u32,
tile_size: u32,
mut get_tile: F,
) -> Self
where
F: FnMut(u8, u32, u32) -> Vec<f32>,
{
let ts = tile_size as usize;
let w = (tiles_x * tile_size) as usize;
let h = (tiles_y * tile_size) as usize;
let mut elev = vec![0f32; w * h];
for tj in 0..tiles_y {
for ti in 0..tiles_x {
let tile = get_tile(zoom, x0 + ti, y0 + tj);
assert_eq!(
tile.len(),
ts * ts,
"tile {}/{}/{} has {} samples, expected {}",
zoom,
x0 + ti,
y0 + tj,
tile.len(),
ts * ts
);
let ox = ti as usize * ts;
let oy = tj as usize * ts;
for r in 0..ts {
let dst = (oy + r) * w + ox;
let src = r * ts;
elev[dst..dst + ts].copy_from_slice(&tile[src..src + ts]);
}
}
}
Self::new(zoom, x0, y0, tiles_x, tiles_y, tile_size, elev)
}
pub fn tiles_covering(
zoom: u8,
west: f64,
south: f64,
east: f64,
north: f64,
) -> (u32, u32, u32, u32) {
let (xw, yn) = web_mercator::lonlat_to_tile(west, north, zoom);
let (xe, ys) = web_mercator::lonlat_to_tile(east, south, zoom);
let x0 = xw.min(xe);
let x1 = xw.max(xe);
let y0 = yn.min(ys);
let y1 = yn.max(ys);
(x0, y0, x1 - x0 + 1, y1 - y0 + 1)
}
#[inline]
pub fn width_px(&self) -> u32 {
self.tiles_x * self.tile_size
}
#[inline]
pub fn height_px(&self) -> u32 {
self.tiles_y * self.tile_size
}
pub fn sample(&self, lon: f64, lat: f64) -> f32 {
let n_tiles = 1u32 << self.zoom;
let world_px = (n_tiles * self.tile_size) as f64;
let lat = lat.clamp(-web_mercator::MAX_LAT, web_mercator::MAX_LAT);
let gx = (lon + 180.0) / 360.0 * world_px;
let lat_rad = lat.to_radians();
let gy = (1.0 - lat_rad.tan().asinh() / PI) / 2.0 * world_px;
let lx = gx - (self.x0 * self.tile_size) as f64 - 0.5;
let ly = gy - (self.y0 * self.tile_size) as f64 - 0.5;
let w = self.width_px() as i64;
let h = self.height_px() as i64;
let fx = lx.floor();
let fy = ly.floor();
let tx = lx - fx;
let ty = ly - fy;
let clamp = |v: i64, max: i64| v.clamp(0, max - 1);
let xi0 = clamp(fx as i64, w);
let xi1 = clamp(fx as i64 + 1, w);
let yi0 = clamp(fy as i64, h);
let yi1 = clamp(fy as i64 + 1, h);
let at = |xi: i64, yi: i64| -> f64 { self.elev[(yi * w + xi) as usize] as f64 };
let top = bilerp(at(xi0, yi0), at(xi1, yi0), tx);
let bot = bilerp(at(xi0, yi1), at(xi1, yi1), tx);
bilerp(top, bot, ty) as f32
}
pub fn geodetic_grid(&self, bounds: &TileBounds, grid_size: u32) -> Vec<f32> {
assert!(grid_size >= 2, "grid_size must be >= 2");
let gs = grid_size as usize;
let lon_span = bounds.east - bounds.west;
let lat_span = bounds.north - bounds.south;
let denom = (grid_size - 1) as f64;
let mut grid = vec![0f32; gs * gs];
for j in 0..gs {
let lat = bounds.north - (j as f64 / denom) * lat_span;
for i in 0..gs {
let lon = bounds.west + (i as f64 / denom) * lon_span;
grid[j * gs + i] = self.sample(lon, lat);
}
}
grid
}
pub fn buffered_geodetic(
&self,
bounds: &TileBounds,
tile_grid_size: u32,
buffer: u32,
) -> BufferedElevations {
assert!(tile_grid_size >= 2, "tile_grid_size must be >= 2");
let denom = (tile_grid_size - 1) as f64;
let cell_lon = (bounds.east - bounds.west) / denom;
let cell_lat = (bounds.north - bounds.south) / denom;
let full = (tile_grid_size + 2 * buffer) as usize;
let buf = buffer as f64;
let mut elev = Vec::with_capacity(full * full);
for j in 0..full {
let lat = bounds.north + buf * cell_lat - (j as f64) * cell_lat;
for i in 0..full {
let lon = bounds.west - buf * cell_lon + (i as f64) * cell_lon;
elev.push(self.sample(lon, lat) as f64);
}
}
BufferedElevations::new(elev, tile_grid_size, buffer)
}
}
#[inline]
fn bilerp(a: f64, b: f64, t: f64) -> f64 {
if a.is_nan() {
b
} else if b.is_nan() {
a
} else {
a * (1.0 - t) + b * t
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sample_hits_pixel_centres() {
let zoom = 4;
let tile_size = 4;
let (x0, y0) = (3, 5);
let w = tile_size;
let h = tile_size;
let elev: Vec<f32> = (0..(w * h)).map(|i| i as f32).collect();
let dem = MercatorDem::new(zoom, x0, y0, 1, 1, tile_size, elev);
let n_tiles = 1u32 << zoom;
let world_px = (n_tiles * tile_size) as f64;
let gx = (x0 * tile_size) as f64 + 1.0 + 0.5;
let gy = (y0 * tile_size) as f64 + 2.0 + 0.5;
let lon = gx / world_px * 360.0 - 180.0;
let m = PI * (1.0 - 2.0 * gy / world_px);
let lat = m.sinh().atan().to_degrees();
let expected = (2 * w + 1) as f32; let got = dem.sample(lon, lat);
assert!(
(got - expected).abs() < 1e-3,
"expected {expected}, got {got}"
);
}
#[test]
fn sample_interpolates_between_posts() {
let zoom = 4;
let tile_size = 4;
let elev: Vec<f32> = (0..(tile_size * tile_size))
.map(|i| (i % tile_size) as f32)
.collect();
let dem = MercatorDem::new(zoom, 0, 0, 1, 1, tile_size, elev);
let world_px = ((1u32 << zoom) * tile_size) as f64;
let lon = 2.0 / world_px * 360.0 - 180.0;
let lat = 0.0; let got = dem.sample(lon, lat);
assert!((got - 1.5).abs() < 1e-3, "expected ~1.5, got {got}");
}
#[test]
fn tiles_covering_is_at_least_one_tile() {
let (w, s, e, n) = web_mercator::tile_to_bounds(12, 3626, 1617);
let (x0, y0, tx, ty) = MercatorDem::tiles_covering(12, w, s, e, n);
assert_eq!(x0, 3626);
assert_eq!(y0, 1617);
assert!((1..=2).contains(&tx));
assert!((1..=2).contains(&ty));
}
#[test]
fn geodetic_grid_of_flat_dem_is_flat() {
let dem = MercatorDem::new(10, 0, 0, 1, 1, 8, vec![42.0f32; 64]);
let bounds = TileBounds::new(0.0, 0.0, 1.0, 1.0);
let grid = dem.geodetic_grid(&bounds, 17);
assert_eq!(grid.len(), 17 * 17);
assert!(grid.iter().all(|&v| (v - 42.0).abs() < 1e-4));
}
#[test]
fn buffered_inner_block_matches_geodetic_grid() {
let zoom = 10;
let tile_size = 64;
let elev: Vec<f32> = (0..(tile_size * tile_size))
.map(|i| ((i % tile_size) + (i / tile_size)) as f32)
.collect();
let dem = MercatorDem::new(zoom, 100, 100, 1, 1, tile_size, elev);
let (w, s, e, n) = web_mercator::tile_to_bounds(zoom, 100, 100);
let inset_x = (e - w) * 0.2;
let inset_y = (n - s) * 0.2;
let bounds = TileBounds::new(w + inset_x, s + inset_y, e - inset_x, n - inset_y);
let tile_grid = 33u32;
let buffer = 2u32;
let plain = dem.geodetic_grid(&bounds, tile_grid);
let buffered = dem.buffered_geodetic(&bounds, tile_grid, buffer);
let full = (tile_grid + 2 * buffer) as usize;
let b = buffer as usize;
let tg = tile_grid as usize;
for j in 0..tg {
for i in 0..tg {
let inner = buffered.elevations[(j + b) * full + (i + b)] as f32;
let p = plain[j * tg + i];
assert!(
(inner - p).abs() < 1e-3,
"inner block mismatch at ({i},{j}): {inner} vs {p}"
);
}
}
}
#[test]
fn end_to_end_with_encode_terrain() {
use crate::terrain::{TerrainOptions, encode_terrain};
use quantized_mesh::DecodedMesh;
let zoom = 12;
let tile_size = 64;
let elev: Vec<f32> = (0..(tile_size * tile_size))
.map(|i| {
let x = (i % tile_size) as f32;
let y = (i / tile_size) as f32;
(x / 8.0).sin() * 20.0 + (y / 8.0).cos() * 15.0
})
.collect();
let dem = MercatorDem::new(zoom, 3626, 1617, 1, 1, tile_size, elev);
let (w, s, e, n) = web_mercator::tile_to_bounds(zoom, 3626, 1617);
let bounds = TileBounds::new(w, s, e, n);
let grid = dem.geodetic_grid(&bounds, 65);
let bytes = encode_terrain(
&grid,
65,
&bounds,
&TerrainOptions {
compression_level: 0,
..Default::default()
},
);
let mesh = DecodedMesh::decode(&bytes).expect("decode");
assert!(mesh.vertices.len() >= 4);
assert!(mesh.indices.len() >= 6);
}
}