use crate::core::gridding;
use crate::core::surface::Surface;
use crate::foundation::{BBox, GridGeometry, Point3, Result, Stats};
use indexmap::IndexMap;
use rstar::primitives::GeomWithData;
use rstar::RTree;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridMethod {
Nearest,
InverseDistance,
MinimumCurvature,
}
pub(crate) type AerialEntry = GeomWithData<[f64; 2], usize>;
pub struct PointSet {
pub(crate) coords: Vec<[f64; 3]>,
pub(crate) attrs: IndexMap<String, Vec<f64>>,
}
impl PointSet {
pub(crate) fn from_parts(coords: Vec<[f64; 3]>, attrs: IndexMap<String, Vec<f64>>) -> PointSet {
PointSet { coords, attrs }
}
pub fn load_csv(path: impl AsRef<Path>, x: &str, y: &str, z: &str) -> Result<PointSet> {
let (coords, attrs) = crate::io::csv_points::load(path.as_ref(), x, y, z)?;
Ok(PointSet::from_parts(coords, attrs))
}
pub fn load_geojson(path: impl AsRef<Path>) -> Result<PointSet> {
let (coords, attrs) = crate::io::vector::load_point_set_geojson(path.as_ref())?;
Ok(PointSet::from_parts(coords, attrs))
}
pub fn load_irap_points(path: impl AsRef<Path>) -> Result<PointSet> {
let coords = crate::io::xyz::load_points(path.as_ref())?;
Ok(PointSet::from_parts(coords, IndexMap::new()))
}
pub fn len(&self) -> usize {
self.coords.len()
}
pub fn is_empty(&self) -> bool {
self.coords.is_empty()
}
pub fn filter(&self, pred: impl Fn(Point3) -> bool) -> PointSet {
let keep: Vec<usize> = (0..self.coords.len())
.filter(|&i| {
let c = self.coords[i];
pred(Point3::new(c[0], c[1], c[2]))
})
.collect();
let coords = keep.iter().map(|&i| self.coords[i]).collect();
let attrs = self
.attrs
.iter()
.map(|(name, col)| (name.clone(), keep.iter().map(|&i| col[i]).collect()))
.collect();
PointSet::from_parts(coords, attrs)
}
pub fn attr(&self, name: &str) -> Option<&[f64]> {
self.attrs.get(name).map(Vec::as_slice)
}
pub fn stats(&self, attr: &str) -> Option<Stats> {
self.attrs.get(attr).map(|col| Stats::of(col))
}
pub fn bbox(&self) -> BBox {
let mut b = BBox {
xmin: f64::INFINITY,
ymin: f64::INFINITY,
xmax: f64::NEG_INFINITY,
ymax: f64::NEG_INFINITY,
};
for c in &self.coords {
b.xmin = b.xmin.min(c[0]);
b.xmax = b.xmax.max(c[0]);
b.ymin = b.ymin.min(c[1]);
b.ymax = b.ymax.max(c[1]);
}
if self.coords.is_empty() {
b = BBox {
xmin: f64::NAN,
ymin: f64::NAN,
xmax: f64::NAN,
ymax: f64::NAN,
};
}
b
}
pub fn nearest(&self, x: f64, y: f64) -> Option<usize> {
if self.coords.is_empty() {
return None;
}
let tree = self.rtree_xy();
tree.nearest_neighbor([x, y]).map(|e| e.data)
}
pub fn to_surface(&self, geom: GridGeometry, method: GridMethod) -> Result<Surface> {
gridding::grid(&self.coords, geom, method)
}
pub fn regrid_min_curvature(&self, prior: &Surface) -> Result<Surface> {
gridding::grid_min_curvature_warm(&self.coords, prior.geom.clone(), prior.values())
}
pub(crate) fn rtree_xy(&self) -> RTree<AerialEntry> {
gridding::build_rtree(&self.coords)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pts() -> PointSet {
let coords = vec![[0.0, 0.0, 1.0], [10.0, 0.0, 2.0], [0.0, 10.0, 3.0]];
let mut attrs = IndexMap::new();
attrs.insert("poro".to_string(), vec![0.1, 0.2, 0.3]);
PointSet::from_parts(coords, attrs)
}
#[test]
fn len_and_attr_and_stats() {
let p = pts();
assert_eq!(p.len(), 3);
assert!(!p.is_empty());
assert_eq!(p.attr("poro").unwrap(), &[0.1, 0.2, 0.3]);
assert!(p.attr("missing").is_none());
let s = p.stats("poro").unwrap();
assert_eq!(s.count, 3);
approx::assert_relative_eq!(s.mean, 0.2);
assert!(p.stats("nope").is_none());
}
#[test]
fn bbox_covers_points() {
let b = pts().bbox();
approx::assert_relative_eq!(b.xmin, 0.0);
approx::assert_relative_eq!(b.xmax, 10.0);
approx::assert_relative_eq!(b.ymin, 0.0);
approx::assert_relative_eq!(b.ymax, 10.0);
}
#[test]
fn nearest_matches_brute_force() {
let p = pts();
let queries = [(1.0, 1.0), (9.0, 1.0), (1.0, 9.0), (5.0, 5.0)];
for (qx, qy) in queries {
let brute = (0..p.len())
.min_by(|&a, &b| {
let da = (p.coords[a][0] - qx).powi(2) + (p.coords[a][1] - qy).powi(2);
let db = (p.coords[b][0] - qx).powi(2) + (p.coords[b][1] - qy).powi(2);
da.total_cmp(&db)
})
.unwrap();
assert_eq!(p.nearest(qx, qy), Some(brute));
}
}
#[test]
fn filter_keeps_matching_rows_and_attrs() {
let p = pts().filter(|pt| pt.x < 5.0);
assert_eq!(p.len(), 2); assert_eq!(p.attr("poro").unwrap(), &[0.1, 0.3]);
}
#[test]
fn empty_nearest_is_none() {
let p = PointSet::from_parts(Vec::new(), IndexMap::new());
assert!(p.is_empty());
assert!(p.nearest(0.0, 0.0).is_none());
}
fn grid5() -> crate::foundation::GridGeometry {
crate::foundation::GridGeometry {
xori: 0.0,
yori: 0.0,
xinc: 2.5,
yinc: 2.5,
ncol: 5,
nrow: 5,
rotation_deg: 0.0,
yflip: false,
}
}
#[test]
fn warm_start_honours_constraints_and_converges() {
let p = pts();
let cold = p.to_surface(grid5(), GridMethod::MinimumCurvature).unwrap();
let warm = p.regrid_min_curvature(&cold).unwrap();
assert_eq!(warm.geom, cold.geom);
approx::assert_relative_eq!(warm.values()[[0, 0]], 1.0, epsilon = 1e-9);
approx::assert_relative_eq!(warm.values()[[4, 0]], 2.0, epsilon = 1e-9);
approx::assert_relative_eq!(warm.values()[[0, 4]], 3.0, epsilon = 1e-9);
let warm2 = p.regrid_min_curvature(&warm).unwrap();
for (a, b) in warm2.values().iter().zip(warm.values().iter()) {
approx::assert_relative_eq!(a, b, epsilon = 1e-6);
}
}
#[test]
fn regrid_empty_errors() {
let empty = PointSet::from_parts(Vec::new(), IndexMap::new());
let prior = pts()
.to_surface(grid5(), GridMethod::MinimumCurvature)
.unwrap();
assert!(empty.regrid_min_curvature(&prior).is_err());
}
}