use crate::traits::{AsCoord, CoordSequence};
const RAD: f64 = std::f64::consts::PI / 180.0;
const R: f64 = 6378137.0;
pub struct SphericalTrajectoryCache {
lng_rad: Vec<f64>,
lat_rad: Vec<f64>,
cos_lat: Vec<f64>,
}
impl SphericalTrajectoryCache {
pub fn from_trajectory<T: CoordSequence>(traj: &T) -> Self
where
T::Coord: AsCoord,
{
let n = traj.len();
let mut lng_rad = Vec::with_capacity(n);
let mut lat_rad = Vec::with_capacity(n);
let mut cos_lat = Vec::with_capacity(n);
for i in 0..n {
let p = traj.get(i);
let lat_r = RAD * p.y();
lng_rad.push(RAD * p.x());
lat_rad.push(lat_r);
cos_lat.push(lat_r.cos());
}
Self {
lng_rad,
lat_rad,
cos_lat,
}
}
pub fn len(&self) -> usize {
self.lat_rad.len()
}
pub fn is_empty(&self) -> bool {
self.lat_rad.is_empty()
}
}
#[inline]
pub fn great_circle_distance_cached(
cache1: &SphericalTrajectoryCache,
i: usize,
cache2: &SphericalTrajectoryCache,
j: usize,
) -> f64 {
let lat1 = cache1.lat_rad[i];
let lon1 = cache1.lng_rad[i];
let lat2 = cache2.lat_rad[j];
let lon2 = cache2.lng_rad[j];
let dlat = lat2 - lat1;
let dlon = lon2 - lon1;
let sin_dlat_half = (dlat * 0.5).sin();
let sin_dlon_half = (dlon * 0.5).sin();
let a = sin_dlat_half * sin_dlat_half
+ cache1.cos_lat[i] * cache2.cos_lat[j] * sin_dlon_half * sin_dlon_half;
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
R * c
}
pub fn great_circle_distance<C: AsCoord, D: AsCoord>(p1: &C, p2: &D) -> f64 {
let lat1: f64 = p1.y();
let lon1 = p1.x();
let lat2: f64 = p2.y();
let lon2 = p2.x();
let dlat = RAD * (lat2 - lat1);
let dlon = RAD * (lon2 - lon1);
let sin_dlat_half = (dlat * 0.5).sin();
let sin_dlon_half = (dlon * 0.5).sin();
let cos_lat1 = (RAD * lat1).cos();
let cos_lat2 = (RAD * lat2).cos();
let a = sin_dlat_half * sin_dlat_half + cos_lat1 * cos_lat2 * sin_dlon_half * sin_dlon_half;
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
R * c
}
fn initial_bearing<C: AsCoord>(p1: &C, p2: &C) -> f64 {
let lat1 = p1.y();
let lon1 = p1.x();
let lat2 = p2.y();
let lon2 = p2.x();
let dlon = RAD * (lon2 - lon1);
let y = dlon.sin() * (RAD * lat2).cos();
let x = (RAD * lat1).cos() * (RAD * lat2).sin()
- (RAD * lat1).sin() * (RAD * lat2).cos() * dlon.cos();
y.atan2(x)
}
fn cross_track_distance<C: AsCoord>(p1: &C, p2: &C, p3: &C, d13: f64) -> f64 {
let theta13 = initial_bearing(p1, p3);
let theta12 = initial_bearing(p1, p2);
((d13 / R).sin() * (theta13 - theta12).sin()).asin() * R
}
fn along_track_distance(crt: f64, d13: f64) -> f64 {
((d13 / R).cos() / (crt / R).cos()).acos() * R
}
pub fn point_to_path<C: AsCoord>(p1: &C, p2: &C, p3: &C, d13: f64, d23: f64, d12: f64) -> f64 {
let crt = cross_track_distance(p1, p2, p3, d13);
let d1p = along_track_distance(crt, d13);
let d2p = along_track_distance(crt, d23);
if d1p > d12 || d2p > d12 {
d13.min(d23)
} else {
crt.abs()
}
}
pub fn point_to_path_simple<C: AsCoord>(p1: &C, p2: &C, p3: &C) -> f64 {
let d13 = great_circle_distance(p1, p3);
let d23 = great_circle_distance(p2, p3);
let d12 = great_circle_distance(p1, p2);
point_to_path(p1, p2, p3, d13, d23, d12)
}
#[inline]
fn initial_bearing_cached(
cache1: &SphericalTrajectoryCache,
i: usize,
cache2: &SphericalTrajectoryCache,
j: usize,
) -> f64 {
let lat1 = cache1.lat_rad[i];
let lon1 = cache1.lng_rad[i];
let lat2 = cache2.lat_rad[j];
let lon2 = cache2.lng_rad[j];
let dlon = lon2 - lon1;
let y = dlon.sin() * cache2.cos_lat[j];
let x = cache1.cos_lat[i] * lat2.sin() - lat1.sin() * cache2.cos_lat[j] * dlon.cos();
y.atan2(x)
}
#[inline]
fn cross_track_distance_cached(
cache1: &SphericalTrajectoryCache,
i: usize,
cache2: &SphericalTrajectoryCache,
j: usize,
cache3: &SphericalTrajectoryCache,
k: usize,
d13: f64,
) -> f64 {
let theta13 = initial_bearing_cached(cache1, i, cache3, k);
let theta12 = initial_bearing_cached(cache1, i, cache2, j);
((d13 / R).sin() * (theta13 - theta12).sin()).asin() * R
}
#[inline]
#[allow(clippy::too_many_arguments)]
pub fn point_to_path_cached(
cache1: &SphericalTrajectoryCache,
i: usize,
cache2: &SphericalTrajectoryCache,
j: usize,
cache3: &SphericalTrajectoryCache,
k: usize,
d_ik: f64,
d_jk: f64,
d_ij: f64,
) -> f64 {
let crt = cross_track_distance_cached(cache1, i, cache2, j, cache3, k, d_ik);
let d1p = along_track_distance(crt, d_ik);
let d2p = along_track_distance(crt, d_jk);
if d1p > d_ij || d2p > d_ij {
d_ik.min(d_jk)
} else {
crt.abs()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_great_circle_distance() {
let p1: [f64; 2] = [-74.006, 40.7128];
let p2: [f64; 2] = [-0.1278, 51.5074];
let dist = great_circle_distance(&p1, &p2);
assert!((dist - 5570000.0).abs() < 10000.0);
}
#[test]
fn test_point_to_path() {
let p1: [f64; 2] = [0.0, 0.0];
let p2: [f64; 2] = [10.0, 0.0];
let p3: [f64; 2] = [5.0, 1.0];
let dist = point_to_path_simple(&p1, &p2, &p3);
assert!((dist - 111195.0).abs() < 1000.0);
}
#[test]
fn test_spherical_cache_basic() {
let traj: Vec<[f64; 2]> = vec![[0.0, 40.0], [1.0, 41.0], [2.0, 42.0]];
let cache = SphericalTrajectoryCache::from_trajectory(&traj);
assert_eq!(cache.len(), 3);
assert!(!cache.is_empty());
}
#[test]
fn test_cached_vs_uncached_distance() {
let traj1: Vec<[f64; 2]> = vec![[-74.006, 40.7128], [-73.9857, 40.7484]];
let traj2: Vec<[f64; 2]> = vec![[-0.1278, 51.5074], [-0.1676, 51.5552]];
let cache1 = SphericalTrajectoryCache::from_trajectory(&traj1);
let cache2 = SphericalTrajectoryCache::from_trajectory(&traj2);
for i in 0..traj1.len() {
for j in 0..traj2.len() {
let dist_uncached = great_circle_distance(&traj1[i], &traj2[j]);
let dist_cached = great_circle_distance_cached(&cache1, i, &cache2, j);
assert!(
(dist_uncached - dist_cached).abs() < 1e-6,
"Mismatch at ({}, {}): uncached={}, cached={}",
i,
j,
dist_uncached,
dist_cached
);
}
}
}
#[test]
fn test_empty_trajectory_cache() {
let traj: Vec<[f64; 2]> = vec![];
let cache = SphericalTrajectoryCache::from_trajectory(&traj);
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
}