mod support;
use ifc_alignment::{AlignmentError, Stationing};
use support::{metres, Builder};
fn close(left: f64, right: f64) -> bool {
(left - right).abs() <= 1e-9 * left.abs().max(right.abs()).max(1.0)
}
#[test]
fn two_alignments_yield_two_tables() {
let mut b = Builder::new();
let (main, spur) = (b.alignment("main"), b.alignment("spur"));
let (main_curve, spur_curve) = (b.curve(), b.curve());
let m0 = b.station(main_curve, 0.0, 1_000.0, None, None);
let m1 = b.station(main_curve, 100.0, 1_200.0, Some(1_100.0), None);
let s0 = b.station(spur_curve, 0.0, 0.0, None, None);
let s1 = b.station(spur_curve, 40.0, 40.0, None, None);
b.nest(main, &[m0, m1]);
b.nest(spur, &[s0]);
b.positions(spur, &[s1]);
let model = b.finish();
let main_table = Stationing::resolve(&model, main, metres()).expect("main");
let spur_table = Stationing::resolve(&model, spur, metres()).expect("spur");
let referents =
|table: &Stationing| -> Vec<_> { table.equations().iter().map(|e| e.referent).collect() };
assert_eq!(main_table.alignment, main);
assert_eq!(referents(&main_table), vec![m0, m1]);
assert_eq!(referents(&spur_table), vec![s0, s1]);
assert_eq!(main_table.station_at(50.0), Ok(1_050.0));
assert_eq!(spur_table.station_at(50.0), Ok(50.0));
#[allow(deprecated)]
let mixed = ifc_alignment::station_equations(&model, metres()).expect("model-wide");
assert_eq!(mixed.len(), 4);
}
#[test]
fn station_and_distance_round_trip_across_an_equation() {
let mut b = Builder::new();
let road = b.alignment("road");
let curve = b.curve();
let r0 = b.station(curve, 0.0, 1_000.0, None, Some(true));
let r1 = b.station(curve, 100.0, 1_200.0, Some(1_100.0), Some(true));
b.nest(road, &[r0, r1]);
let model = b.finish();
let table = Stationing::resolve(&model, road, metres()).expect("resolves");
for distance in [0.0, 12.5, 99.0, 100.5, 150.0, 1_000.0] {
let station = table.station_at(distance).expect("station");
let back = table.distance_at(station).expect("distance");
assert!(close(back, distance), "{distance} -> {station} -> {back}");
}
assert_eq!(table.station_at(150.0), Ok(1_250.0));
assert_eq!(table.station_at(100.0), Ok(1_200.0));
assert_eq!(table.distance_at(1_100.0), Ok(100.0));
assert_eq!(table.distance_at(1_200.0), Ok(100.0));
assert!(matches!(
table.distance_at(1_150.0),
Err(AlignmentError::OutOfRange {
quantity: "station",
..
})
));
}
#[test]
fn a_station_that_occurs_twice_is_ambiguous_not_guessed() {
let mut b = Builder::new();
let road = b.alignment("road");
let curve = b.curve();
let r0 = b.station(curve, 0.0, 0.0, None, None);
let r1 = b.station(curve, 100.0, 80.0, Some(100.0), None);
b.nest(road, &[r0, r1]);
let model = b.finish();
let table = Stationing::resolve(&model, road, metres()).expect("resolves");
assert_eq!(table.distances_at(90.0), Ok(vec![90.0, 110.0]));
assert!(matches!(
table.distance_at(90.0),
Err(AlignmentError::AmbiguousStation { station, ref distances, .. })
if station == 90.0 && distances == &vec![90.0, 110.0]
));
assert_eq!(table.distance_at(50.0), Ok(50.0));
assert_eq!(table.distance_at(150.0), Ok(170.0));
}
#[test]
fn decreasing_stations_map_both_ways_across_an_equation() {
let mut b = Builder::new();
let road = b.alignment("road");
let curve = b.curve();
let r0 = b.station(curve, 0.0, 5_000.0, None, Some(false));
let r1 = b.station(curve, 300.0, 4_600.0, Some(4_700.0), Some(false));
b.nest(road, &[r0, r1]);
let model = b.finish();
let table = Stationing::resolve(&model, road, metres()).expect("resolves");
assert_eq!(table.station_at(250.0), Ok(4_750.0));
assert_eq!(table.station_at(350.0), Ok(4_550.0));
assert_eq!(table.distance_at(4_750.0), Ok(250.0));
assert_eq!(table.distance_at(4_550.0), Ok(350.0));
for distance in [0.0, 10.0, 299.0, 301.0, 2_000.0] {
let station = table.station_at(distance).expect("station");
assert!(close(table.distance_at(station).expect("back"), distance));
}
assert!(table.distance_at(5_001.0).is_err());
}
#[test]
fn a_distance_before_the_first_referent_is_out_of_range() {
let mut b = Builder::new();
let road = b.alignment("road");
let curve = b.curve();
let r0 = b.station(curve, 10.0, 0.0, None, None);
b.nest(road, &[r0]);
let model = b.finish();
let table = Stationing::resolve(&model, road, metres()).expect("resolves");
assert!(matches!(
table.station_at(9.0),
Err(AlignmentError::OutOfRange { quantity: "distance along", entity, .. }) if entity == road
));
assert!(table.station_at(f64::NAN).is_err());
assert!(table.distance_at(-1.0).is_err());
}
#[test]
fn an_alignment_without_stationing_has_an_empty_table() {
let mut b = Builder::new();
let road = b.alignment("road");
let curve = b.curve();
let pier = b.marker(curve, 20.0);
b.nest(road, &[pier]);
let model = b.finish();
let table = Stationing::resolve(&model, road, metres()).expect("resolves");
assert!(table.equations().is_empty());
assert!(table.station_at(20.0).is_err());
}
#[test]
fn contradictory_stations_are_refused() {
let mut b = Builder::new();
let (wrong_incoming, silent_jump, same_place) = (
b.alignment("wrong incoming"),
b.alignment("silent jump"),
b.alignment("same place"),
);
let curve = b.curve();
let a0 = b.station(curve, 0.0, 0.0, None, None);
let a1 = b.station(curve, 100.0, 200.0, Some(90.0), None);
b.nest(wrong_incoming, &[a0, a1]);
let s0 = b.station(curve, 0.0, 0.0, None, None);
let s1 = b.station(curve, 100.0, 200.0, None, None);
b.nest(silent_jump, &[s0, s1]);
let p0 = b.station(curve, 50.0, 0.0, None, None);
let p1 = b.station(curve, 50.0, 0.0, None, None);
b.nest(same_place, &[p0, p1]);
let model = b.finish();
for (alignment, referent) in [(wrong_incoming, a1), (silent_jump, s1), (same_place, p1)] {
assert!(
matches!(
Stationing::resolve(&model, alignment, metres()),
Err(AlignmentError::SemanticViolation { entity: Some(e), .. }) if e == referent
),
"{alignment}"
);
}
}
#[test]
fn stationing_of_something_other_than_an_alignment_is_refused() {
let mut b = Builder::new();
let curve = b.curve();
let marker = b.station(curve, 0.0, 0.0, None, None);
let model = b.finish();
assert!(matches!(
Stationing::resolve(&model, marker, metres()),
Err(AlignmentError::WrongType { entity, .. }) if entity == marker
));
}