use std::sync::Arc;
use axiolid_curve::ElevationLaw;
use ifc_alignment::{
profile_law, read_vertical_segment, AlignmentError, AlignmentUnits, ProfileSeam,
VerticalSegment,
};
use ifc_model::{Entity, EntityId, Model, Value};
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
}
}
type Row = (f64, f64, f64, f64, f64, Option<f64>, &'static str);
fn read_profile(rows: &[Row]) -> Vec<VerticalSegment> {
let mut model = Model::new();
for (index, (start, length, height, entry, exit, radius, kind)) in rows.iter().enumerate() {
model.insert(
EntityId(index as u64 + 1),
Entity::new(
"IFCALIGNMENTVERTICALSEGMENT",
vec![
Value::Null,
Value::Null,
Value::Real(*start),
Value::Real(*length),
Value::Real(*height),
Value::Real(*entry),
Value::Real(*exit),
radius.map_or(Value::Null, Value::Real),
Value::Enum(Arc::from(*kind)),
],
),
);
}
(1..=rows.len() as u64)
.map(|id| read_vertical_segment(&model, EntityId(id), metres()).expect("reads"))
.collect()
}
const GRADE: Row = (1000.0, 100.0, 50.0, 0.02, 0.02, None, "CONSTANTGRADIENT");
const CREST: Row = (
1100.0,
200.0,
52.0,
0.02,
-0.03,
Some(4000.0),
"PARABOLICARC",
);
#[test]
fn a_matched_seam_is_accepted() {
let law = profile_law(&read_profile(&[GRADE, CREST])).expect("continuous");
assert_eq!(law.height_at(100.0), Some(52.0));
assert_eq!(law.height_at(200.0), Some(52.75));
}
#[test]
fn a_matched_seam_after_a_parabola_is_accepted() {
let tail = (1300.0, 50.0, 51.0, -0.03, -0.03, None, "CONSTANTGRADIENT");
let law = profile_law(&read_profile(&[GRADE, CREST, tail])).expect("continuous");
let end = law.height_at(350.0).expect("height");
assert!((end - 49.5).abs() < 1e-12, "profile end was {end}");
}
#[test]
fn a_seam_equal_up_to_rounding_is_accepted() {
let first = (
23_456.789,
87.65,
312.456,
0.0123,
0.0123,
None,
"CONSTANTGRADIENT",
);
let second = (
23_544.439,
40.0,
313.534_095,
0.0123,
0.0123,
None,
"CONSTANTGRADIENT",
);
profile_law(&read_profile(&[first, second])).expect("equal up to rounding");
}
#[test]
fn a_height_step_is_refused() {
let stepped = (
1100.0,
200.0,
52.01,
0.02,
-0.03,
Some(4000.0),
"PARABOLICARC",
);
let error = profile_law(&read_profile(&[GRADE, stepped])).expect_err("step");
let AlignmentError::ProfileDiscontinuity {
entity,
previous,
seam,
expected,
actual,
} = error
else {
panic!("expected a typed discontinuity, got {error}");
};
assert_eq!(entity, EntityId(2));
assert_eq!(previous, EntityId(1));
assert_eq!(seam, ProfileSeam::Height);
assert!((expected - 52.0).abs() < 1e-12, "expected {expected}");
assert_eq!(actual, 52.01);
}
#[test]
fn a_millimetre_step_is_refused() {
let stepped = (
1100.0,
200.0,
52.001,
0.02,
-0.03,
Some(4000.0),
"PARABOLICARC",
);
assert!(matches!(
profile_law(&read_profile(&[GRADE, stepped])),
Err(AlignmentError::ProfileDiscontinuity {
seam: ProfileSeam::Height,
..
})
));
}
fn pieces(law: &ElevationLaw, at: f64) -> (&ElevationLaw, &ElevationLaw, f64) {
let ElevationLaw::Piecewise { breaks, laws } = law else {
panic!("a multi-segment profile is piecewise: {law:?}");
};
let index = breaks.iter().position(|b| *b == at).expect("seam");
let start = if index == 0 { 0.0 } else { breaks[index - 1] };
(&laws[index], &laws[index + 1], at - start)
}
#[test]
fn a_grade_break_with_continuous_height_is_accepted() {
let broken = (1100.0, 100.0, 52.0, 0.03, 0.03, None, "CONSTANTGRADIENT");
let law = profile_law(&read_profile(&[GRADE, broken])).expect("grade break");
let (before, after, local_end) = pieces(&law, 100.0);
assert_eq!(before.height_at(local_end), Some(52.0));
assert_eq!(after.height_at(0.0), Some(52.0));
assert_eq!(before.grade_at(local_end), Some(0.02));
assert_eq!(after.grade_at(0.0), Some(0.03));
assert_eq!(law.height_at(100.0), Some(52.0));
assert_eq!(law.grade_at(100.0), Some(0.03));
assert_eq!(law.height_at(200.0), Some(55.0));
}
#[test]
fn a_grade_break_after_a_parabola_is_accepted() {
let rising = (1300.0, 50.0, 51.0, 0.02, 0.02, None, "CONSTANTGRADIENT");
let law = profile_law(&read_profile(&[GRADE, CREST, rising])).expect("grade break");
let (before, after, local_end) = pieces(&law, 300.0);
assert_eq!(before.height_at(local_end), Some(51.0));
assert_eq!(after.height_at(0.0), Some(51.0));
let exit = before.grade_at(local_end).expect("grade");
assert!((exit + 0.03).abs() < 1e-15, "parabola exit grade {exit}");
assert_eq!(after.grade_at(0.0), Some(0.02));
assert_eq!(law.height_at(350.0), Some(52.0));
}
#[test]
fn a_grade_break_with_a_height_step_is_refused() {
let stepped = (1100.0, 100.0, 52.5, 0.03, 0.03, None, "CONSTANTGRADIENT");
assert_eq!(
profile_law(&read_profile(&[GRADE, stepped])),
Err(AlignmentError::ProfileDiscontinuity {
entity: EntityId(2),
previous: EntityId(1),
seam: ProfileSeam::Height,
expected: 52.0,
actual: 52.5,
})
);
}