pelorus 0.1.0

A high-performance GPS map-matching library for geospatial road network processing
Documentation
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
use geo::{Point, Coord};
use crate::types::{GpsMeasurement, RoadSegment};
use crate::road_network::RoadNetwork;
use crate::matching::{MapMatcher, MapMatchingConfig, MatchedPoint, MatchedTrace, MapMatchingError};

/// Python wrapper for GpsMeasurement
#[pyclass(name = "GpsMeasurement")]
#[derive(Clone)]
pub struct PyGpsMeasurement {
    pub inner: GpsMeasurement,
}

#[pymethods]
impl PyGpsMeasurement {
    #[new]
    pub fn new(latitude: f64, longitude: f64, timestamp: f64) -> Self {
        Self {
            inner: GpsMeasurement {
                point: Point::new(longitude, latitude), // Note: geo crate uses (x, y) = (lon, lat)
                timestamp,
            },
        }
    }
    
    #[getter]
    pub fn latitude(&self) -> f64 {
        self.inner.point.y()
    }
    
    #[getter]
    pub fn longitude(&self) -> f64 {
        self.inner.point.x()
    }
    
    #[getter]
    pub fn timestamp(&self) -> f64 {
        self.inner.timestamp
    }
    
    fn __repr__(&self) -> String {
        format!(
            "GpsMeasurement(latitude={:.6}, longitude={:.6}, timestamp={:.1})",
            self.latitude(),
            self.longitude(),
            self.timestamp()
        )
    }
}

/// Python wrapper for RoadNetwork
#[pyclass(name = "RoadNetwork")]
pub struct PyRoadNetwork {
    pub inner: RoadNetwork,
}

#[pymethods]
impl PyRoadNetwork {
    #[new]
    pub fn new() -> Self {
        Self {
            inner: RoadNetwork::new(),
        }
    }
    
    pub fn add_segment(&mut self, segment_id: u32, coordinates: Vec<(f64, f64)>) -> PyResult<()> {
        if coordinates.len() < 2 {
            return Err(PyValueError::new_err("Road segment must have at least 2 coordinates"));
        }
        
        // Convert (lat, lon) tuples to Coords and create LineString
        let coords: Vec<Coord<f64>> = coordinates
            .into_iter()
            .map(|(lat, lon)| Coord { x: lon, y: lat }) // geo crate uses (x, y) = (lon, lat)
            .collect();
        
        let geometry = geo::LineString::new(coords);
        let segment = RoadSegment {
            id: segment_id,
            geometry,
        };
        
        self.inner.add_segment(segment);
        Ok(())
    }
    
    #[getter]
    pub fn segment_count(&self) -> usize {
        self.inner.segment_count()
    }
    
    #[getter]
    pub fn intersection_count(&self) -> usize {
        self.inner.intersection_count()
    }
    
    fn __repr__(&self) -> String {
        format!(
            "RoadNetwork(segments={}, intersections={})",
            self.segment_count(),
            self.intersection_count()
        )
    }
}

/// Python wrapper for MapMatchingConfig
#[pyclass(name = "MapMatchingConfig")]
#[derive(Clone)]
pub struct PyMapMatchingConfig {
    pub inner: MapMatchingConfig,
}

#[pymethods]
impl PyMapMatchingConfig {
    #[new]
    #[pyo3(signature = (max_candidate_distance=50.0, max_candidates_per_point=10, gps_sigma=5.0, break_time_threshold=120.0, break_distance_threshold=200.0))]
    pub fn new(
        max_candidate_distance: f64,
        max_candidates_per_point: usize,
        gps_sigma: f64,
        break_time_threshold: f64,
        break_distance_threshold: f64,
    ) -> Self {
        Self {
            inner: MapMatchingConfig {
                max_candidate_distance,
                max_candidates_per_point,
                gps_sigma,
                break_time_threshold,
                break_distance_threshold,
            },
        }
    }
    
    #[getter]
    pub fn max_candidate_distance(&self) -> f64 {
        self.inner.max_candidate_distance
    }
    
    #[getter]
    pub fn max_candidates_per_point(&self) -> usize {
        self.inner.max_candidates_per_point
    }
    
    #[getter]
    pub fn gps_sigma(&self) -> f64 {
        self.inner.gps_sigma
    }
    
    #[getter]
    pub fn break_time_threshold(&self) -> f64 {
        self.inner.break_time_threshold
    }
    
