use alloc::vec::Vec;
use geometry_cs::{CartesianFamily, CoordinateSystem};
use geometry_model::Linestring;
use geometry_tag::SameAs;
use geometry_trait::{Linestring as LinestringTrait, Point, PointMut};
use crate::cartesian::Pythagoras;
use crate::distance::DistanceStrategy;
pub trait DensifyStrategy<G> {
type Output;
fn densify(&self, g: &G, max_distance: f64) -> Self::Output;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CartesianDensify;
impl<L, P> DensifyStrategy<L> for CartesianDensify
where
L: LinestringTrait<Point = P>,
P: Point<Scalar = f64> + PointMut + Default + Copy,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
Pythagoras: DistanceStrategy<P, P, Out = f64>,
{
type Output = Linestring<P>;
fn densify(&self, ls: &L, max_distance: f64) -> Self::Output {
let pts: Vec<P> = ls.points().copied().collect();
#[allow(
clippy::neg_cmp_op_on_partial_ord,
reason = "NaN must take the guard branch"
)]
if !(max_distance > 0.0) {
return Linestring::from_vec(pts);
}
let mut out: Vec<P> = Vec::with_capacity(pts.len() * 2);
if pts.is_empty() {
return Linestring::from_vec(out);
}
for w in pts.windows(2) {
out.push(w[0]);
let d_total = Pythagoras.distance(&w[0], &w[1]);
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
reason = "d_total / max_distance >= 0 keeps the floor non-negative and small; \
the fraction cast loses no meaningful precision for realistic counts."
)]
{
let n = (d_total / max_distance) as usize;
if n > 0 {
let den = (n + 1) as f64;
for i in 1..=n {
let t = i as f64 / den;
out.push(interpolate(&w[0], &w[1], t));
}
}
}
}
out.push(*pts.last().unwrap());
Linestring::from_vec(out)
}
}
#[inline]
fn interpolate<P>(a: &P, b: &P, t: f64) -> P
where
P: Point<Scalar = f64> + PointMut + Default,
{
let mut out = P::default();
geometry_trait::fold_dims((), a, |(), _p, d| {
let av = match d {
0 => a.get::<0>(),
1 => a.get::<1>(),
2 => a.get::<2>(),
3 => a.get::<3>(),
_ => unreachable!(),
};
let bv = match d {
0 => b.get::<0>(),
1 => b.get::<1>(),
2 => b.get::<2>(),
3 => b.get::<3>(),
_ => unreachable!(),
};
let v = av + t * (bv - av);
match d {
0 => out.set::<0>(v),
1 => out.set::<1>(v),
2 => out.set::<2>(v),
3 => out.set::<3>(v),
_ => unreachable!(),
}
});
out
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
reason = "Densified coordinates are exact literals."
)]
mod tests {
use super::{CartesianDensify, DensifyStrategy};
use crate::cartesian::Pythagoras;
use crate::distance::DistanceStrategy;
use geometry_cs::Cartesian;
use geometry_model::{Linestring, Point2D, linestring};
use geometry_trait::{Linestring as _, Point as _};
type Pt = Point2D<f64, Cartesian>;
#[test]
fn segment_of_length_10_max_2_5_yields_6_points() {
let ls: Linestring<Pt> = linestring![(0., 0.), (10., 0.)];
let out = CartesianDensify.densify(&ls, 2.5);
let xs: alloc::vec::Vec<f64> = out.points().map(Pt::get::<0>).collect();
assert_eq!(xs, alloc::vec![0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
}
#[test]
fn exact_integer_ratio_matches_boost_denominator() {
let ls: Linestring<Pt> = linestring![(0., 0.), (6., 0.)];
let out = CartesianDensify.densify(&ls, 2.0);
let xs: alloc::vec::Vec<f64> = out.points().map(Pt::get::<0>).collect();
assert_eq!(xs, alloc::vec![0.0, 1.5, 3.0, 4.5, 6.0]);
}
#[test]
fn no_output_segment_exceeds_max_distance() {
let ls: Linestring<Pt> = linestring![(0., 0.), (10., 0.), (10., 7.)];
let out = CartesianDensify.densify(&ls, 1.0);
let pts: alloc::vec::Vec<&Pt> = out.points().collect();
for w in pts.windows(2) {
assert!(Pythagoras.distance(w[0], w[1]) <= 1.0 + 1e-9);
}
}
#[test]
fn non_positive_max_distance_copies_through_without_hanging() {
let ls: Linestring<Pt> = linestring![(0., 0.), (10., 0.)];
for bad in [0.0, -1.0, f64::NAN] {
let out = CartesianDensify.densify(&ls, bad);
let xs: alloc::vec::Vec<f64> = out.points().map(Pt::get::<0>).collect();
assert_eq!(xs, alloc::vec![0.0, 10.0], "max_distance = {bad}");
}
}
}