use crate::error::Result;
use crate::vector::delaunay::{DelaunayOptions, delaunay_triangulation};
use oxigdal_core::vector::Point;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TinPoint {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl TinPoint {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
}
#[derive(Debug, Clone)]
pub struct Tin {
pub points: Vec<TinPoint>,
pub triangles: Vec<[usize; 3]>,
}
impl Tin {
pub fn triangle_count(&self) -> usize {
self.triangles.len()
}
pub fn point_count(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.triangles.is_empty()
}
pub fn bounding_box(&self) -> Option<(f64, f64, f64, f64)> {
let mut iter = self.points.iter();
let first = iter.next()?;
let mut min_x = first.x;
let mut min_y = first.y;
let mut max_x = first.x;
let mut max_y = first.y;
for p in iter {
if p.x < min_x {
min_x = p.x;
}
if p.y < min_y {
min_y = p.y;
}
if p.x > max_x {
max_x = p.x;
}
if p.y > max_y {
max_y = p.y;
}
}
Some((min_x, min_y, max_x, max_y))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TinInterpMethod {
Idw {
power: f64,
},
NaturalNeighbor,
}
impl Default for TinInterpMethod {
fn default() -> Self {
Self::NaturalNeighbor
}
}
pub fn build_tin(points: &[TinPoint]) -> Result<Tin> {
if points.len() < 3 {
return Ok(Tin {
points: points.to_vec(),
triangles: Vec::new(),
});
}
let core_points: Vec<Point> = points.iter().map(|p| Point::new(p.x, p.y)).collect();
let options = DelaunayOptions::default();
let triangulation = delaunay_triangulation(&core_points, &options)?;
let triangles: Vec<[usize; 3]> = triangulation
.triangles
.into_iter()
.map(|t| t.vertices)
.collect();
Ok(Tin {
points: points.to_vec(),
triangles,
})
}
const BARY_EPS: f64 = 1e-9;
const COINCIDENT_EPS: f64 = 1e-12;
fn find_triangle(tin: &Tin, qx: f64, qy: f64) -> Option<(usize, [f64; 3])> {
for (ti, tri) in tin.triangles.iter().enumerate() {
let [a, b, c] = *tri;
let pa = tin.points[a];
let pb = tin.points[b];
let pc = tin.points[c];
let det = (pb.y - pc.y) * (pa.x - pc.x) + (pc.x - pb.x) * (pa.y - pc.y);
if det.abs() < COINCIDENT_EPS {
continue;
}
let l1 = ((pb.y - pc.y) * (qx - pc.x) + (pc.x - pb.x) * (qy - pc.y)) / det;
let l2 = ((pc.y - pa.y) * (qx - pc.x) + (pa.x - pc.x) * (qy - pc.y)) / det;
let l3 = 1.0 - l1 - l2;
if l1 >= -BARY_EPS && l2 >= -BARY_EPS && l3 >= -BARY_EPS {
return Some((ti, [l1, l2, l3]));
}
}
None
}
pub fn interpolate_idw_tin(tin: &Tin, qx: f64, qy: f64, power: f64) -> Option<f64> {
let (tri_idx, _bary) = find_triangle(tin, qx, qy)?;
let [a, b, c] = tin.triangles[tri_idx];
let pa = tin.points[a];
let pb = tin.points[b];
let pc = tin.points[c];
let effective_power = if power.is_finite() && power > 0.0 {
power
} else {
1.0
};
let dist = |p: TinPoint| -> f64 {
let dx = p.x - qx;
let dy = p.y - qy;
(dx * dx + dy * dy).sqrt()
};
let da = dist(pa);
let db = dist(pb);
let dc = dist(pc);
if da < COINCIDENT_EPS {
return Some(pa.z);
}
if db < COINCIDENT_EPS {
return Some(pb.z);
}
if dc < COINCIDENT_EPS {
return Some(pc.z);
}
let wa = 1.0 / da.powf(effective_power);
let wb = 1.0 / db.powf(effective_power);
let wc = 1.0 / dc.powf(effective_power);
let total = wa + wb + wc;
if total <= 0.0 || !total.is_finite() {
return None;
}
Some((pa.z * wa + pb.z * wb + pc.z * wc) / total)
}
pub fn interpolate_natural_neighbor(tin: &Tin, qx: f64, qy: f64) -> Option<f64> {
let (tri_idx, bary) = find_triangle(tin, qx, qy)?;
let [a, b, c] = tin.triangles[tri_idx];
let pa = tin.points[a];
let pb = tin.points[b];
let pc = tin.points[c];
Some(pa.z * bary[0] + pb.z * bary[1] + pc.z * bary[2])
}
pub fn rasterize_tin(
tin: &Tin,
min_x: f64,
min_y: f64,
max_x: f64,
max_y: f64,
width: usize,
height: usize,
method: TinInterpMethod,
) -> Vec<f32> {
let mut out = vec![f32::NAN; width * height];
if width == 0 || height == 0 || max_x <= min_x || max_y <= min_y {
return out;
}
let dx = (max_x - min_x) / width as f64;
let dy = (max_y - min_y) / height as f64;
for j in 0..height {
let qy = max_y - (j as f64 + 0.5) * dy;
for i in 0..width {
let qx = min_x + (i as f64 + 0.5) * dx;
let v = match method {
TinInterpMethod::Idw { power } => interpolate_idw_tin(tin, qx, qy, power),
TinInterpMethod::NaturalNeighbor => interpolate_natural_neighbor(tin, qx, qy),
};
if let Some(z) = v {
out[j * width + i] = z as f32;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_triangle() -> Tin {
let pts = vec![
TinPoint::new(0.0, 0.0, 0.0),
TinPoint::new(1.0, 0.0, 10.0),
TinPoint::new(0.5, 1.0, 5.0),
];
build_tin(&pts).expect("build TIN")
}
#[test]
fn unit_build_tin_three_points() {
let tin = sample_triangle();
assert_eq!(tin.triangle_count(), 1);
assert_eq!(tin.point_count(), 3);
}
#[test]
fn unit_build_tin_too_few() {
let tin = build_tin(&[]).expect("empty ok");
assert!(tin.is_empty());
let tin = build_tin(&[TinPoint::new(0.0, 0.0, 1.0)]).expect("single ok");
assert!(tin.is_empty());
let tin = build_tin(&[TinPoint::new(0.0, 0.0, 1.0), TinPoint::new(1.0, 0.0, 2.0)])
.expect("pair ok");
assert!(tin.is_empty());
}
#[test]
fn unit_idw_at_vertex() {
let tin = sample_triangle();
let z = interpolate_idw_tin(&tin, 0.0, 0.0, 2.0).expect("inside hull");
assert!((z - 0.0).abs() < 1e-9);
}
#[test]
fn unit_natural_neighbor_at_vertex() {
let tin = sample_triangle();
let z = interpolate_natural_neighbor(&tin, 1.0, 0.0).expect("inside hull");
assert!((z - 10.0).abs() < 1e-9);
}
#[test]
fn unit_outside_hull_returns_none() {
let tin = sample_triangle();
assert!(interpolate_idw_tin(&tin, 10.0, 10.0, 2.0).is_none());
assert!(interpolate_natural_neighbor(&tin, 10.0, 10.0).is_none());
}
#[test]
fn unit_bounding_box() {
let tin = sample_triangle();
let bbox = tin.bounding_box().expect("non-empty");
assert!((bbox.0 - 0.0).abs() < 1e-12);
assert!((bbox.1 - 0.0).abs() < 1e-12);
assert!((bbox.2 - 1.0).abs() < 1e-12);
assert!((bbox.3 - 1.0).abs() < 1e-12);
}
#[test]
fn unit_rasterize_idw_dims() {
let tin = sample_triangle();
let grid = rasterize_tin(
&tin,
0.0,
0.0,
1.0,
1.0,
8,
6,
TinInterpMethod::Idw { power: 2.0 },
);
assert_eq!(grid.len(), 8 * 6);
}
#[test]
fn unit_rasterize_natural_neighbor_dims() {
let tin = sample_triangle();
let grid = rasterize_tin(
&tin,
0.0,
0.0,
1.0,
1.0,
4,
4,
TinInterpMethod::NaturalNeighbor,
);
assert_eq!(grid.len(), 16);
}
#[test]
fn unit_rasterize_degenerate_returns_all_nan() {
let tin = sample_triangle();
let grid = rasterize_tin(
&tin,
1.0,
1.0,
0.0,
0.0,
4,
4,
TinInterpMethod::NaturalNeighbor,
);
assert_eq!(grid.len(), 16);
assert!(grid.iter().all(|v| v.is_nan()));
}
}