    #[getter]
    pub fn break_distance_threshold(&self) -> f64 {
        self.inner.break_distance_threshold
    }
    
    fn __repr__(&self) -> String {
        format!(
            "MapMatchingConfig(max_candidate_distance={:.1}, max_candidates_per_point={}, gps_sigma={:.1})",
            self.max_candidate_distance(),
            self.max_candidates_per_point(),
            self.gps_sigma()
        )
    }
}

/// Python wrapper for MatchedPoint
#[pyclass(name = "MatchedPoint")]
#[derive(Clone)]
pub struct PyMatchedPoint {
    pub inner: MatchedPoint,
}

#[pymethods]
impl PyMatchedPoint {
    #[getter]
    pub fn gps_measurement(&self) -> PyGpsMeasurement {
        PyGpsMeasurement {
            inner: self.inner.gps_measurement.clone(),
        }
    }
    
    #[getter]
    pub fn segment_id(&self) -> u32 {
        self.inner.segment_id
    }
    
    #[getter]
    pub fn projected_coordinates(&self) -> (f64, f64) {
        let point = &self.inner.projected_point;
        (point.y(), point.x()) // Return as (lat, lon)
    }
    
    #[getter]
    pub fn emission_probability(&self) -> f64 {
        self.inner.emission_probability
    }
    
    fn __repr__(&self) -> String {
        let (lat, lon) = self.projected_coordinates();
        format!(
            "MatchedPoint(segment_id={}, projected=({:.6}, {:.6}), probability={:.3})",
            self.segment_id(),
            lat,
            lon,
            self.emission_probability()
        )
    }
}

/// Python wrapper for MatchedTrace
#[pyclass(name = "MatchedTrace")]
#[derive(Clone)]
pub struct PyMatchedTrace {
    pub inner: MatchedTrace,
}

#[pymethods]
impl PyMatchedTrace {
    #[getter]
    pub fn points(&self) -> Vec<PyMatchedPoint> {
        self.inner
            .points
            .iter()
            .map(|point| PyMatchedPoint {
                inner: point.clone(),
            })
            .collect()
    }
    
    #[getter]
    pub fn total_probability(&self) -> f64 {
        self.inner.total_probability
    }
    
    fn __len__(&self) -> usize {
        self.inner.points.len()
    }
    
    fn __repr__(&self) -> String {
        format!(
            "MatchedTrace(points={}, total_probability={:.3})",
            self.__len__(),
            self.total_probability()
        )
    }
}

/// Python wrapper for MapMatcher
#[pyclass(name = "MapMatcher")]
pub struct PyMapMatcher {
    pub inner: MapMatcher,
}

#[pymethods]
impl PyMapMatcher {
    #[new]
    #[pyo3(signature = (config=None))]
    pub fn new(config: Option<PyMapMatchingConfig>) -> Self {
        let matcher = match config {
            Some(py_config) => MapMatcher::with_config(py_config.inner),
            None => MapMatcher::new(),
        };
        
        Self { inner: matcher }
    }
    
    pub fn match_trace(
        &self,
        gps_points: Vec<PyGpsMeasurement>,
        road_network: &PyRoadNetwork,
    ) -> PyResult<Vec<PyMatchedTrace>> {
        // Convert Python types to Rust types
        let gps_trace: Vec<GpsMeasurement> = gps_points
            .into_iter()
            .map(|py_gps| py_gps.inner)
            .collect();
        
        // Run map matching
        let result = self.inner.match_trace(&gps_trace, &road_network.inner);
        
        // Convert result to Python types
        match result {
            Ok(matched_traces) => {
                let py_traces: Vec<PyMatchedTrace> = matched_traces
                    .into_iter()
                    .map(|trace| PyMatchedTrace { inner: trace })
                    .collect();
                Ok(py_traces)
            }
            Err(e) => Err(convert_map_matching_error(e)),
        }
    }
    
    fn __repr__(&self) -> String {
        "MapMatcher()".to_string()
    }
}

/// Convert MapMatchingError to Python exception
fn convert_map_matching_error(error: MapMatchingError) -> PyErr {
    let message = error.to_string();
    match error {
        MapMatchingError::InvalidInput(_) => PyValueError::new_err(message),
        MapMatchingError::NoValidCandidates(_) => PyValueError::new_err(message),
        MapMatchingError::ViterbiSolverError(_) => PyValueError::new_err(message),
        MapMatchingError::ProjectionError(_) => PyValueError::new_err(message),
    }
}