use crate::core::points::{AerialEntry, GridMethod};
use crate::core::surface::Surface;
use crate::foundation::{GeoError, GridGeometry, Result};
use ndarray::Array2;
use rstar::primitives::GeomWithData;
use rstar::RTree;
const IDW_POWER: f64 = 2.0;
pub(crate) fn build_rtree(coords: &[[f64; 3]]) -> RTree<AerialEntry> {
let entries: Vec<AerialEntry> = coords
.iter()
.enumerate()
.map(|(i, c)| GeomWithData::new([c[0], c[1]], i))
.collect();
RTree::bulk_load(entries)
}
pub(crate) fn grid(coords: &[[f64; 3]], geom: GridGeometry, method: GridMethod) -> Result<Surface> {
if coords.is_empty() {
return Err(GeoError::NotFound(
"PointSet::to_surface: no points to grid".into(),
));
}
let values = match method {
GridMethod::Nearest => grid_nearest(coords, &geom),
GridMethod::InverseDistance => grid_idw(coords, &geom),
GridMethod::MinimumCurvature => grid_min_curvature(coords, &geom, None),
};
Surface::new(geom, values)
}
pub(crate) fn grid_min_curvature_warm(
coords: &[[f64; 3]],
geom: GridGeometry,
seed: &Array2<f64>,
) -> Result<Surface> {
if coords.is_empty() {
return Err(GeoError::NotFound(
"PointSet::regrid_min_curvature: no points to grid".into(),
));
}
let values = grid_min_curvature(coords, &geom, Some(seed));
Surface::new(geom, values)
}
fn grid_nearest(coords: &[[f64; 3]], geom: &GridGeometry) -> Array2<f64> {
let tree = build_rtree(coords);
let mut out = Array2::from_elem((geom.ncol, geom.nrow), f64::NAN);
for j in 0..geom.nrow {
for i in 0..geom.ncol {
let (x, y) = geom.node_xy(i, j);
if let Some(e) = tree.nearest_neighbor([x, y]) {
out[[i, j]] = coords[e.data][2];
}
}
}
out
}
fn grid_idw(coords: &[[f64; 3]], geom: &GridGeometry) -> Array2<f64> {
let mut out = Array2::from_elem((geom.ncol, geom.nrow), f64::NAN);
for j in 0..geom.nrow {
for i in 0..geom.ncol {
let (x, y) = geom.node_xy(i, j);
out[[i, j]] = idw_at(coords, x, y);
}
}
out
}
fn idw_at(coords: &[[f64; 3]], x: f64, y: f64) -> f64 {
let mut wsum = 0.0;
let mut vsum = 0.0;
for c in coords {
let d2 = (c[0] - x).powi(2) + (c[1] - y).powi(2);
if d2 == 0.0 {
return c[2]; }
let w = 1.0 / d2.powf(IDW_POWER / 2.0);
wsum += w;
vsum += w * c[2];
}
if wsum > 0.0 {
vsum / wsum
} else {
f64::NAN
}
}
fn grid_min_curvature(
coords: &[[f64; 3]],
geom: &GridGeometry,
seed: Option<&Array2<f64>>,
) -> Array2<f64> {
let (nc, nr) = (geom.ncol, geom.nrow);
let mut z = match seed {
Some(s) if s.dim() == (nc, nr) => s.clone(),
_ => grid_idw(coords, geom),
};
let mut fixed = Array2::from_elem((nc, nr), false);
let mut acc: std::collections::HashMap<(usize, usize), (f64, usize)> =
std::collections::HashMap::new();
for c in coords {
if let Some((fi, fj)) = geom.xy_to_ij(c[0], c[1]) {
let i = fi.round();
let j = fj.round();
if i < 0.0 || j < 0.0 {
continue;
}
let (i, j) = (i as usize, j as usize);
if i < nc && j < nr {
let e = acc.entry((i, j)).or_insert((0.0, 0));
e.0 += c[2];
e.1 += 1;
}
}
}
for ((i, j), (sum, n)) in acc {
z[[i, j]] = sum / n as f64;
fixed[[i, j]] = true;
}
if nc < 2 || nr < 2 {
return z;
}
const MAX_ITERS: usize = 5000;
const TOL: f64 = 1e-6;
const OMEGA: f64 = 1.5;
let range = {
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for c in coords {
lo = lo.min(c[2]);
hi = hi.max(c[2]);
}
(hi - lo).abs().max(1.0)
};
for _ in 0..MAX_ITERS {
let mut max_delta = 0.0_f64;
for j in 0..nr {
for i in 0..nc {
if fixed[[i, j]] {
continue;
}
let target = if i >= 2 && i + 2 < nc && j >= 2 && j + 2 < nr {
biharmonic_target(&z, i, j)
} else {
harmonic_target(&z, i, j, nc, nr)
};
let old = z[[i, j]];
let new = old + OMEGA * (target - old);
z[[i, j]] = new;
max_delta = max_delta.max((new - old).abs());
}
}
if max_delta < TOL * range {
break;
}
}
z
}
fn biharmonic_target(z: &Array2<f64>, i: usize, j: usize) -> f64 {
let n = z[[i, j + 1]];
let s = z[[i, j - 1]];
let e = z[[i + 1, j]];
let w = z[[i - 1, j]];
let ne = z[[i + 1, j + 1]];
let nw = z[[i - 1, j + 1]];
let se = z[[i + 1, j - 1]];
let sw = z[[i - 1, j - 1]];
let nn = z[[i, j + 2]];
let ss = z[[i, j - 2]];
let ee = z[[i + 2, j]];
let ww = z[[i - 2, j]];
(8.0 * (n + s + e + w) - 2.0 * (ne + nw + se + sw) - (nn + ss + ee + ww)) / 20.0
}
fn harmonic_target(z: &Array2<f64>, i: usize, j: usize, nc: usize, nr: usize) -> f64 {
let mut sum = 0.0;
let mut n = 0.0;
if i > 0 {
sum += z[[i - 1, j]];
n += 1.0;
}
if i + 1 < nc {
sum += z[[i + 1, j]];
n += 1.0;
}
if j > 0 {
sum += z[[i, j - 1]];
n += 1.0;
}
if j + 1 < nr {
sum += z[[i, j + 1]];
n += 1.0;
}
if n > 0.0 {
sum / n
} else {
z[[i, j]]
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn geom(ncol: usize, nrow: usize) -> GridGeometry {
GridGeometry {
xori: 0.0,
yori: 0.0,
xinc: 1.0,
yinc: 1.0,
ncol,
nrow,
rotation_deg: 0.0,
yflip: false,
}
}
#[test]
fn nearest_hand_calc() {
let coords = vec![[0.0, 0.0, 1.0], [2.0, 0.0, 2.0], [0.0, 2.0, 3.0]];
let s = grid(&coords, geom(3, 3), GridMethod::Nearest).unwrap();
let v = s.values();
assert_relative_eq!(v[[0, 0]], 1.0); assert_relative_eq!(v[[2, 0]], 2.0); assert_relative_eq!(v[[0, 2]], 3.0); let corner = v[[2, 2]];
assert!(corner == 2.0 || corner == 3.0);
}
#[test]
fn idw_exact_at_samples_and_midpoint() {
let coords = vec![[0.0, 0.0, 0.0], [2.0, 0.0, 10.0]];
let s = grid(&coords, geom(3, 1), GridMethod::InverseDistance).unwrap();
let v = s.values();
assert_relative_eq!(v[[0, 0]], 0.0); assert_relative_eq!(v[[2, 0]], 10.0); assert_relative_eq!(v[[1, 0]], 5.0);
}
#[test]
fn idw_weighting_hand_calc() {
let coords = vec![[0.0, 0.0, 0.0], [4.0, 0.0, 12.0]];
let g = GridGeometry {
xori: 1.0,
yori: 0.0,
xinc: 1.0,
yinc: 1.0,
ncol: 1,
nrow: 1,
rotation_deg: 0.0,
yflip: false,
};
let s = grid(&coords, g, GridMethod::InverseDistance).unwrap();
assert_relative_eq!(s.values()[[0, 0]], 1.2, epsilon = 1e-12);
}
#[test]
fn min_curvature_reproduces_plane() {
let plane = |x: f64, y: f64| 2.0 * x + 3.0 * y + 1.0;
let g = geom(7, 7);
let mut coords = Vec::new();
for j in 0..g.nrow {
for i in 0..g.ncol {
if i == 0 || j == 0 || i == g.ncol - 1 || j == g.nrow - 1 {
let (x, y) = g.node_xy(i, j);
coords.push([x, y, plane(x, y)]);
}
}
}
let s = grid(&coords, g.clone(), GridMethod::MinimumCurvature).unwrap();
let v = s.values();
for j in 1..g.nrow - 1 {
for i in 1..g.ncol - 1 {
let (x, y) = g.node_xy(i, j);
assert_relative_eq!(v[[i, j]], plane(x, y), epsilon = 1e-3);
}
}
}
#[test]
fn empty_points_error() {
assert!(grid(&[], geom(2, 2), GridMethod::Nearest).is_err());
}
}