use crate::errors::ProjectionError;
use crate::models::PathOrigin;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AssociatedNetElement {
pub netelement_id: String,
pub probability: f64,
pub start_intrinsic: f64,
pub end_intrinsic: f64,
pub gnss_start_index: usize,
pub gnss_end_index: usize,
#[serde(default)]
pub origin: PathOrigin,
}
impl AssociatedNetElement {
pub fn new(
netelement_id: String,
probability: f64,
start_intrinsic: f64,
end_intrinsic: f64,
gnss_start_index: usize,
gnss_end_index: usize,
) -> Result<Self, ProjectionError> {
let element = Self {
netelement_id,
probability,
start_intrinsic,
end_intrinsic,
gnss_start_index,
gnss_end_index,
origin: PathOrigin::Algorithm,
};
element.validate()?;
Ok(element)
}
fn validate(&self) -> Result<(), ProjectionError> {
if self.netelement_id.is_empty() {
return Err(ProjectionError::InvalidGeometry(
"AssociatedNetElement netelement_id must not be empty".to_string(),
));
}
if !(0.0..=1.0).contains(&self.probability) {
return Err(ProjectionError::InvalidGeometry(format!(
"Probability must be in [0, 1], got {}",
self.probability
)));
}
if !(0.0..=1.0).contains(&self.start_intrinsic) {
return Err(ProjectionError::InvalidGeometry(format!(
"start_intrinsic must be in [0, 1], got {}",
self.start_intrinsic
)));
}
if !(0.0..=1.0).contains(&self.end_intrinsic) {
return Err(ProjectionError::InvalidGeometry(format!(
"end_intrinsic must be in [0, 1], got {}",
self.end_intrinsic
)));
}
if self.gnss_start_index > self.gnss_end_index {
return Err(ProjectionError::InvalidGeometry(format!(
"gnss_start_index ({}) must be <= gnss_end_index ({})",
self.gnss_start_index, self.gnss_end_index
)));
}
Ok(())
}
pub fn fractional_length(&self) -> f64 {
(self.end_intrinsic - self.start_intrinsic).abs()
}
pub fn fractional_coverage(&self) -> f64 {
self.fractional_length()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_associated_netelement_valid() {
let segment = AssociatedNetElement::new("NE_A".to_string(), 0.87, 0.25, 0.78, 5, 12);
assert!(segment.is_ok());
let seg = segment.unwrap();
assert_eq!(seg.fractional_length(), 0.53);
}
#[test]
fn test_associated_netelement_invalid_indices() {
let segment = AssociatedNetElement::new(
"NE_A".to_string(),
0.87,
0.25,
0.78,
15, 12,
);
assert!(segment.is_err());
}
#[test]
fn test_origin_defaults_to_algorithm_when_missing() {
let json = r#"{
"netelement_id": "NE_A",
"probability": 0.87,
"start_intrinsic": 0.25,
"end_intrinsic": 0.78,
"gnss_start_index": 5,
"gnss_end_index": 12
}"#;
let segment: AssociatedNetElement =
serde_json::from_str(json).expect("deserialization must succeed");
assert_eq!(
segment.origin,
PathOrigin::Algorithm,
"missing origin field must default to Algorithm"
);
}
#[test]
fn test_origin_manual_roundtrip() {
let json = r#"{
"netelement_id": "NE_B",
"probability": 1.0,
"start_intrinsic": 0.0,
"end_intrinsic": 1.0,
"gnss_start_index": 0,
"gnss_end_index": 0,
"origin": "manual"
}"#;
let segment: AssociatedNetElement =
serde_json::from_str(json).expect("deserialization must succeed");
assert_eq!(segment.origin, PathOrigin::Manual);
let serialised = serde_json::to_string(&segment).expect("serialization must succeed");
assert!(
serialised.contains(r#""origin":"manual""#),
"manual origin must serialise as lowercase"
);
}
}