use super::rtree::{build_rtree, IndexedPoint};
use super::{FrequentSection, SectionConfig};
use crate::geo_utils::polyline_length;
use crate::GpsPoint;
use log::info;
use rstar::{PointDistance, RTree};
use std::collections::HashMap;
fn detect_fold_point(polyline: &[GpsPoint], threshold: f64) -> Option<usize> {
if polyline.len() < 10 {
return None;
}
let threshold_deg = threshold / 111_000.0;
let threshold_deg_sq = threshold_deg * threshold_deg;
let half = polyline.len() / 2;
let first_half_tree = build_rtree(&polyline[..half]);
let mut fold_candidates: Vec<(usize, f64)> = Vec::new();
for (i, point) in polyline[half..].iter().enumerate() {
let idx = half + i;
let query = [point.latitude, point.longitude];
if let Some(nearest) = first_half_tree.nearest_neighbor(&query) {
let dist_sq = nearest.distance_2(&query);
if dist_sq <= threshold_deg_sq {
fold_candidates.push((idx, dist_sq));
}
}
}
if fold_candidates.len() >= 3 {
Some(fold_candidates[0].0)
} else {
None
}
}
fn compute_fold_ratio(polyline: &[GpsPoint], threshold: f64) -> f64 {
if polyline.len() < 6 {
return 0.0;
}
let threshold_deg = threshold / 111_000.0;
let threshold_deg_sq = threshold_deg * threshold_deg;
let third = polyline.len() / 3;
let first_third = &polyline[..third];
let last_third: Vec<GpsPoint> = polyline[(polyline.len() - third)..].to_vec();
let first_tree = build_rtree(first_third);
let mut close_count = 0;
for point in last_third.iter().rev() {
let query = [point.latitude, point.longitude];
if let Some(nearest) = first_tree.nearest_neighbor(&query) {
if nearest.distance_2(&query) <= threshold_deg_sq {
close_count += 1;
}
}
}
close_count as f64 / third as f64
}
pub fn split_folding_sections(
sections: Vec<FrequentSection>,
config: &SectionConfig,
) -> Vec<FrequentSection> {
let mut result = Vec::new();
for section in sections {
let fold_ratio = compute_fold_ratio(§ion.polyline, config.proximity_threshold);
if fold_ratio > 0.5 {
if let Some(fold_idx) = detect_fold_point(§ion.polyline, config.proximity_threshold)
{
let outbound_polyline = section.polyline[..fold_idx].to_vec();
let outbound_length = polyline_length(&outbound_polyline);
if outbound_length >= config.min_section_length {
let mut outbound = section.clone();
outbound.id = format!("{}_out", section.id);
outbound.polyline = outbound_polyline;
outbound.distance_meters = outbound_length;
outbound.activity_traces = HashMap::new(); result.push(outbound);
}
let return_polyline = section.polyline[fold_idx..].to_vec();
let return_length = polyline_length(&return_polyline);
if return_length >= config.min_section_length {
let mut return_section = section.clone();
return_section.id = format!("{}_ret", section.id);
return_section.polyline = return_polyline;
return_section.distance_meters = return_length;
return_section.activity_traces = HashMap::new();
result.push(return_section);
}
info!(
"[Sections] Split folding section {} at index {} (fold_ratio={:.2})",
section.id, fold_idx, fold_ratio
);
} else {
result.push(section);
}
} else {
result.push(section);
}
}
result
}
pub fn merge_nearby_sections(
mut sections: Vec<FrequentSection>,
config: &SectionConfig,
) -> Vec<FrequentSection> {
if sections.len() < 2 {
return sections;
}
sections.sort_by(|a, b| b.visit_count.cmp(&a.visit_count));
let mut keep: Vec<bool> = vec![true; sections.len()];
let merge_threshold = config.proximity_threshold * 2.0;
for i in 0..sections.len() {
if !keep[i] {
continue;
}
let section_i = §ions[i];
let tree_i = build_rtree(§ion_i.polyline);
for j in (i + 1)..sections.len() {
if !keep[j] {
continue;
}
let section_j = §ions[j];
let length_ratio = section_i.distance_meters / section_j.distance_meters.max(1.0);
if !(0.33..=3.0).contains(&length_ratio) {
continue;
}
let forward_containment =
compute_containment(§ion_j.polyline, &tree_i, merge_threshold);
let reversed_j: Vec<GpsPoint> = section_j.polyline.iter().rev().cloned().collect();
let reverse_containment = compute_containment(&reversed_j, &tree_i, merge_threshold);
let max_containment = forward_containment.max(reverse_containment);
if max_containment > 0.4 {
keep[j] = false;
let direction = if reverse_containment > forward_containment {
"reverse"
} else {
"same"
};
info!(
"[Sections] Merged nearby {} section {} into {} ({:.0}% overlap @ {}m threshold)",
direction, section_j.id, section_i.id, max_containment * 100.0, merge_threshold as i32
);
}
}
}
sections
.into_iter()
.zip(keep)
.filter_map(|(s, k)| if k { Some(s) } else { None })
.collect()
}
pub fn remove_overlapping_sections(
mut sections: Vec<FrequentSection>,
config: &SectionConfig,
) -> Vec<FrequentSection> {
if sections.len() < 2 {
return sections;
}
sections.sort_by(
|a, b| match a.distance_meters.partial_cmp(&b.distance_meters) {
Some(std::cmp::Ordering::Equal) => b.visit_count.cmp(&a.visit_count),
Some(ord) => ord,
None => std::cmp::Ordering::Equal,
},
);
let mut keep: Vec<bool> = vec![true; sections.len()];
for i in 0..sections.len() {
if !keep[i] {
continue;
}
let section_i = §ions[i];
let tree_i = build_rtree(§ion_i.polyline);
for j in (i + 1)..sections.len() {
if !keep[j] {
continue;
}
let section_j = §ions[j];
let tree_j = build_rtree(§ion_j.polyline);
let j_in_i =
compute_containment(§ion_j.polyline, &tree_i, config.proximity_threshold);
let i_in_j =
compute_containment(§ion_i.polyline, &tree_j, config.proximity_threshold);
if j_in_i > 0.6 {
info!(
"[Sections] Removing {} ({}m) - {}% contained in {} ({}m)",
section_j.id,
section_j.distance_meters as u32,
(j_in_i * 100.0) as u32,
section_i.id,
section_i.distance_meters as u32
);
keep[j] = false;
} else if i_in_j > 0.8 {
info!(
"[Sections] Removing {} ({}m) - {}% contained in {} ({}m)",
section_i.id,
section_i.distance_meters as u32,
(i_in_j * 100.0) as u32,
section_j.id,
section_j.distance_meters as u32
);
keep[i] = false;
break; } else if j_in_i > 0.4 && i_in_j > 0.4 {
info!(
"[Sections] Removing {} due to mutual overlap with {} ({}% vs {}%)",
section_j.id,
section_i.id,
(j_in_i * 100.0) as u32,
(i_in_j * 100.0) as u32
);
keep[j] = false;
}
}
}
sections
.into_iter()
.zip(keep)
.filter_map(|(s, k)| if k { Some(s) } else { None })
.collect()
}
fn compute_containment(poly_a: &[GpsPoint], tree_b: &RTree<IndexedPoint>, threshold: f64) -> f64 {
if poly_a.is_empty() {
return 0.0;
}
let threshold_deg = threshold / 111_000.0;
let threshold_deg_sq = threshold_deg * threshold_deg;
let mut contained_points = 0;
for point in poly_a {
let query = [point.latitude, point.longitude];
if let Some(nearest) = tree_b.nearest_neighbor(&query) {
if nearest.distance_2(&query) <= threshold_deg_sq {
contained_points += 1;
}
}
}
contained_points as f64 / poly_a.len() as f64
}
const SPLIT_DENSITY_RATIO: f64 = 2.0;
const MIN_SPLIT_LENGTH: f64 = 100.0;
const MIN_SPLIT_POINTS: usize = 10;
#[derive(Debug)]
struct SplitCandidate {
start_idx: usize,
end_idx: usize,
avg_density: f64,
density_ratio: f64,
}
fn find_split_candidates(section: &FrequentSection) -> Vec<SplitCandidate> {
let density = §ion.point_density;
if density.len() < MIN_SPLIT_POINTS * 2 {
return vec![]; }
let endpoint_window = (density.len() / 10).max(3);
let start_density: f64 = density[..endpoint_window]
.iter()
.map(|&d| d as f64)
.sum::<f64>()
/ endpoint_window as f64;
let end_density: f64 = density[density.len() - endpoint_window..]
.iter()
.map(|&d| d as f64)
.sum::<f64>()
/ endpoint_window as f64;
let endpoint_density = (start_density + end_density) / 2.0;
if endpoint_density < 1.0 {
return vec![]; }
let window_size = (density.len() / 5).max(MIN_SPLIT_POINTS);
let mut candidates = Vec::new();
let mut i = window_size;
while i < density.len() - window_size {
let window_density: f64 = density[i - window_size / 2..i + window_size / 2]
.iter()
.map(|&d| d as f64)
.sum::<f64>()
/ window_size as f64;
let ratio = window_density / endpoint_density;
if ratio >= SPLIT_DENSITY_RATIO {
let mut start_idx = i - window_size / 2;
let mut end_idx = i + window_size / 2;
while start_idx > 0 {
let local_density = density[start_idx - 1] as f64;
if local_density < endpoint_density * 1.5 {
break;
}
start_idx -= 1;
}
while end_idx < density.len() - 1 {
let local_density = density[end_idx + 1] as f64;
if local_density < endpoint_density * 1.5 {
break;
}
end_idx += 1;
}
let portion_distance = if end_idx > start_idx {
polyline_length(§ion.polyline[start_idx..=end_idx])
} else {
0.0
};
if portion_distance >= MIN_SPLIT_LENGTH && end_idx - start_idx >= MIN_SPLIT_POINTS {
let portion_density: f64 = density[start_idx..=end_idx]
.iter()
.map(|&d| d as f64)
.sum::<f64>()
/ (end_idx - start_idx + 1) as f64;
candidates.push(SplitCandidate {
start_idx,
end_idx,
avg_density: portion_density,
density_ratio: portion_density / endpoint_density,
});
i = end_idx + window_size;
} else {
i += 1;
}
} else {
i += 1;
}
}
candidates
}
fn split_section_by_density(
section: FrequentSection,
track_map: &HashMap<String, Vec<GpsPoint>>,
config: &SectionConfig,
) -> Vec<FrequentSection> {
let candidates = find_split_candidates(§ion);
if candidates.is_empty() {
return vec![section];
}
info!(
"[Sections] Found {} split candidates for section {} (len={}m)",
candidates.len(),
section.id,
section.distance_meters as i32
);
let mut result = Vec::new();
for (split_idx, candidate) in candidates.iter().enumerate() {
let split_polyline = section.polyline[candidate.start_idx..=candidate.end_idx].to_vec();
let split_density = section.point_density[candidate.start_idx..=candidate.end_idx].to_vec();
let split_distance = polyline_length(&split_polyline);
let mut split_activity_ids = Vec::new();
let mut split_activity_traces = HashMap::new();
let split_tree = build_rtree(&split_polyline);
let threshold_deg = config.proximity_threshold / 111_000.0;
let threshold_deg_sq = threshold_deg * threshold_deg;
for activity_id in §ion.activity_ids {
if let Some(track) = track_map.get(activity_id) {
let mut overlap_points = Vec::new();
for point in track {
let query = [point.latitude, point.longitude];
if let Some(nearest) = split_tree.nearest_neighbor(&query) {
if nearest.distance_2(&query) <= threshold_deg_sq {
overlap_points.push(*point);
}
}
}
let overlap_distance = polyline_length(&overlap_points);
if overlap_distance >= split_distance * 0.5 {
split_activity_ids.push(activity_id.clone());
if !overlap_points.is_empty() {
split_activity_traces.insert(activity_id.clone(), overlap_points);
}
}
}
}
if split_activity_ids.len() >= config.min_activities as usize {
let split_section = FrequentSection {
id: format!("{}_split{}", section.id, split_idx),
name: None,
sport_type: section.sport_type.clone(),
polyline: split_polyline,
representative_activity_id: section.representative_activity_id.clone(),
activity_ids: split_activity_ids,
activity_portions: Vec::new(), route_ids: section.route_ids.clone(),
visit_count: candidate.avg_density as u32,
distance_meters: split_distance,
activity_traces: split_activity_traces,
confidence: section.confidence,
observation_count: candidate.avg_density as u32,
average_spread: section.average_spread,
point_density: split_density,
scale: section.scale.clone(),
};
info!(
"[Sections] Created split section {} with {} activities (density ratio {:.1}x)",
split_section.id,
split_section.activity_ids.len(),
candidate.density_ratio
);
result.push(split_section);
}
}
result.push(section);
result
}
pub fn split_high_variance_sections(
sections: Vec<FrequentSection>,
track_map: &HashMap<String, Vec<GpsPoint>>,
config: &SectionConfig,
) -> Vec<FrequentSection> {
let mut result = Vec::new();
for section in sections {
let split = split_section_by_density(section, track_map, config);
result.extend(split);
}
result
}