use crate::errors::ProjectionError;
use geo::Point;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GnssNetElementLink {
pub gnss_index: usize,
pub netelement_id: String,
pub projected_point: Point<f64>,
pub distance_meters: f64,
pub intrinsic_coordinate: f64,
pub heading_difference: Option<f64>,
pub probability: f64,
}
impl GnssNetElementLink {
pub fn new(
gnss_index: usize,
netelement_id: String,
projected_point: Point<f64>,
distance_meters: f64,
intrinsic_coordinate: f64,
heading_difference: Option<f64>,
probability: f64,
) -> Result<Self, ProjectionError> {
let link = Self {
gnss_index,
netelement_id,
projected_point,
distance_meters,
intrinsic_coordinate,
heading_difference,
probability,
};
link.validate()?;
Ok(link)
}
fn validate(&self) -> Result<(), ProjectionError> {
if self.netelement_id.is_empty() {
return Err(ProjectionError::InvalidGeometry(
"GnssNetElementLink netelement_id must not be empty".to_string(),
));
}
if self.distance_meters < 0.0 {
return Err(ProjectionError::InvalidGeometry(format!(
"distance_meters must be non-negative, got {}",
self.distance_meters
)));
}
if !(0.0..=1.0).contains(&self.intrinsic_coordinate) {
return Err(ProjectionError::InvalidGeometry(format!(
"intrinsic_coordinate must be in [0, 1], got {}",
self.intrinsic_coordinate
)));
}
if let Some(heading_diff) = self.heading_difference {
if !(0.0..=180.0).contains(&heading_diff) {
return Err(ProjectionError::InvalidGeometry(format!(
"heading_difference must be in [0, 180], got {}",
heading_diff
)));
}
}
if !(0.0..=1.0).contains(&self.probability) {
return Err(ProjectionError::InvalidGeometry(format!(
"Probability must be in [0, 1], got {}",
self.probability
)));
}
Ok(())
}
pub fn is_high_probability(&self, threshold: f64) -> bool {
self.probability >= threshold
}
pub fn is_within_distance(&self, max_distance_meters: f64) -> bool {
self.distance_meters <= max_distance_meters
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gnss_link_valid() {
let link = GnssNetElementLink::new(
5,
"NE_A".to_string(),
Point::new(4.3517, 50.8503),
3.2,
0.45,
Some(5.3),
0.89,
);
assert!(link.is_ok());
}
#[test]
fn test_gnss_link_invalid_probability() {
let link = GnssNetElementLink::new(
5,
"NE_A".to_string(),
Point::new(4.3517, 50.8503),
3.2,
0.45,
Some(5.3),
1.5, );
assert!(link.is_err());
}
#[test]
fn test_gnss_link_is_high_probability() {
let link = GnssNetElementLink::new(
5,
"NE_A".to_string(),
Point::new(4.3517, 50.8503),
3.2,
0.45,
Some(5.3),
0.89,
)
.unwrap();
assert!(link.is_high_probability(0.8));
assert!(link.is_high_probability(0.89));
assert!(!link.is_high_probability(0.9));
}
#[test]
fn test_gnss_link_is_within_distance() {
let link = GnssNetElementLink::new(
5,
"NE_A".to_string(),
Point::new(4.3517, 50.8503),
3.2,
0.45,
Some(5.3),
0.89,
)
.unwrap();
assert!(link.is_within_distance(5.0));
assert!(link.is_within_distance(3.2));
assert!(!link.is_within_distance(3.0));
}
#[test]
fn test_gnss_link_negative_distance() {
let link = GnssNetElementLink::new(
5,
"NE_A".to_string(),
Point::new(4.3517, 50.8503),
-1.0, 0.45,
Some(5.3),
0.89,
);
assert!(link.is_err());
}
#[test]
fn test_gnss_link_invalid_intrinsic() {
let link = GnssNetElementLink::new(
5,
"NE_A".to_string(),
Point::new(4.3517, 50.8503),
3.2,
1.5, Some(5.3),
0.89,
);
assert!(link.is_err());
}
#[test]
fn test_gnss_link_invalid_heading_difference() {
let link = GnssNetElementLink::new(
5,
"NE_A".to_string(),
Point::new(4.3517, 50.8503),
3.2,
0.45,
Some(200.0), 0.89,
);
assert!(link.is_err());
}
#[test]
fn test_gnss_link_no_heading_difference() {
let link = GnssNetElementLink::new(
5,
"NE_A".to_string(),
Point::new(4.3517, 50.8503),
3.2,
0.45,
None, 0.89,
);
assert!(link.is_ok());
}
#[test]
fn test_gnss_link_empty_netelement_id() {
let link = GnssNetElementLink::new(
5,
"".to_string(), Point::new(4.3517, 50.8503),
3.2,
0.45,
Some(5.3),
0.89,
);
assert!(link.is_err());
}
}