pelorus 0.1.0

A high-performance GPS map-matching library for geospatial road network processing
Documentation
use crate::matching::{MapMatchingConfig, MapMatchingError, TraceBreak};
use crate::types::GpsMeasurement;
use geo::algorithm::euclidean_distance::EuclideanDistance;

/// Handles detection and management of breaks in GPS traces
pub struct BreakHandler {
    config: MapMatchingConfig,
}

impl BreakHandler {
    /// Create a new break handler
    pub fn new(config: &MapMatchingConfig) -> Self {
        Self {
            config: config.clone(),
        }
    }

    /// Detect breaks in a GPS trace based on time and distance gaps
    /// Also handles break healing and 180-second splitting
    pub fn detect_breaks(
        &self,
        gps_trace: &[GpsMeasurement],
    ) -> Result<Vec<TraceBreak>, MapMatchingError> {
        if gps_trace.len() < 2 {
            return Ok(Vec::new());
        }

        // First pass: detect potential breaks
        let mut potential_breaks = Vec::new();
        for i in 1..gps_trace.len() {
            let previous = &gps_trace[i - 1];
            let current = &gps_trace[i];

            if let Some(break_info) =
                self.check_for_break(previous, current, i - 1, i)?
            {
                potential_breaks.push(break_info);
            }
        }

        // Try to heal breaks by removing problematic points
        let healed_breaks = self.try_heal_breaks(gps_trace, potential_breaks)?;

        // Apply 180-second rule: split traces if gap > 180 seconds
        let final_breaks = self.apply_time_splitting_rule(&healed_breaks)?;

        // Merge adjacent breaks if they overlap
        let merged_breaks = self.merge_adjacent_breaks(final_breaks);

        Ok(merged_breaks)
    }

    /// Try to heal breaks by identifying and marking points for removal
    fn try_heal_breaks(
        &self,
        _gps_trace: &[GpsMeasurement],
        breaks: Vec<TraceBreak>,
    ) -> Result<Vec<TraceBreak>, MapMatchingError> {
        let mut healable_breaks = Vec::new();

        for break_info in breaks {
            // For short breaks (< 3 points involved), try to heal
            let affected_points = break_info.end_index - break_info.start_index + 1;

            if affected_points <= 3 && break_info.time_gap <= 60.0 {
                // Small break, might be healable by removing middle point(s)
                // For now, we'll mark it as a break that could be healed in preprocessing
                // In a full implementation, we'd analyze if removing the middle point
                // creates a valid path

                // Check if this is likely a GPS glitch (very short time, large distance)
                if break_info.time_gap < 10.0 && break_info.distance_gap > 100.0 {
                    // Likely GPS glitch - don't treat as break
                    continue;
                }
            }

            // Keep this as a real break
            healable_breaks.push(break_info);
        }

        Ok(healable_breaks)
    }

    /// Apply 180-second rule to split traces at long time gaps
    fn apply_time_splitting_rule(
        &self,
        breaks: &[TraceBreak],
    ) -> Result<Vec<TraceBreak>, MapMatchingError> {
        let mut final_breaks = Vec::new();

        for break_info in breaks {
            if break_info.time_gap > 180.0 {
                // Force split at 180+ second gaps regardless of distance
                final_breaks.push(break_info.clone());
            } else if break_info.distance_gap > self.config.break_distance_threshold {
                // Keep distance-based breaks
                final_breaks.push(break_info.clone());
            }
            // Skip breaks that are only based on shorter time gaps
        }

        Ok(final_breaks)
    }

    /// Check if there's a break between two consecutive GPS measurements
    fn check_for_break(
        &self,
        previous: &GpsMeasurement,
        current: &GpsMeasurement,
        prev_index: usize,
        curr_index: usize,
    ) -> Result<Option<TraceBreak>, MapMatchingError> {
        let time_gap = current.timestamp - previous.timestamp;
        let distance = previous.point.euclidean_distance(&current.point);

        // TODO: Convert from degrees to meters for proper distance calculation
        let distance_gap = distance * 111_000.0; // Rough conversion: 1 degree ≈ 111km

        // Check if either time or distance threshold is exceeded
        let time_break = time_gap > self.config.break_time_threshold;
        let distance_break = distance_gap > self.config.break_distance_threshold;

        if time_break || distance_break {
            return Ok(Some(TraceBreak {
                start_index: prev_index,
                end_index: curr_index,
                time_gap,
                distance_gap,
            }));
        }

        // Check for speed-based breaks (unrealistic jumps)
        if time_gap > 0.0 {
            let speed_ms = distance_gap / time_gap;
            // If speed exceeds 200 km/h (55.56 m/s), consider it a break
            if speed_ms > 55.56 {
                return Ok(Some(TraceBreak {
                    start_index: prev_index,
                    end_index: curr_index,
                    time_gap,
                    distance_gap,
                }));
            }
        }

        Ok(None)
    }

