use crate::octree::Octree;
use crate::point::{BoundingBox3D, Point3D};
#[derive(Debug, Clone)]
pub struct ProfileSegment {
pub distance_along_line: f64,
pub points: Vec<Point3D>,
pub max_z: f64,
pub min_z: f64,
pub mean_z: f64,
pub point_count: usize,
}
impl ProfileSegment {
fn from_points(distance: f64, pts: Vec<Point3D>) -> Self {
if pts.is_empty() {
return Self {
distance_along_line: distance,
points: pts,
max_z: f64::NEG_INFINITY,
min_z: f64::INFINITY,
mean_z: 0.0,
point_count: 0,
};
}
let mut max_z = f64::NEG_INFINITY;
let mut min_z = f64::INFINITY;
let mut sum_z = 0.0_f64;
for p in &pts {
if p.z > max_z {
max_z = p.z;
}
if p.z < min_z {
min_z = p.z;
}
sum_z += p.z;
}
let count = pts.len();
Self {
distance_along_line: distance,
max_z,
min_z,
mean_z: sum_z / count as f64,
point_count: count,
points: pts,
}
}
}
pub struct HeightProfile {
pub segments: Vec<ProfileSegment>,
pub bin_width: f64,
pub height_min: f64,
pub height_max: f64,
}
impl HeightProfile {
pub fn along_line(
octree: &Octree,
x1: f64,
y1: f64,
x2: f64,
y2: f64,
corridor_width: f64,
bin_count: u32,
) -> Self {
let bin_count = bin_count.max(1) as usize;
let dx = x2 - x1;
let dy = y2 - y1;
let total_length = (dx * dx + dy * dy).sqrt();
let (ux, uy) = if total_length > 0.0 {
(dx / total_length, dy / total_length)
} else {
(1.0, 0.0)
};
let (px, py) = (-uy, ux);
let bin_width = if total_length > 0.0 {
total_length / bin_count as f64
} else {
1.0
};
let cw = corridor_width.abs();
let all_corners = [
(x1 + px * cw, y1 + py * cw),
(x1 - px * cw, y1 - py * cw),
(x2 + px * cw, y2 + py * cw),
(x2 - px * cw, y2 - py * cw),
];
let cmin_x = all_corners
.iter()
.map(|c| c.0)
.fold(f64::INFINITY, f64::min);
let cmin_y = all_corners
.iter()
.map(|c| c.1)
.fold(f64::INFINITY, f64::min);
let cmax_x = all_corners
.iter()
.map(|c| c.0)
.fold(f64::NEG_INFINITY, f64::max);
let cmax_y = all_corners
.iter()
.map(|c| c.1)
.fold(f64::NEG_INFINITY, f64::max);
let root_min_z = octree
.stats()
.bounds
.as_ref()
.map(|b| b.min_z)
.unwrap_or(f64::NEG_INFINITY);
let root_max_z = octree
.stats()
.bounds
.as_ref()
.map(|b| b.max_z)
.unwrap_or(f64::INFINITY);
let candidates = if let Some(query_bbox) =
BoundingBox3D::new(cmin_x, cmin_y, root_min_z, cmax_x, cmax_y, root_max_z)
{
octree.query_bbox(&query_bbox)
} else {
Vec::new()
};
let mut bins: Vec<Vec<Point3D>> = (0..bin_count).map(|_| Vec::new()).collect();
for &pt in &candidates {
let rel_x = pt.x - x1;
let rel_y = pt.y - y1;
let along = rel_x * ux + rel_y * uy;
let perp = rel_x * px + rel_y * py;
if perp.abs() > cw {
continue;
}
if total_length > 0.0 && (along < 0.0 || along > total_length) {
continue;
}
let bin_idx = if total_length > 0.0 {
let idx = (along / bin_width).floor() as usize;
idx.min(bin_count - 1)
} else {
0
};
bins[bin_idx].push(pt.clone());
}
let segments: Vec<ProfileSegment> = bins
.into_iter()
.enumerate()
.map(|(i, pts)| {
let dist = (i as f64 + 0.5) * bin_width;
ProfileSegment::from_points(dist, pts)
})
.collect();
let height_min = segments
.iter()
.filter(|s| s.point_count > 0)
.map(|s| s.min_z)
.fold(f64::INFINITY, f64::min);
let height_max = segments
.iter()
.filter(|s| s.point_count > 0)
.map(|s| s.max_z)
.fold(f64::NEG_INFINITY, f64::max);
Self {
segments,
bin_width,
height_min,
height_max,
}
}
#[inline]
pub fn total_length(&self) -> f64 {
self.bin_width * self.segments.len() as f64
}
pub fn highest_point(&self) -> Option<&Point3D> {
self.segments
.iter()
.flat_map(|s| s.points.iter())
.max_by(|a, b| a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
}
}
pub struct GroundFilter {
pub max_slope: f64,
pub cell_size: f64,
pub classification_code: u8,
}
impl Default for GroundFilter {
fn default() -> Self {
Self::new()
}
}
impl GroundFilter {
pub fn new() -> Self {
Self {
max_slope: 0.3,
cell_size: 1.0,
classification_code: 2,
}
}
pub fn by_classification(points: &[Point3D]) -> (Vec<Point3D>, Vec<Point3D>) {
let mut ground = Vec::new();
let mut non_ground = Vec::new();
for p in points {
if p.classification == 2 {
ground.push(p.clone());
} else {
non_ground.push(p.clone());
}
}
(ground, non_ground)
}
pub fn apply(&self, points: &[Point3D]) -> Vec<bool> {
if points.is_empty() {
return Vec::new();
}
if self.cell_size <= 0.0 {
return vec![false; points.len()];
}
let mut cell_min: std::collections::HashMap<(i64, i64), f64> =
std::collections::HashMap::new();
for p in points {
let ix = (p.x / self.cell_size).floor() as i64;
let iy = (p.y / self.cell_size).floor() as i64;
let entry = cell_min.entry((ix, iy)).or_insert(f64::INFINITY);
if p.z < *entry {
*entry = p.z;
}
}
let threshold = self.max_slope * self.cell_size;
points
.iter()
.map(|p| {
let ix = (p.x / self.cell_size).floor() as i64;
let iy = (p.y / self.cell_size).floor() as i64;
let min_z = cell_min.get(&(ix, iy)).copied().unwrap_or(f64::INFINITY);
p.z - min_z <= threshold
})
.collect()
}
}