pub mod crs;
pub mod detections;
pub mod errors;
pub mod io;
pub mod models;
pub mod path;
pub mod projection;
pub mod temporal;
pub mod workflow;
pub use detections::{
prepare_detections, prepare_detections_from_loaded, DetectionError, PreparedDetections,
};
pub use errors::ProjectionError;
pub use io::{
build_netelements_query, build_netrelations_query, parse_gnss_csv, parse_gnss_csv_str,
parse_gnss_geojson, parse_gnss_geojson_str, parse_netrelations_geojson, parse_network_geojson,
parse_network_geojson_str, parse_trainpath_csv, parse_trainpath_geojson, write_csv,
write_geojson, write_network_geojson, write_trainpath_csv, write_trainpath_geojson,
SparqlClient, UreqSparqlClient,
};
pub use models::{
AssociatedNetElement,
Detection,
DetectionKind,
DetectionRecord,
DetectionStatus,
DiscardReason,
GeographicLocation,
GnssNetElementLink,
GnssPosition,
LinearDetection,
NetRelation,
Netelement,
PathDiagnosticInfo,
PathMetadata,
PathOrigin,
ProjectedPosition,
PunctualDetection,
ResolvedAnchor,
RetrievalArea,
RetrievalOutcome,
RetrievalStatus,
RetrievedTopology,
RinfNavigability,
RinfNetelementRow,
RinfNetrelationRow,
SegmentDiagnostic,
TimestampOrRange,
TopologySource,
TopologyValidationReport,
TopologyValidationStatus,
TrainPath,
WorkflowKind,
DEFAULT_RETRIEVAL_BUFFER_METERS,
DEFAULT_RINF_ENDPOINT,
};
pub use path::{
calculate_mean_spacing,
calculate_train_path,
export_all_debug_info,
project_onto_path,
select_resampled_subset,
CandidateInfo,
CandidatePath,
DebugInfo,
PathCalculationMode,
PathConfig,
PathConfigBuilder,
PathDecision,
PathResult,
PositionCandidates,
TransitionProbabilityEntry,
};
pub use workflow::{build_retrieval_area, resolve_topology, validate_topology, RetrievalConfig};
pub type Result<T> = std::result::Result<T, ProjectionError>;
use geo::Point;
use projection::geom::project_gnss_position;
use projection::spatial::{find_nearest_netelement, NetworkIndex};
#[derive(Debug, Clone)]
pub struct ProjectionConfig {
pub projection_distance_warning_threshold: f64,
pub suppress_warnings: bool,
}
impl Default for ProjectionConfig {
fn default() -> Self {
Self {
projection_distance_warning_threshold: 50.0,
suppress_warnings: false,
}
}
}
pub struct RailwayNetwork {
index: NetworkIndex,
}
impl Clone for RailwayNetwork {
fn clone(&self) -> Self {
Self {
index: self.index.clone(),
}
}
}
impl RailwayNetwork {
pub fn new(netelements: Vec<Netelement>) -> Result<Self> {
let index = NetworkIndex::new(netelements)?;
Ok(Self { index })
}
pub fn find_nearest(&self, point: &Point<f64>) -> Result<usize> {
find_nearest_netelement(point, &self.index)
}
pub fn get_by_index(&self, index: usize) -> Option<&Netelement> {
self.index.netelements().get(index)
}
pub fn netelements(&self) -> &[Netelement] {
self.index.netelements()
}
pub fn netelement_count(&self) -> usize {
self.index.netelements().len()
}
pub fn len(&self) -> usize {
self.netelement_count()
}
pub fn is_empty(&self) -> bool {
self.netelement_count() == 0
}
pub fn iter(&self) -> impl Iterator<Item = &Netelement> {
self.index.netelements().iter()
}
}
#[tracing::instrument(skip(positions, network), fields(position_count = positions.len(), netelement_count = network.netelement_count()))]
pub fn project_gnss(
positions: &[GnssPosition],
network: &RailwayNetwork,
config: &ProjectionConfig,
) -> Result<Vec<ProjectedPosition>> {
tracing::info!(
"Starting projection of {} GNSS positions onto {} netelements",
positions.len(),
network.netelement_count()
);
let mut results = Vec::with_capacity(positions.len());
for (idx, gnss) in positions.iter().enumerate() {
let gnss_point = Point::new(gnss.longitude, gnss.latitude);
tracing::debug!(
position_idx = idx,
latitude = gnss.latitude,
longitude = gnss.longitude,
timestamp = %gnss.timestamp,
crs = %gnss.crs,
"Processing GNSS position"
);
let netelement_idx = network.find_nearest(&gnss_point)?;
let netelement = network.get_by_index(netelement_idx).ok_or_else(|| {
ProjectionError::InvalidGeometry(format!(
"Netelement index {} out of bounds",
netelement_idx
))
})?;
tracing::debug!(
position_idx = idx,
netelement_id = %netelement.id,
netelement_idx = netelement_idx,
"Assigned to nearest netelement"
);
let projected = project_gnss_position(
gnss,
netelement.id.clone(),
&netelement.geometry,
netelement.crs.clone(),
)?;
tracing::debug!(
position_idx = idx,
netelement_id = %netelement.id,
measure_meters = projected.measure_meters,
projection_distance_meters = projected.projection_distance_meters,
"Projection completed"
);
if !config.suppress_warnings
&& projected.projection_distance_meters > config.projection_distance_warning_threshold
{
tracing::warn!(
position_idx = idx,
projection_distance_meters = projected.projection_distance_meters,
threshold = config.projection_distance_warning_threshold,
timestamp = %gnss.timestamp,
netelement_id = %netelement.id,
"Large projection distance exceeds threshold"
);
eprintln!(
"WARNING: Large projection distance ({:.2}m > {:.2}m threshold) for position at {:?}",
projected.projection_distance_meters,
config.projection_distance_warning_threshold,
gnss.timestamp
);
}
results.push(projected);
}
tracing::info!(
projected_count = results.len(),
"Projection completed successfully"
);
Ok(results)
}
#[cfg(test)]
mod tests;