use crate::models::GnssPosition;
pub fn calculate_mean_spacing(gnss_positions: &[GnssPosition]) -> f64 {
if gnss_positions.len() < 2 {
return 0.0;
}
let mut total_distance = 0.0;
let mut count = 0;
for i in 0..gnss_positions.len() - 1 {
let curr = &gnss_positions[i];
let next = &gnss_positions[i + 1];
let spacing = if let (Some(curr_dist), Some(next_dist)) = (curr.distance, next.distance) {
(next_dist - curr_dist).abs()
} else {
use geo::{HaversineDistance, Point};
let p1 = Point::new(curr.longitude, curr.latitude);
let p2 = Point::new(next.longitude, next.latitude);
p1.haversine_distance(&p2)
};
total_distance += spacing;
count += 1;
}
if count > 0 {
total_distance / count as f64
} else {
0.0
}
}
pub fn select_resampled_subset(
gnss_positions: &[GnssPosition],
resampling_distance: f64,
) -> Vec<usize> {
if gnss_positions.len() < 3 || resampling_distance <= 0.0 {
return (0..gnss_positions.len()).collect();
}
let mean_spacing = calculate_mean_spacing(gnss_positions);
if mean_spacing <= 0.0 {
return (0..gnss_positions.len()).collect();
}
let step_size = (resampling_distance / mean_spacing).ceil() as usize;
if step_size < 2 {
return (0..gnss_positions.len()).collect();
}
let mut indices: Vec<usize> = (0..gnss_positions.len()).step_by(step_size).collect();
let last_idx = gnss_positions.len() - 1;
if indices.last() != Some(&last_idx) && last_idx > 0 {
indices.push(last_idx);
}
indices
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_mean_spacing_with_distance_column() {
let positions = vec![
GnssPosition::with_heading_distance(
50.8503,
4.3502,
Utc::now().into(),
"EPSG:4326".to_string(),
None,
Some(0.0), )
.unwrap(),
GnssPosition::with_heading_distance(
50.8513,
4.3512,
Utc::now().into(),
"EPSG:4326".to_string(),
None,
Some(10.0), )
.unwrap(),
GnssPosition::with_heading_distance(
50.8523,
4.3522,
Utc::now().into(),
"EPSG:4326".to_string(),
None,
Some(23.0), )
.unwrap(),
];
let mean = calculate_mean_spacing(&positions);
assert!(
(mean - 11.5).abs() < 0.001,
"Expected mean 11.5, got {}",
mean
);
}
#[test]
fn test_mean_spacing_without_distance_column() {
let positions = vec![
GnssPosition::new(50.8503, 4.3502, Utc::now().into(), "EPSG:4326".to_string()).unwrap(),
GnssPosition::new(50.8513, 4.3512, Utc::now().into(), "EPSG:4326".to_string()).unwrap(),
GnssPosition::new(50.8523, 4.3522, Utc::now().into(), "EPSG:4326".to_string()).unwrap(),
];
let mean = calculate_mean_spacing(&positions);
assert!(mean > 0.0, "Mean spacing should be positive");
assert!(mean < 5000.0, "Mean spacing should be reasonable (< 5km)");
}
#[test]
fn test_mean_spacing_single_position() {
let positions =
vec![
GnssPosition::new(50.8503, 4.3502, Utc::now().into(), "EPSG:4326".to_string())
.unwrap(),
];
let mean = calculate_mean_spacing(&positions);
assert_eq!(mean, 0.0, "Single position should return 0.0");
}
#[test]
fn test_mean_spacing_empty() {
let positions: Vec<GnssPosition> = vec![];
let mean = calculate_mean_spacing(&positions);
assert_eq!(mean, 0.0, "Empty positions should return 0.0");
}
#[test]
fn test_mean_spacing_mixed_distance_values() {
let positions = vec![
GnssPosition::with_heading_distance(
50.8503,
4.3502,
Utc::now().into(),
"EPSG:4326".to_string(),
None,
Some(10.0),
)
.unwrap(),
GnssPosition::new(50.8513, 4.3512, Utc::now().into(), "EPSG:4326".to_string()).unwrap(),
GnssPosition::with_heading_distance(
50.8523,
4.3522,
Utc::now().into(),
"EPSG:4326".to_string(),
None,
Some(11.0),
)
.unwrap(),
];
let mean = calculate_mean_spacing(&positions);
assert!(
mean > 0.0,
"Mean spacing should be positive with mixed data"
);
}
#[test]
fn test_select_resampled_subset_basic() {
let positions: Vec<GnssPosition> = (0..100)
.map(|i| {
let mut pos = GnssPosition::new(
50.85 + i as f64 * 0.00001,
4.35,
Utc::now().into(),
"EPSG:4326".to_string(),
)
.unwrap();
pos.distance = Some(i as f64); pos
})
.collect();
let indices = select_resampled_subset(&positions, 10.0);
assert!(
indices.len() >= 10 && indices.len() <= 12,
"Should select ~10 positions, got {}",
indices.len()
);
assert_eq!(indices[0], 0, "First position should be included");
assert_eq!(
indices[indices.len() - 1],
99,
"Last position should be included"
);
}
#[test]
fn test_select_resampled_subset_no_resampling_needed() {
let positions: Vec<GnssPosition> = (0..10)
.map(|i| {
let mut pos = GnssPosition::new(
50.85 + i as f64 * 0.0001,
4.35,
Utc::now().into(),
"EPSG:4326".to_string(),
)
.unwrap();
pos.distance = Some(i as f64 * 10.0); pos
})
.collect();
let indices = select_resampled_subset(&positions, 10.0);
assert_eq!(
indices.len(),
10,
"Should return all positions when resampling not beneficial"
);
}
#[test]
fn test_select_resampled_subset_too_few_positions() {
let positions = vec![
GnssPosition::new(50.8503, 4.3502, Utc::now().into(), "EPSG:4326".to_string()).unwrap(),
GnssPosition::new(50.8513, 4.3512, Utc::now().into(), "EPSG:4326".to_string()).unwrap(),
];
let indices = select_resampled_subset(&positions, 10.0);
assert_eq!(indices.len(), 2, "Should return all positions when too few");
assert_eq!(indices, vec![0, 1]);
}
#[test]
fn test_select_resampled_subset_invalid_distance() {
let positions: Vec<GnssPosition> = (0..10)
.map(|i| {
GnssPosition::new(
50.85 + i as f64 * 0.00001,
4.35,
Utc::now().into(),
"EPSG:4326".to_string(),
)
.unwrap()
})
.collect();
let indices = select_resampled_subset(&positions, 0.0);
assert_eq!(
indices.len(),
10,
"Should return all positions for invalid distance"
);
let indices = select_resampled_subset(&positions, -5.0);
assert_eq!(
indices.len(),
10,
"Should return all positions for negative distance"
);
}
#[test]
fn test_select_resampled_subset_ensures_last_position() {
let positions: Vec<GnssPosition> = (0..99)
.map(|i| {
let mut pos = GnssPosition::new(
50.85 + i as f64 * 0.00001,
4.35,
Utc::now().into(),
"EPSG:4326".to_string(),
)
.unwrap();
pos.distance = Some(i as f64); pos
})
.collect();
let indices = select_resampled_subset(&positions, 10.0);
assert_eq!(
indices[indices.len() - 1],
98,
"Last position should always be included"
);
}
}