    /// Merge adjacent or overlapping breaks
    fn merge_adjacent_breaks(&self, breaks: Vec<TraceBreak>) -> Vec<TraceBreak> {
        if breaks.len() <= 1 {
            return breaks;
        }

        let mut merged = Vec::new();
        let mut current_break = breaks[0].clone();

        for i in 1..breaks.len() {
            let next_break = &breaks[i];

            // If breaks are adjacent or overlapping, merge them
            if current_break.end_index >= next_break.start_index {
                current_break = TraceBreak {
                    start_index: current_break.start_index,
                    end_index: next_break.end_index,
                    time_gap: current_break.time_gap + next_break.time_gap,
                    distance_gap: current_break
                        .distance_gap
                        .max(next_break.distance_gap),
                };
            } else {
                // No overlap, save current break and start new one
                merged.push(current_break);
                current_break = next_break.clone();
            }
        }

        // Don't forget the last break
        merged.push(current_break);

        merged
    }

    /// Analyze break patterns in a GPS trace
    pub fn analyze_break_patterns(&self, breaks: &[TraceBreak]) -> BreakAnalysis {
        if breaks.is_empty() {
            return BreakAnalysis::default();
        }

        let total_breaks = breaks.len();
        let total_time_gap: f64 = breaks.iter().map(|b| b.time_gap).sum();
        let total_distance_gap: f64 = breaks.iter().map(|b| b.distance_gap).sum();

        let average_time_gap = total_time_gap / total_breaks as f64;
        let average_distance_gap = total_distance_gap / total_breaks as f64;

        let max_time_gap = breaks.iter().map(|b| b.time_gap).fold(0.0, f64::max);

        let max_distance_gap =
            breaks.iter().map(|b| b.distance_gap).fold(0.0, f64::max);

        let time_based_breaks = breaks
            .iter()
            .filter(|b| b.time_gap > self.config.break_time_threshold)
            .count();

        let distance_based_breaks = breaks
            .iter()
            .filter(|b| b.distance_gap > self.config.break_distance_threshold)
            .count();

        BreakAnalysis {
            total_breaks,
            time_based_breaks,
            distance_based_breaks,
            total_time_gap,
            total_distance_gap,
            average_time_gap,
            average_distance_gap,
            max_time_gap,
            max_distance_gap,
        }
    }

    /// Determine if a trace should be split based on break analysis
    pub fn should_split_trace(&self, analysis: &BreakAnalysis) -> bool {
        // Split if we have any significant breaks
        if analysis.total_breaks > 0 {
            // Split if any break exceeds the configured thresholds significantly
            if analysis.max_time_gap > self.config.break_time_threshold * 2.0 {
                return true;
            }
            if analysis.max_distance_gap > self.config.break_distance_threshold * 2.0 {
                return true;
            }
            // Split if we have many small breaks (might indicate poor GPS quality)
            if analysis.total_breaks > 10 {
                return true;
            }
        }

        false
    }

    /// Get break detection statistics
    pub fn get_break_stats(&self) -> BreakDetectionStats {
        BreakDetectionStats {
            time_threshold: self.config.break_time_threshold,
            distance_threshold: self.config.break_distance_threshold,
        }
    }
}

/// Analysis of break patterns in a GPS trace
#[derive(Debug, Clone, PartialEq)]
pub struct BreakAnalysis {
    pub total_breaks: usize,
    pub time_based_breaks: usize,
    pub distance_based_breaks: usize,
    pub total_time_gap: f64,       // seconds
    pub total_distance_gap: f64,   // meters
    pub average_time_gap: f64,     // seconds
    pub average_distance_gap: f64, // meters
    pub max_time_gap: f64,         // seconds
    pub max_distance_gap: f64,     // meters
}

impl Default for BreakAnalysis {
    fn default() -> Self {
        Self {
            total_breaks: 0,
            time_based_breaks: 0,
            distance_based_breaks: 0,
            total_time_gap: 0.0,
            total_distance_gap: 0.0,
            average_time_gap: 0.0,
            average_distance_gap: 0.0,
            max_time_gap: 0.0,
            max_distance_gap: 0.0,
        }
    }
}

/// Statistics about break detection configuration
#[derive(Debug, Clone, PartialEq)]
pub struct BreakDetectionStats {
    pub time_threshold: f64,     // seconds
    pub distance_threshold: f64, // meters
}