use crate::lunar::{mcmf_to_selenographic, R_MOON_M};
type Vec3 = [f64; 3];
fn norm(v: Vec3) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[derive(Clone, Debug, PartialEq)]
pub struct DemTile {
pub lat_min_deg: f64,
pub lat_max_deg: f64,
pub lon_min_deg: f64,
pub lon_max_deg: f64,
pub n_lat: usize,
pub n_lon: usize,
pub heights_m: Vec<f64>,
}
impl DemTile {
pub fn lat_of(&self, i: usize) -> f64 {
if self.n_lat <= 1 {
self.lat_min_deg
} else {
self.lat_min_deg
+ (self.lat_max_deg - self.lat_min_deg) * (i as f64) / ((self.n_lat - 1) as f64)
}
}
pub fn lon_of(&self, j: usize) -> f64 {
if self.n_lon <= 1 {
self.lon_min_deg
} else {
self.lon_min_deg
+ (self.lon_max_deg - self.lon_min_deg) * (j as f64) / ((self.n_lon - 1) as f64)
}
}
pub fn height_at(&self, lat_deg: f64, lon_deg: f64) -> f64 {
if self.heights_m.is_empty() {
return 0.0;
}
let fi = if self.n_lat <= 1 {
0.0
} else {
((lat_deg - self.lat_min_deg) / (self.lat_max_deg - self.lat_min_deg)).clamp(0.0, 1.0)
* ((self.n_lat - 1) as f64)
};
let fj = if self.n_lon <= 1 {
0.0
} else {
((lon_deg - self.lon_min_deg) / (self.lon_max_deg - self.lon_min_deg)).clamp(0.0, 1.0)
* ((self.n_lon - 1) as f64)
};
let i0 = fi.floor() as usize;
let j0 = fj.floor() as usize;
let i1 = (i0 + 1).min(self.n_lat - 1);
let j1 = (j0 + 1).min(self.n_lon - 1);
let di = fi - i0 as f64;
let dj = fj - j0 as f64;
let h = |i: usize, j: usize| self.heights_m[i * self.n_lon + j];
let top = h(i0, j0) * (1.0 - dj) + h(i0, j1) * dj;
let bot = h(i1, j0) * (1.0 - dj) + h(i1, j1) * dj;
top * (1.0 - di) + bot * di
}
pub fn surface_radius_m(&self, lat_deg: f64, lon_deg: f64) -> f64 {
R_MOON_M + self.height_at(lat_deg, lon_deg)
}
pub fn parse(text: &str) -> Result<DemTile, String> {
let mut nums = text
.lines()
.filter(|l| !l.trim_start().starts_with('#'))
.flat_map(|l| l.split_whitespace())
.map(|t| {
t.parse::<f64>()
.map_err(|e| format!("bad number '{t}': {e}"))
});
let mut next = || {
nums.next()
.ok_or_else(|| "unexpected end of DEM".to_string())?
};
let lat_min_deg = next()?;
let lat_max_deg = next()?;
let lon_min_deg = next()?;
let lon_max_deg = next()?;
let n_lat = next()? as usize;
let n_lon = next()? as usize;
let mut heights_m = Vec::with_capacity(n_lat * n_lon);
for _ in 0..(n_lat * n_lon) {
heights_m.push(next()?);
}
if n_lat == 0 || n_lon == 0 {
return Err("DEM has a zero dimension".into());
}
Ok(DemTile {
lat_min_deg,
lat_max_deg,
lon_min_deg,
lon_max_deg,
n_lat,
n_lon,
heights_m,
})
}
pub fn to_text(&self) -> String {
let mut s = format!(
"{} {} {} {} {} {}\n",
self.lat_min_deg,
self.lat_max_deg,
self.lon_min_deg,
self.lon_max_deg,
self.n_lat,
self.n_lon
);
for i in 0..self.n_lat {
let row: Vec<String> = (0..self.n_lon)
.map(|j| format!("{}", self.heights_m[i * self.n_lon + j]))
.collect();
s.push_str(&row.join(" "));
s.push('\n');
}
s
}
pub fn representative_south_polar() -> DemTile {
let (n_lat, n_lon) = (21usize, 37usize);
let (lat_min, lat_max) = (-90.0, -80.0);
let (lon_min, lon_max) = (-180.0, 180.0);
let mut heights_m = Vec::with_capacity(n_lat * n_lon);
for i in 0..n_lat {
let lat = lat_min + (lat_max - lat_min) * (i as f64) / ((n_lat - 1) as f64);
for j in 0..n_lon {
let lon = lon_min + (lon_max - lon_min) * (j as f64) / ((n_lon - 1) as f64);
let colat = 90.0 + lat; let rim = 2500.0 * (-((colat - 5.0).powi(2)) / (2.0 * 1.5_f64.powi(2))).exp();
let basin = -1200.0 * (-(colat.powi(2)) / (2.0 * 3.0_f64.powi(2))).exp();
let massif = 1800.0
* (-((lon - 60.0).powi(2)) / (2.0 * 20.0_f64.powi(2))).exp()
* (-((colat - 6.0).powi(2)) / (2.0 * 2.0_f64.powi(2))).exp();
heights_m.push(rim + basin + massif);
}
}
DemTile {
lat_min_deg: lat_min,
lat_max_deg: lat_max,
lon_min_deg: lon_min,
lon_max_deg: lon_max,
n_lat,
n_lon,
heights_m,
}
}
}
pub fn terrain_los_clear(dem: &DemTile, a_mcmf: Vec3, b_mcmf: Vec3, n_steps: usize) -> bool {
let steps = n_steps.max(2);
for k in 1..steps {
let t = k as f64 / steps as f64;
let p = [
a_mcmf[0] + (b_mcmf[0] - a_mcmf[0]) * t,
a_mcmf[1] + (b_mcmf[1] - a_mcmf[1]) * t,
a_mcmf[2] + (b_mcmf[2] - a_mcmf[2]) * t,
];
let sel = mcmf_to_selenographic(p);
let lat_deg = sel.lat_rad.to_degrees();
let lon_deg = sel.lon_rad.to_degrees();
if norm(p) < dem.surface_radius_m(lat_deg, lon_deg) - 1e-6 {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn site(lat_deg: f64, lon_deg: f64, height_m: f64) -> Vec3 {
let lat = lat_deg.to_radians();
let lon = lon_deg.to_radians();
let r = R_MOON_M + height_m;
[
r * lat.cos() * lon.cos(),
r * lat.cos() * lon.sin(),
r * lat.sin(),
]
}
#[test]
fn parse_round_trips() {
let dem = DemTile::representative_south_polar();
let text = dem.to_text();
let back = DemTile::parse(&text).expect("parse");
assert_eq!(back.n_lat, dem.n_lat);
assert_eq!(back.n_lon, dem.n_lon);
assert_eq!(back.heights_m.len(), dem.heights_m.len());
for (a, b) in back.heights_m.iter().zip(&dem.heights_m) {
assert!((a - b).abs() < 1e-6);
}
}
#[test]
fn height_at_matches_grid_nodes_and_interpolates() {
let dem = DemTile {
lat_min_deg: -90.0,
lat_max_deg: -80.0,
lon_min_deg: 0.0,
lon_max_deg: 10.0,
n_lat: 2,
n_lon: 2,
heights_m: vec![0.0, 100.0, 200.0, 300.0],
};
assert!((dem.height_at(-90.0, 0.0) - 0.0).abs() < 1e-9);
assert!((dem.height_at(-90.0, 10.0) - 100.0).abs() < 1e-9);
assert!((dem.height_at(-80.0, 0.0) - 200.0).abs() < 1e-9);
assert!((dem.height_at(-80.0, 10.0) - 300.0).abs() < 1e-9);
assert!((dem.height_at(-85.0, 5.0) - 150.0).abs() < 1e-9);
}
#[test]
fn terrain_blocks_a_line_through_a_ridge_and_passes_a_clear_one() {
let a = site(-85.0, 0.0, 2.0);
let b = site(-85.0, 0.2, 2.0); let wall = DemTile {
lat_min_deg: -86.0,
lat_max_deg: -84.0,
lon_min_deg: -0.1,
lon_max_deg: 0.3,
n_lat: 3,
n_lon: 3,
heights_m: vec![
0.0, 3000.0, 0.0, 0.0, 3000.0, 0.0, 0.0, 3000.0, 0.0,
],
};
assert!(
!terrain_los_clear(&wall, a, b, 64),
"the 3 km wall must occlude the line"
);
let flat = DemTile {
heights_m: vec![0.0; 9],
..wall.clone()
};
assert!(
terrain_los_clear(&flat, a, b, 64),
"a flat tile must not occlude a 6 km near-surface segment"
);
}
#[test]
fn representative_tile_has_a_rim_above_the_basin() {
let dem = DemTile::representative_south_polar();
let rim = dem.height_at(-85.0, 0.0); let basin = dem.height_at(-90.0, 0.0); assert!(
rim > basin + 1000.0,
"rim {rim} should top the basin {basin}"
);
}
}