use crate::error::{SpatialError, SpatialResult};
pub type Point2D = [f64; 2];
pub type Trajectory = Vec<Point2D>;
#[inline]
fn point_dist(a: &Point2D, b: &Point2D) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
(dx * dx + dy * dy).sqrt()
}
pub fn dtw_distance(t1: &[Point2D], t2: &[Point2D]) -> SpatialResult<f64> {
if t1.is_empty() || t2.is_empty() {
return Err(SpatialError::ValueError(
"DTW: trajectories must not be empty".to_string(),
));
}
let n = t1.len();
let m = t2.len();
let mut dp = vec![vec![f64::INFINITY; m + 1]; n + 1];
dp[0][0] = 0.0;
for i in 1..=n {
for j in 1..=m {
let cost = point_dist(&t1[i - 1], &t2[j - 1]);
let prev = dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1]);
dp[i][j] = cost + prev;
}
}
Ok(dp[n][m])
}
pub fn dtw_with_window(t1: &[Point2D], t2: &[Point2D], window: usize) -> SpatialResult<f64> {
if t1.is_empty() || t2.is_empty() {
return Err(SpatialError::ValueError(
"DTW: trajectories must not be empty".to_string(),
));
}
let n = t1.len();
let m = t2.len();
let mut dp = vec![vec![f64::INFINITY; m + 1]; n + 1];
dp[0][0] = 0.0;
for i in 1..=n {
let j_lo = if i > window { i - window } else { 1 };
let j_hi = (i + window).min(m);
for j in j_lo..=j_hi {
let cost = point_dist(&t1[i - 1], &t2[j - 1]);
let prev = dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1]);
dp[i][j] = cost + prev;
}
}
if dp[n][m].is_infinite() {
Err(SpatialError::ComputationError(
"DTW: no valid warping path within the given window".to_string(),
))
} else {
Ok(dp[n][m])
}
}
pub fn frechet_distance(t1: &[Point2D], t2: &[Point2D]) -> SpatialResult<f64> {
if t1.is_empty() || t2.is_empty() {
return Err(SpatialError::ValueError(
"Fréchet: trajectories must not be empty".to_string(),
));
}
let n = t1.len();
let m = t2.len();
let mut ca = vec![vec![f64::NEG_INFINITY; m]; n];
for i in 0..n {
for j in 0..m {
let d = point_dist(&t1[i], &t2[j]);
ca[i][j] = if i == 0 && j == 0 {
d
} else if i == 0 {
ca[0][j - 1].max(d)
} else if j == 0 {
ca[i - 1][0].max(d)
} else {
ca[i - 1][j].min(ca[i][j - 1]).min(ca[i - 1][j - 1]).max(d)
};
}
}
Ok(ca[n - 1][m - 1])
}
pub fn continuous_frechet(t1: &[Point2D], t2: &[Point2D], eps: f64) -> SpatialResult<f64> {
if t1.is_empty() || t2.is_empty() {
return Err(SpatialError::ValueError(
"Continuous Fréchet: trajectories must not be empty".to_string(),
));
}
if eps <= 0.0 {
return Err(SpatialError::ValueError(
"Continuous Fréchet: eps must be positive".to_string(),
));
}
let d_discrete = frechet_distance(t1, t2)?;
let d_dtw = dtw_distance(t1, t2)?;
let mut lo = 0.0_f64;
let mut hi = d_dtw.max(d_discrete);
if hi == 0.0 {
return Ok(0.0);
}
let mut iters = 0usize;
while hi - lo > eps && iters < 100 {
let mid = (lo + hi) / 2.0;
if is_reachable(t1, t2, mid) {
hi = mid;
} else {
lo = mid;
}
iters += 1;
}
Ok(hi)
}
fn free_interval_on_segment(
fixed: &Point2D,
seg_start: &Point2D,
seg_end: &Point2D,
delta: f64,
) -> (f64, f64) {
let dx = seg_end[0] - seg_start[0];
let dy = seg_end[1] - seg_start[1];
let fx = seg_start[0] - fixed[0];
let fy = seg_start[1] - fixed[1];
let a = dx * dx + dy * dy;
let b = 2.0 * (fx * dx + fy * dy);
let c = fx * fx + fy * fy - delta * delta;
if a.abs() < 1e-30 {
if c <= 1e-12 {
return (0.0, 1.0);
} else {
return (1.0, 0.0); }
}
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return (1.0, 0.0); }
let sqrt_disc = discriminant.sqrt();
let t0 = (-b - sqrt_disc) / (2.0 * a);
let t1 = (-b + sqrt_disc) / (2.0 * a);
let lo = t0.max(0.0);
let hi = t1.min(1.0);
if lo > hi + 1e-12 {
(1.0, 0.0) } else {
(lo.max(0.0), hi.min(1.0))
}
}
fn is_reachable(t1: &[Point2D], t2: &[Point2D], delta: f64) -> bool {
let n = t1.len();
let m = t2.len();
if n < 2 || m < 2 {
if n >= 1 && m >= 1 {
return point_dist(&t1[0], &t2[0]) <= delta;
}
return false;
}
if point_dist(&t1[0], &t2[0]) > delta || point_dist(&t1[n - 1], &t2[m - 1]) > delta {
return false;
}
let num_seg_t1 = n - 1; let num_seg_t2 = m - 1;
let mut br: Vec<(f64, f64)> = vec![(1.0, 0.0); num_seg_t2];
let mut lr: Vec<(f64, f64)> = vec![(1.0, 0.0); num_seg_t1];
{
let fi = free_interval_on_segment(&t2[0], &t1[0], &t1[1], delta);
if fi.0 <= 1e-12 {
lr[0] = (0.0, fi.1);
}
}
for i in 1..num_seg_t1 {
let prev = lr[i - 1];
if prev.0 <= prev.1 && prev.1 >= 1.0 - 1e-12 {
let fi = free_interval_on_segment(&t2[0], &t1[i], &t1[i + 1], delta);
if fi.0 <= 1e-12 {
lr[i] = (0.0, fi.1);
}
}
}
{
let fi = free_interval_on_segment(&t1[0], &t2[0], &t2[1], delta);
if fi.0 <= 1e-12 {
br[0] = (0.0, fi.1);
}
}
for j in 1..num_seg_t2 {
let prev = br[j - 1];
if prev.0 <= prev.1 && prev.1 >= 1.0 - 1e-12 {
let fi = free_interval_on_segment(&t1[0], &t2[j], &t2[j + 1], delta);
if fi.0 <= 1e-12 {
br[j] = (0.0, fi.1);
}
}
}
for j in 0..num_seg_t2 {
let mut new_lr = vec![(1.0, 0.0); num_seg_t1];
let mut new_br = vec![(1.0, 0.0); num_seg_t2];
for i in 0..num_seg_t1 {
let fi_left = free_interval_on_segment(&t2[j], &t1[i], &t1[i + 1], delta);
let fi_bottom = free_interval_on_segment(&t1[i], &t2[j], &t2[j + 1], delta);
let fi_right = free_interval_on_segment(&t2[j + 1], &t1[i], &t1[i + 1], delta);
let fi_top = free_interval_on_segment(&t1[i + 1], &t2[j], &t2[j + 1], delta);
let reach_left = lr[i];
let reach_bottom = br[j];
let left_enters = is_interval_nonempty(reach_left)
&& is_interval_nonempty(fi_left)
&& intervals_overlap(reach_left, fi_left);
let bottom_enters = is_interval_nonempty(reach_bottom)
&& is_interval_nonempty(fi_bottom)
&& intervals_overlap(reach_bottom, fi_bottom);
let mut reach_right = (1.0_f64, 0.0_f64); let mut reach_top = (1.0_f64, 0.0_f64);
if left_enters || bottom_enters {
if is_interval_nonempty(fi_right) {
reach_right = fi_right;
}
if is_interval_nonempty(fi_top) {
reach_top = fi_top;
}
}
if j + 1 < num_seg_t2 && is_interval_nonempty(reach_right) {
new_lr[i] = interval_union(new_lr[i], reach_right);
}
if is_interval_nonempty(reach_top) {
br[j] = reach_top;
} else if i + 1 < num_seg_t1 {
br[j] = (1.0, 0.0); }
if i == num_seg_t1 - 1 && j == num_seg_t2 - 1 {
if (left_enters || bottom_enters)
&& is_interval_nonempty(fi_right)
&& fi_right.1 >= 1.0 - 1e-12
&& is_interval_nonempty(fi_top)
&& fi_top.1 >= 1.0 - 1e-12
{
return true;
}
}
}
if j + 1 < num_seg_t2 {
lr = new_lr;
}
}
false
}
#[inline]
fn is_interval_nonempty(interval: (f64, f64)) -> bool {
interval.0 <= interval.1 + 1e-12
}
#[inline]
fn intervals_overlap(a: (f64, f64), b: (f64, f64)) -> bool {
a.0 <= b.1 + 1e-12 && b.0 <= a.1 + 1e-12
}
#[inline]
fn interval_union(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
if !is_interval_nonempty(a) {
return b;
}
if !is_interval_nonempty(b) {
return a;
}
(a.0.min(b.0), a.1.max(b.1))
}
pub fn directed_hausdorff_distance(t1: &[Point2D], t2: &[Point2D]) -> SpatialResult<f64> {
if t1.is_empty() || t2.is_empty() {
return Err(SpatialError::ValueError(
"Hausdorff: trajectories must not be empty".to_string(),
));
}
let mut max_min = 0.0_f64;
for p in t1 {
let min_d = t2
.iter()
.map(|q| point_dist(p, q))
.fold(f64::INFINITY, f64::min);
if min_d > max_min {
max_min = min_d;
}
}
Ok(max_min)
}
pub fn hausdorff_distance(t1: &[Point2D], t2: &[Point2D]) -> SpatialResult<f64> {
let h1 = directed_hausdorff_distance(t1, t2)?;
let h2 = directed_hausdorff_distance(t2, t1)?;
Ok(h1.max(h2))
}
pub fn edr_distance(t1: &[Point2D], t2: &[Point2D], epsilon: f64) -> SpatialResult<f64> {
if t1.is_empty() || t2.is_empty() {
return Err(SpatialError::ValueError(
"EDR: trajectories must not be empty".to_string(),
));
}
if epsilon <= 0.0 {
return Err(SpatialError::ValueError(
"EDR: epsilon must be positive".to_string(),
));
}
let n = t1.len();
let m = t2.len();
let mut dp = vec![vec![0usize; m + 1]; n + 1];
for i in 0..=n {
dp[i][0] = i;
}
for j in 0..=m {
dp[0][j] = j;
}
for i in 1..=n {
for j in 1..=m {
let sub_cost = if point_dist(&t1[i - 1], &t2[j - 1]) <= epsilon {
0
} else {
1
};
dp[i][j] = (dp[i - 1][j] + 1)
.min(dp[i][j - 1] + 1)
.min(dp[i - 1][j - 1] + sub_cost);
}
}
let max_len = n.max(m) as f64;
Ok(dp[n][m] as f64 / max_len)
}
pub fn erp_distance(t1: &[Point2D], t2: &[Point2D], gap: &Point2D) -> SpatialResult<f64> {
if t1.is_empty() || t2.is_empty() {
return Err(SpatialError::ValueError(
"ERP: trajectories must not be empty".to_string(),
));
}
let n = t1.len();
let m = t2.len();
let mut dp = vec![vec![0.0_f64; m + 1]; n + 1];
for i in 1..=n {
dp[i][0] = dp[i - 1][0] + point_dist(&t1[i - 1], gap);
}
for j in 1..=m {
dp[0][j] = dp[0][j - 1] + point_dist(&t2[j - 1], gap);
}
for i in 1..=n {
for j in 1..=m {
let d_match = dp[i - 1][j - 1] + point_dist(&t1[i - 1], &t2[j - 1]);
let d_del = dp[i - 1][j] + point_dist(&t1[i - 1], gap);
let d_ins = dp[i][j - 1] + point_dist(&t2[j - 1], gap);
dp[i][j] = d_match.min(d_del).min(d_ins);
}
}
Ok(dp[n][m])
}
#[cfg(test)]
mod tests {
use super::*;
fn make_line(n: usize) -> Trajectory {
(0..n).map(|i| [i as f64, 0.0]).collect()
}
#[test]
fn test_dtw_identical() {
let t = make_line(5);
let d = dtw_distance(&t, &t).expect("dtw identical");
assert!(d.abs() < 1e-10, "DTW of identical trajectories must be 0");
}
#[test]
fn test_dtw_simple() {
let t1: Trajectory = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
let t2: Trajectory = vec![[0.0, 1.0], [1.0, 1.0], [2.0, 1.0]];
let d = dtw_distance(&t1, &t2).expect("dtw simple");
assert!((d - 3.0).abs() < 1e-10);
}
#[test]
fn test_dtw_window_same_as_unconstrained() {
let t1: Trajectory = vec![[0.0, 0.0], [1.0, 0.0]];
let t2: Trajectory = vec![[0.0, 0.0], [1.0, 0.0]];
let d_unc = dtw_distance(&t1, &t2).expect("dtw unc");
let d_win = dtw_with_window(&t1, &t2, 10).expect("dtw win");
assert!((d_unc - d_win).abs() < 1e-10);
}
#[test]
fn test_frechet_identical() {
let t = make_line(4);
let d = frechet_distance(&t, &t).expect("frechet identical");
assert!(d.abs() < 1e-10);
}
#[test]
fn test_frechet_parallel() {
let t1: Trajectory = vec![[0.0, 0.0], [1.0, 0.0]];
let t2: Trajectory = vec![[0.0, 1.0], [1.0, 1.0]];
let d = frechet_distance(&t1, &t2).expect("frechet parallel");
assert!((d - 1.0).abs() < 1e-10);
}
#[test]
fn test_continuous_frechet_parallel() {
let t1: Trajectory = vec![[0.0, 0.0], [1.0, 0.0]];
let t2: Trajectory = vec![[0.0, 1.0], [1.0, 1.0]];
let d = continuous_frechet(&t1, &t2, 1e-4).expect("cont frechet");
assert!((d - 1.0).abs() < 1e-3, "got {d}");
}
#[test]
fn test_hausdorff_identical() {
let t = make_line(4);
let d = hausdorff_distance(&t, &t).expect("hausdorff identical");
assert!(d.abs() < 1e-10);
}
#[test]
fn test_hausdorff_symmetric() {
let t1: Trajectory = vec![[0.0, 0.0], [2.0, 0.0]];
let t2: Trajectory = vec![[1.0, 0.0]];
let h12 = hausdorff_distance(&t1, &t2).expect("h12");
let h21 = hausdorff_distance(&t2, &t1).expect("h21");
assert!((h12 - h21).abs() < 1e-10, "should be symmetric");
}
#[test]
fn test_edr_identical() {
let t = make_line(5);
let d = edr_distance(&t, &t, 0.1).expect("edr identical");
assert!(d.abs() < 1e-10);
}
#[test]
fn test_edr_disjoint() {
let t1: Trajectory = vec![[0.0, 0.0], [1.0, 0.0]];
let t2: Trajectory = vec![[100.0, 0.0], [101.0, 0.0]];
let d = edr_distance(&t1, &t2, 0.5).expect("edr disjoint");
assert!((d - 1.0).abs() < 1e-10, "got {d}");
}
#[test]
fn test_erp_identical() {
let t = make_line(4);
let gap = [0.0, 0.0];
let d = erp_distance(&t, &t, &gap).expect("erp identical");
assert!(d.abs() < 1e-10);
}
#[test]
fn test_erp_gap_penalty() {
let t1: Trajectory = vec![[0.0, 0.0]];
let t2: Trajectory = vec![[3.0, 4.0]]; let gap = [0.0, 0.0];
let d = erp_distance(&t1, &t2, &gap).expect("erp gap");
assert!((d - 5.0).abs() < 1e-10, "got {d}");
}
}