use geo::{LineString, SimplifyVw};
pub fn simplify_for_zoom(
coords: &[(f64, f64, f64)],
zoom: u8,
simplify_max_zoom: u8,
) -> Vec<(f64, f64, f64)> {
if zoom > simplify_max_zoom || coords.len() < 3 {
return coords.to_vec();
}
let epsilon = calculate_epsilon(zoom);
if epsilon <= 0.0 {
return coords.to_vec();
}
let line: LineString<f64> = coords
.iter()
.map(|(x, y, _)| geo::Coord { x: *x, y: *y })
.collect();
let simplified = line.simplify_vw(&epsilon);
if simplified.0.len() < 2 || simplified.0.len() >= coords.len() {
return coords.to_vec();
}
let mut result = Vec::with_capacity(simplified.0.len());
for coord in simplified.0.iter() {
let alt = interpolate_altitude(coords, coord.x, coord.y);
result.push((coord.x, coord.y, alt));
}
result
}
pub fn simplify_td_tr_for_zoom(
coords: &[(f64, f64, f64)],
times: &[u64],
values: &[f32],
zoom: u8,
simplify_max_zoom: u8,
) -> (Vec<(f64, f64, f64)>, Vec<u64>, Vec<f32>) {
if zoom > simplify_max_zoom
|| coords.len() < 3
|| times.len() != coords.len()
|| values.len() != coords.len()
{
return (coords.to_vec(), times.to_vec(), values.to_vec());
}
let epsilon = calculate_epsilon(zoom);
if epsilon <= 0.0 {
return (coords.to_vec(), times.to_vec(), values.to_vec());
}
simplify_td_tr(coords, times, values, epsilon)
}
fn simplify_td_tr(
coords: &[(f64, f64, f64)],
times: &[u64],
values: &[f32],
epsilon: f64,
) -> (Vec<(f64, f64, f64)>, Vec<u64>, Vec<f32>) {
let n = coords.len();
if n <= 2 || epsilon <= 0.0 || times.len() != n || values.len() != n {
return (coords.to_vec(), times.to_vec(), values.to_vec());
}
let mut keep = vec![false; n];
keep[0] = true;
keep[n - 1] = true;
td_tr_recurse(coords, times, 0, n - 1, epsilon, &mut keep);
let mut sc = Vec::new();
let mut st = Vec::new();
let mut sv = Vec::new();
for i in 0..n {
if keep[i] {
sc.push(coords[i]);
st.push(times[i]);
sv.push(values[i]);
}
}
(sc, st, sv)
}
fn td_tr_recurse(
coords: &[(f64, f64, f64)],
times: &[u64],
start: usize,
end: usize,
epsilon: f64,
keep: &mut [bool],
) {
if end <= start + 1 {
return;
}
let t_start = times[start] as f64;
let t_end = times[end] as f64;
let dt = t_end - t_start;
let (xs, ys, _) = coords[start];
let (xe, ye, _) = coords[end];
let mut max_sed = 0.0f64;
let mut max_idx = start;
for i in (start + 1)..end {
let ratio = if dt > 0.0 {
(times[i] as f64 - t_start) / dt
} else {
0.0
};
let sync_x = xs + ratio * (xe - xs);
let sync_y = ys + ratio * (ye - ys);
let (xi, yi, _) = coords[i];
let sed = ((xi - sync_x).powi(2) + (yi - sync_y).powi(2)).sqrt();
if sed > max_sed {
max_sed = sed;
max_idx = i;
}
}
if max_sed > epsilon {
keep[max_idx] = true;
td_tr_recurse(coords, times, start, max_idx, epsilon, keep);
td_tr_recurse(coords, times, max_idx, end, epsilon, keep);
}
}
fn calculate_epsilon(zoom: u8) -> f64 {
match zoom {
0..=6 => 0.01, 7..=9 => 0.002, 10..=11 => 0.0008, 12..=13 => 0.0003, 14 => 0.0001, _ => 0.0, }
}
fn interpolate_altitude(coords: &[(f64, f64, f64)], lon: f64, lat: f64) -> f64 {
let mut best_alt = 0.0;
let mut best_dist = f64::MAX;
for window in coords.windows(2) {
let (x1, y1, alt1) = window[0];
let (x2, y2, alt2) = window[1];
let dx = x2 - x1;
let dy = y2 - y1;
let len_sq = dx * dx + dy * dy;
let t = if len_sq > 0.0 {
((lon - x1) * dx + (lat - y1) * dy) / len_sq
} else {
0.0
};
let t = t.clamp(0.0, 1.0);
let px = x1 + t * dx;
let py = y1 + t * dy;
let dist = (lon - px).powi(2) + (lat - py).powi(2);
if dist < best_dist {
best_dist = dist;
best_alt = alt1 + t * (alt2 - alt1);
}
}
for (x, y, alt) in coords {
let dist = (lon - x).powi(2) + (lat - y).powi(2);
if dist < best_dist {
best_dist = dist;
best_alt = *alt;
}
}
best_alt
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_simplification_high_zoom() {
let coords = vec![
(-122.4, 37.7, 0.0),
(-122.41, 37.71, 10.0),
(-122.42, 37.72, 20.0),
];
let result = simplify_for_zoom(&coords, 16, 14);
assert_eq!(result.len(), coords.len());
}
#[test]
fn test_simplification_low_zoom() {
let mut coords = Vec::new();
for i in 0..100 {
let t = i as f64 / 100.0;
let noise = (i as f64 * 0.1).sin() * 0.0001;
coords.push((
-122.4 + t * 0.15 + noise,
37.7 + t * 0.1 + noise,
t * 100.0,
));
}
let result = simplify_for_zoom(&coords, 8, 14);
assert!(result.len() < coords.len(), "Should have fewer points");
assert!(result.len() >= 2, "Should have at least 2 points");
}
#[test]
fn test_preserves_altitude() {
let coords = vec![
(0.0, 0.0, 100.0),
(0.5, 0.5, 200.0),
(1.0, 1.0, 300.0),
];
let result = simplify_for_zoom(&coords, 5, 14);
for (_, _, alt) in &result {
assert!(*alt >= 100.0 && *alt <= 300.0, "Altitude should be in range");
}
}
#[test]
fn td_tr_keeps_temporally_displaced_collinear_vertex() {
let coords = vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (2.0, 0.0, 0.0)];
let times = vec![0u64, 9000, 10000];
let values = vec![f32::NAN; 3];
let (sc, st, _) = simplify_td_tr(&coords, ×, &values, 0.1);
assert_eq!(sc.len(), 3, "TD-TR should keep the temporally-displaced midpoint");
assert_eq!(st, times, "kept vertices preserve their real times");
}
#[test]
fn td_tr_drops_collinear_uniform_time_vertex() {
let coords = vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (2.0, 0.0, 0.0)];
let times = vec![0u64, 5000, 10000];
let values = vec![f32::NAN; 3];
let (sc, _, _) = simplify_td_tr(&coords, ×, &values, 0.1);
assert_eq!(sc.len(), 2, "uniform-time collinear midpoint should be dropped");
}
#[test]
fn td_tr_for_zoom_is_noop_above_max_zoom() {
let coords = vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (2.0, 0.0, 0.0)];
let times = vec![0u64, 9000, 10000];
let values = vec![f32::NAN; 3];
let (sc, _, _) = simplify_td_tr_for_zoom(&coords, ×, &values, 16, 14);
assert_eq!(sc.len(), 3);
}
#[test]
fn test_epsilon_decreases_with_zoom() {
let eps10 = calculate_epsilon(10);
let eps12 = calculate_epsilon(12);
let eps14 = calculate_epsilon(14);
assert!(eps10 > eps12, "Lower zoom should have larger epsilon");
assert!(eps12 > eps14, "Lower zoom should have larger epsilon");
}
}