use super::*;
use crate::models::{AssociatedNetElement, PathMetadata, TrainPath};
use chrono::{DateTime, FixedOffset};
use geo::Point;
use std::collections::HashMap;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_test_projected_position() -> ProjectedPosition {
ProjectedPosition {
original: GnssPosition {
latitude: 50.8503,
longitude: 4.3517,
timestamp: "2024-01-15T10:30:00Z"
.parse::<DateTime<FixedOffset>>()
.unwrap(),
crs: "EPSG:4326".to_string(),
heading: None,
distance: None,
metadata: HashMap::new(),
},
projected_coords: Point::new(4.3517, 50.8503),
netelement_id: "NE001".to_string(),
measure_meters: 100.5,
projection_distance_meters: 5.2,
crs: "EPSG:4326".to_string(),
intrinsic: None,
}
}
#[test]
fn test_parse_network_geojson_basic() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"features": [
{{
"type": "Feature",
"properties": {{ "id": "NE001" }},
"geometry": {{
"type": "LineString",
"coordinates": [[4.3517, 50.8503], [4.3527, 50.8513]]
}}
}}
]
}}"#
)
.unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_ok());
let (netelements, netrelations) = result.unwrap();
assert_eq!(netelements.len(), 1);
assert_eq!(netelements[0].id, "NE001");
assert_eq!(netrelations.len(), 0);
}
#[test]
fn test_parse_network_geojson_with_netrelations() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"features": [
{{
"type": "Feature",
"properties": {{
"id": "NE001"
}},
"geometry": {{
"type": "LineString",
"coordinates": [[4.3517, 50.8503], [4.3527, 50.8513]]
}}
}},
{{
"type": "Feature",
"properties": {{
"type": "netrelation",
"id": "NR001",
"navigability": "Both",
"from": "NE001",
"to": "NE002",
"positionOnA": 1.0,
"positionOnB": 0.0
}},
"geometry": null
}}
]
}}"#
)
.unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_ok());
let (netelements, netrelations) = result.unwrap();
assert_eq!(netelements.len(), 1);
assert_eq!(netrelations.len(), 1);
assert_eq!(netrelations[0].id, "NR001");
}
#[test]
fn test_parse_network_geojson_not_feature_collection() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "Feature",
"properties": {{}},
"geometry": null
}}"#
)
.unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
if let Err(ProjectionError::InvalidGeometry(msg)) = result {
assert!(msg.contains("FeatureCollection"));
} else {
panic!("Expected InvalidGeometry error");
}
}
#[test]
fn test_parse_network_geojson_invalid_json() {
let mut file = NamedTempFile::new().unwrap();
write!(file, "not valid json").unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_parse_network_geojson_missing_id() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"features": [
{{
"type": "Feature",
"properties": {{}},
"geometry": {{
"type": "LineString",
"coordinates": [[4.3517, 50.8503], [4.3527, 50.8513]]
}}
}}
]
}}"#
)
.unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_parse_network_geojson_non_wgs84_crs() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"crs": {{
"type": "name",
"properties": {{ "name": "urn:ogc:def:crs:EPSG::31370" }}
}},
"features": []
}}"#
)
.unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
if let Err(ProjectionError::InvalidCrs(msg)) = result {
assert!(msg.contains("WGS84"));
} else {
panic!("Expected InvalidCrs error");
}
}
#[test]
fn test_parse_gnss_geojson_basic() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"features": [
{{
"type": "Feature",
"properties": {{
"timestamp": "2024-01-15T10:30:00Z"
}},
"geometry": {{
"type": "Point",
"coordinates": [4.3517, 50.8503]
}}
}}
]
}}"#
)
.unwrap();
let result = parse_gnss_geojson(file.path().to_str().unwrap(), "EPSG:4326");
assert!(result.is_ok());
let positions = result.unwrap();
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].latitude, 50.8503);
assert_eq!(positions[0].longitude, 4.3517);
}
#[test]
fn test_parse_gnss_geojson_missing_timestamp() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"features": [
{{
"type": "Feature",
"properties": {{}},
"geometry": {{
"type": "Point",
"coordinates": [4.3517, 50.8503]
}}
}}
]
}}"#
)
.unwrap();
let result = parse_gnss_geojson(file.path().to_str().unwrap(), "EPSG:4326");
assert!(result.is_err());
}
#[test]
fn test_parse_gnss_geojson_not_point() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"features": [
{{
"type": "Feature",
"properties": {{ "timestamp": "2024-01-15T10:30:00Z" }},
"geometry": {{
"type": "LineString",
"coordinates": [[4.3517, 50.8503], [4.3527, 50.8513]]
}}
}}
]
}}"#
)
.unwrap();
let result = parse_gnss_geojson(file.path().to_str().unwrap(), "EPSG:4326");
assert!(result.is_err());
}
#[test]
fn test_parse_gnss_geojson_with_metadata() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"features": [
{{
"type": "Feature",
"properties": {{
"timestamp": "2024-01-15T10:30:00Z",
"speed": 50.5,
"altitude": 100
}},
"geometry": {{
"type": "Point",
"coordinates": [4.3517, 50.8503]
}}
}}
]
}}"#
)
.unwrap();
let result = parse_gnss_geojson(file.path().to_str().unwrap(), "EPSG:4326");
assert!(result.is_ok());
let positions = result.unwrap();
assert_eq!(
positions[0].metadata.get("speed"),
Some(&"50.5".to_string())
);
}
#[test]
fn test_write_geojson_basic() {
let positions = vec![create_test_projected_position()];
let mut file = NamedTempFile::new().unwrap();
let result = write_geojson(&positions, &mut file);
assert!(result.is_ok());
let contents = std::fs::read_to_string(file.path()).unwrap();
assert!(contents.contains("FeatureCollection"));
assert!(contents.contains("NE001"));
}
#[test]
fn test_write_geojson_empty() {
let positions: Vec<ProjectedPosition> = vec![];
let mut file = NamedTempFile::new().unwrap();
let result = write_geojson(&positions, &mut file);
assert!(result.is_ok());
}
#[test]
fn test_parse_netrelations_geojson_basic() {
let mut file = NamedTempFile::new().unwrap();
write!(
file,
r#"{{
"type": "FeatureCollection",
"features": [
{{
"type": "Feature",
"properties": {{
"type": "netrelation",
"id": "NR001",
"navigability": "Both",
"from": "NE001",
"to": "NE002",
"positionOnA": 1.0,
"positionOnB": 0.0
}},
"geometry": null
}}
]
}}"#
)
.unwrap();
let result = parse_netrelations_geojson(file.path().to_str().unwrap());
assert!(result.is_ok());
let netrelations = result.unwrap();
assert_eq!(netrelations.len(), 1);
assert_eq!(netrelations[0].id, "NR001");
}
#[test]
fn test_write_trainpath_geojson_basic() {
let mut netelements_map = HashMap::new();
netelements_map.insert(
"NE001".to_string(),
Netelement {
id: "NE001".to_string(),
geometry: LineString::new(vec![
Coord {
x: 4.3517,
y: 50.8503,
},
Coord {
x: 4.3527,
y: 50.8513,
},
]),
crs: "EPSG:4326".to_string(),
},
);
let path = TrainPath::new(
vec![AssociatedNetElement {
netelement_id: "NE001".to_string(),
start_intrinsic: 0.0,
end_intrinsic: 1.0,
probability: 0.95,
gnss_start_index: 0,
gnss_end_index: 10,
origin: Default::default(),
}],
0.95,
None,
Some(PathMetadata {
distance_scale: 50.0,
heading_scale: 45.0,
cutoff_distance: 100.0,
heading_cutoff: 90.0,
probability_threshold: 0.001,
resampling_distance: None,
fallback_mode: false,
candidate_paths_evaluated: 1,
bidirectional_path: true,
diagnostic_info: None,
}),
)
.unwrap();
let mut file = NamedTempFile::new().unwrap();
let result = write_trainpath_geojson(&path, &netelements_map, &mut file);
assert!(result.is_ok());
let contents = std::fs::read_to_string(file.path()).unwrap();
assert!(contents.contains("FeatureCollection"));
assert!(contents.contains("NE001"));
}
#[test]
fn test_parse_network_geojson_empty_geometry() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {"id": "NE001"},
"geometry": null
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("geometry"));
}
}
#[test]
fn test_parse_network_geojson_point_geometry() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {"id": "NE001"},
"geometry": {
"type": "Point",
"coordinates": [4.3517, 50.8503]
}
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("LineString") || e.to_string().contains("geometry"));
}
}
#[test]
fn test_parse_network_geojson_polygon_geometry() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {"id": "NE001"},
"geometry": {
"type": "Polygon",
"coordinates": [[[4.3517, 50.8503], [4.3527, 50.8513], [4.3537, 50.8523], [4.3517, 50.8503]]]
}
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_parse_network_geojson_netrelation_missing_navigability() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"id": "NE001"
},
"geometry": {
"type": "LineString",
"coordinates": [[4.3517, 50.8503], [4.3527, 50.8513]]
}
},
{
"type": "Feature",
"properties": {
"id": "NR001",
"from": "NE001",
"to": "NE002",
"position_on_a": 1.0,
"position_on_b": 0.0
},
"geometry": null
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_parse_network_geojson_netrelation_invalid_navigability() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"id": "NE001"
},
"geometry": {
"type": "LineString",
"coordinates": [[4.3517, 50.8503], [4.3527, 50.8513]]
}
},
{
"type": "Feature",
"properties": {
"id": "NR001",
"from": "NE001",
"to": "NE002",
"position_on_a": 1.0,
"position_on_b": 0.0,
"navigability": "InvalidValue"
},
"geometry": null
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_parse_network_geojson_netrelation_missing_position_on_a() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"id": "NR001",
"from": "NE001",
"to": "NE002",
"position_on_b": 0.0,
"navigability": "Both"
},
"geometry": null
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_parse_network_geojson_netrelation_missing_position_on_b() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"id": "NR001",
"from": "NE001",
"to": "NE002",
"position_on_a": 1.0,
"navigability": "Both"
},
"geometry": null
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_network_geojson(file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_parse_gnss_geojson_invalid_coordinates_length() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"timestamp": "2025-12-09T14:30:00+01:00"
},
"geometry": {
"type": "Point",
"coordinates": [4.3517]
}
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_gnss_geojson(file.path().to_str().unwrap(), "EPSG:4326");
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("position") || e.to_string().contains("coordinate"));
}
}
#[test]
fn test_parse_gnss_geojson_with_heading_property() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"timestamp": "2025-12-09T14:30:00+01:00",
"heading": 90.5
},
"geometry": {
"type": "Point",
"coordinates": [4.3517, 50.8503]
}
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_gnss_geojson(file.path().to_str().unwrap(), "EPSG:4326");
assert!(result.is_ok());
let positions = result.unwrap();
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].heading, Some(90.5));
}
#[test]
fn test_parse_gnss_geojson_with_distance_property() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"timestamp": "2025-12-09T14:30:00+01:00",
"distance": 123.45
},
"geometry": {
"type": "Point",
"coordinates": [4.3517, 50.8503]
}
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_gnss_geojson(file.path().to_str().unwrap(), "EPSG:4326");
assert!(result.is_ok());
let positions = result.unwrap();
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].distance, Some(123.45));
}
#[test]
fn test_write_geojson_with_multiple_netelements() {
use chrono::Utc;
use geo::Point;
let gnss1 =
GnssPosition::new(50.8503, 4.3517, Utc::now().into(), "EPSG:4326".to_string()).unwrap();
let gnss2 =
GnssPosition::new(50.8513, 4.3527, Utc::now().into(), "EPSG:4326".to_string()).unwrap();
let positions = vec![
ProjectedPosition {
original: gnss1,
projected_coords: Point::new(4.3519, 50.8505),
netelement_id: "NE001".to_string(),
measure_meters: 50.0,
projection_distance_meters: 10.0,
crs: "EPSG:4326".to_string(),
intrinsic: Some(0.5),
},
ProjectedPosition {
original: gnss2,
projected_coords: Point::new(4.3529, 50.8515),
netelement_id: "NE002".to_string(),
measure_meters: 150.0,
projection_distance_meters: 10.0,
crs: "EPSG:4326".to_string(),
intrinsic: Some(0.5),
},
];
let mut file = NamedTempFile::new().unwrap();
let result = write_geojson(&positions, &mut file);
assert!(result.is_ok());
let contents = std::fs::read_to_string(file.path()).unwrap();
assert!(contents.contains("NE001"));
assert!(contents.contains("NE002"));
}
#[test]
fn test_parse_netrelations_geojson_empty_file() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": []
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_netrelations_geojson(file.path().to_str().unwrap());
assert!(result.is_ok());
let netrelations = result.unwrap();
assert_eq!(netrelations.len(), 0);
}
#[test]
fn test_parse_netrelations_geojson_with_netelement_a_and_netelement_b() {
let geojson_content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"type": "netrelation",
"id": "NR001",
"netelementA": "NE001",
"netelementB": "NE002",
"position_on_a": 1,
"position_on_b": 0,
"navigability": "AB"
},
"geometry": null
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(geojson_content.as_bytes()).unwrap();
let result = parse_netrelations_geojson(file.path().to_str().unwrap());
match &result {
Ok(netrelations) => {
assert_eq!(netrelations.len(), 1);
assert_eq!(netrelations[0].from_netelement_id, "NE001");
assert_eq!(netrelations[0].to_netelement_id, "NE002");
}
Err(e) => {
println!("Error: {}", e);
assert!(result.is_err());
}
}
}
#[test]
fn test_parse_network_geojson_str_basic() {
let content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "id": "NE001" },
"geometry": {
"type": "LineString",
"coordinates": [[4.3517, 50.8503], [4.3527, 50.8513]]
}
}
]
}"#;
let (netelements, netrelations) = parse_network_geojson_str(content).unwrap();
assert_eq!(netelements.len(), 1);
assert_eq!(netrelations.len(), 0);
assert_eq!(netelements[0].id, "NE001");
}
#[test]
fn test_parse_gnss_geojson_str_accepts_naive_iso_timestamp() {
let content = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "timestamp": "2026-05-25T12:34:56" },
"geometry": {
"type": "Point",
"coordinates": [4.3517, 50.8503]
}
}
]
}"#;
let positions = parse_gnss_geojson_str(content, "EPSG:4326").unwrap();
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].latitude, 50.8503);
assert_eq!(positions[0].longitude, 4.3517);
}
#[test]
fn test_write_network_geojson_round_trips_all_navigabilities() {
let netelements = vec![
Netelement::new(
"NE_A".to_string(),
LineString::from(vec![(4.0, 50.0), (4.1, 50.1)]),
"EPSG:4326".to_string(),
)
.unwrap(),
Netelement::new(
"NE_B".to_string(),
LineString::from(vec![(4.1, 50.1), (4.2, 50.2)]),
"EPSG:4326".to_string(),
)
.unwrap(),
];
let netrelations = vec![
NetRelation::new(
"NR_BOTH".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
0,
1,
true,
true,
)
.unwrap(),
NetRelation::new(
"NR_AB".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1,
0,
true,
false,
)
.unwrap(),
NetRelation::new(
"NR_BA".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1,
1,
false,
true,
)
.unwrap(),
NetRelation::new(
"NR_NONE".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
0,
0,
false,
false,
)
.unwrap(),
];
let mut out = Vec::new();
write_network_geojson(&netelements, &netrelations, &mut out).unwrap();
let content = String::from_utf8(out).unwrap();
let (parsed_nes, parsed_nrs) = parse_network_geojson_str(&content).unwrap();
assert_eq!(parsed_nes.len(), 2);
assert_eq!(parsed_nrs.len(), 4);
let ids: std::collections::HashSet<String> = parsed_nrs.into_iter().map(|r| r.id).collect();
assert!(ids.contains("NR_BOTH"));
assert!(ids.contains("NR_AB"));
assert!(ids.contains("NR_BA"));
assert!(ids.contains("NR_NONE"));
}
#[test]
fn test_parse_trainpath_geojson_accepts_naive_calculated_at() {
let mut file = NamedTempFile::new().unwrap();
let content = r#"{
"type": "FeatureCollection",
"properties": {
"overall_probability": 0.75,
"calculated_at": "2026-05-25T12:34:56"
},
"features": [
{
"type": "Feature",
"geometry": {"type": "LineString", "coordinates": [[4.0, 50.0], [4.1, 50.1]]},
"properties": {
"netelement_id": "NE_A",
"probability": 0.75,
"start_intrinsic": 0.0,
"end_intrinsic": 1.0,
"gnss_start_index": 0,
"gnss_end_index": 1
}
}
]
}"#;
file.write_all(content.as_bytes()).unwrap();
let parsed = parse_trainpath_geojson(file.path().to_str().unwrap()).unwrap();
assert_eq!(parsed.overall_probability, 0.75);
assert!(parsed.calculated_at.is_some());
assert_eq!(parsed.segments.len(), 1);
}