use ndarray::Array2;
use crate::srtm::TileSource;
use crate::Bbox;
pub fn sample(
source: &dyn TileSource,
bbox: &Bbox,
num_lines: usize,
elevation_pts: usize,
) -> Array2<f64> {
let (lat0, lat1) = bbox.lats();
let (lon0, lon1) = bbox.longs();
let mut values = Array2::<f64>::from_elem((num_lines, elevation_pts), f64::NAN);
for r in 0..num_lines {
let lat = lat0 + r as f64 / num_lines as f64 * (lat1 - lat0);
let lat_tile = lat.floor() as i32;
for c in 0..elevation_pts {
let lon = lon0 + c as f64 / elevation_pts as f64 * (lon1 - lon0);
let lon_tile = lon.floor() as i32;
let tile = match source.tile(lat_tile, lon_tile) {
Some(t) => t,
None => continue, };
values[(r, c)] = tile.elevation(lat, lon);
}
}
values
}
pub fn sample_disc(
source: &dyn TileSource,
center_lat: f64,
center_lon: f64,
span_deg: f64,
n: usize,
) -> Array2<f64> {
let mut values = Array2::<f64>::from_elem((n, n), f64::NAN);
let half = span_deg / 2.0;
let radius_sq = half * half;
for r in 0..n {
let lat = center_lat - half + r as f64 / n as f64 * span_deg;
let dlat = lat - center_lat;
for c in 0..n {
let lon = center_lon - half + c as f64 / n as f64 * span_deg;
let dlon = lon - center_lon;
if dlat * dlat + dlon * dlon > radius_sq {
continue; }
let lat_tile = lat.floor() as i32;
let lon_tile = lon.floor() as i32;
if let Some(tile) = source.tile(lat_tile, lon_tile) {
values[(r, c)] = tile.elevation(lat, lon);
}
}
}
values
}
#[allow(clippy::too_many_arguments)]
pub fn sample_window(
data: &Array2<f64>,
d_lat0: f64,
d_lon0: f64,
d_span: f64,
lat0: f64,
lon0: f64,
lat1: f64,
lon1: f64,
num_lines: usize,
elevation_pts: usize,
) -> Array2<f64> {
let mut out = Array2::from_elem((num_lines, elevation_pts), f64::NAN);
let step = d_span / data.nrows() as f64; for i in 0..num_lines {
let lat = lat0 + i as f64 / num_lines as f64 * (lat1 - lat0);
for j in 0..elevation_pts {
let lon = lon0 + j as f64 / elevation_pts as f64 * (lon1 - lon0);
let r = ((lat - d_lat0) / step).round();
let c = ((lon - d_lon0) / step).round();
if r >= 0.0 && c >= 0.0 {
let (ru, cu) = (r as usize, c as usize);
if ru < data.nrows() && cu < data.ncols() {
out[(i, j)] = data[(ru, cu)];
}
}
}
}
out
}
pub fn swap_for_angle(viewpoint_angle: f64) -> bool {
let a = viewpoint_angle.rem_euclid(360.0);
(45.0 < a && a < 135.0) || (225.0 < a && a < 315.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::srtm::SyntheticSource;
#[test]
fn sample_shape_and_values() {
let src = SyntheticSource { side: 1201 };
let bbox = Bbox::new(-72.0, 43.0, -71.0, 44.0);
let grid = sample(&src, &bbox, 4, 5);
assert_eq!(grid.dim(), (4, 5));
let v00 = grid[(0, 0)];
let expected = SyntheticSource::value(43.0, -72.0).round();
assert!((v00 - expected).abs() < 1e-9);
let lat_last = 43.0 + 3.0 / 4.0 * 1.0;
let lon_last = -72.0 + 4.0 / 5.0 * 1.0;
let tile = src.tile(43, -72).unwrap();
assert_eq!(grid[(3, 4)], tile.elevation(lat_last, lon_last));
let lat = 43.0 + 1.0 / 4.0;
let lon = -72.0 + 2.0 / 5.0;
let expected = SyntheticSource::value(lat, lon).round();
assert!((grid[(1, 2)] - expected).abs() < 1e-9);
}
struct OneTileSource;
impl crate::srtm::TileSource for OneTileSource {
fn tile(&self, lat_lo: i32, lon_lo: i32) -> Option<std::sync::Arc<crate::srtm::Tile>> {
if lat_lo == 43 && lon_lo == -72 {
SyntheticSource { side: 1201 }.tile(lat_lo, lon_lo)
} else {
None
}
}
}
#[test]
fn ocean_is_nan() {
let src = OneTileSource;
let bbox = Bbox::new(-71.5, 43.5, -70.5, 44.5);
let grid = sample(&src, &bbox, 5, 5);
assert!(grid.iter().any(|v| v.is_nan()), "missing tile -> NaN");
assert!(grid.iter().any(|v| v.is_finite()), "present tile -> values");
}
#[test]
fn disc_mask_is_rotation_invariant() {
let src = SyntheticSource { side: 1201 };
let grid = sample_disc(&src, 44.0, -72.0, 1.0, 40);
let finite = grid.iter().filter(|v| v.is_finite()).count();
let expected = (std::f64::consts::FRAC_PI_4 * 1600.0) as usize;
assert!(
(finite as i64 - expected as i64).abs() < 40,
"finite {finite} vs {expected}"
);
for r in 0..40 {
for c in 0..40 {
let dlat = (44.0 - 0.5 + r as f64 / 40.0) - 44.0;
let dlon = (-72.0 - 0.5 + c as f64 / 40.0) - -72.0;
if dlat * dlat + dlon * dlon > 0.25 {
assert!(grid[(r, c)].is_nan(), "({r},{c}) outside disc must be NaN");
}
}
}
}
#[test]
fn anisotropic_window_samples_correct_positions() {
let src = SyntheticSource { side: 8 }; let _ = src;
let mut data = Array2::from_elem((8, 8), f64::NAN);
for r in 0..8 {
for c in 0..8 {
data[(r, c)] = (r * 100 + c) as f64;
}
}
let (d_lat0, d_lon0, d_span) = (43.5f64, -72.5f64, 1.0f64);
let out = sample_window(
&data, d_lat0, d_lon0, d_span, 43.5, -72.5, 44.3, -71.7, 4, 8,
);
assert_eq!(out.dim(), (4, 8));
for (j, expected_col) in [
(0usize, 0usize),
(1, 1),
(2, 2),
(3, 2),
(4, 3),
(5, 4),
(6, 5),
(7, 6),
] {
assert_eq!(out[(0, j)], (expected_col) as f64, "row 0 col {j}");
}
assert_eq!(out[(2, 0)], 300.0); assert_eq!(out[(3, 0)], 500.0); }
#[test]
fn angle_bands() {
assert!(!swap_for_angle(0.0));
assert!(!swap_for_angle(45.0));
assert!(swap_for_angle(90.0));
assert!(!swap_for_angle(135.0));
assert!(swap_for_angle(-90.0)); assert!(swap_for_angle(280.0));
}
}
#[cfg(test)]
mod parity_tests {
use super::*;
use crate::srtm::DirSource;
use std::path::Path;
#[test]
fn white_mountains_matches_upstream_fixture() {
let bin = Path::new("../../fixtures/new_hampshire.f64.bin");
let srtm_dir = Path::new("../../fixtures/srtm");
if !bin.exists() || !srtm_dir.exists() {
assert!(
std::env::var_os("RIDGE_REQUIRE_FIXTURES").is_none(),
"RIDGE_REQUIRE_FIXTURES is set but the fixtures are missing; run scripts/fetch_fixtures.sh"
);
eprintln!("skipping: fixtures not fetched (scripts/fetch_fixtures.sh)");
return;
}
let bytes = std::fs::read(bin).unwrap();
let expected: Vec<f64> = bytes
.as_chunks::<8>()
.0
.iter()
.map(|&b| f64::from_le_bytes(b))
.collect();
let src = DirSource::new(srtm_dir);
let grid = sample(&src, &crate::DEFAULT_BBOX, 80, 300);
assert_eq!(grid.len(), expected.len());
let mut mismatches = 0usize;
let mut worst = 0.0f64;
for (got, want) in grid.iter().zip(expected.iter()) {
match (got.is_nan(), want.is_nan()) {
(true, true) => {}
(false, false) => {
let d = (got - want).abs();
if d > 1e-9 {
mismatches += 1;
worst = worst.max(d);
}
}
_ => {
mismatches += 1;
}
}
}
let frac = mismatches as f64 / (expected.len() as f64);
println!(
"parity: {mismatches}/{} mismatches (worst delta {worst:.2e})",
expected.len()
);
assert!(frac < 0.01, "{mismatches} mismatches (worst delta {worst})");
}
}