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};
#[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), 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()
)
}
}
#[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"));
}
let coords: Vec<Coord<f64>> = coordinates
.into_iter()
.map(|(lat, lon)| Coord { x: lon, y: 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()
)
}
}
#[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()
)
}
}
#[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()) }
#[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()
)
}
}
#[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()
)
}
}
#[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>> {
let gps_trace: Vec<GpsMeasurement> = gps_points
.into_iter()
.map(|py_gps| py_gps.inner)
.collect();
let result = self.inner.match_trace(&gps_trace, &road_network.inner);
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()
}
}
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),
}
}