use crate::cant::segment::{CantSegment, CantSegmentType};
use crate::error::{AlignmentError, AlignmentResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CantAtStation {
pub left: f64,
pub right: f64,
}
pub fn cant_at(
segment: &CantSegment,
xi: f64,
rail_head_distance: Option<f64>,
) -> AlignmentResult<CantAtStation> {
if !(0.0..=1.0).contains(&xi) || !xi.is_finite() {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "normalized position must lie in [0.0, 1.0]",
});
}
match &segment.predefined_type {
CantSegmentType::ConstantCant => {
if segment
.end_cant_left
.is_some_and(|end| end != segment.start_cant_left)
|| segment
.end_cant_right
.is_some_and(|end| end != segment.start_cant_right)
{
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CONSTANTCANT requires equal (or absent) start and end cant",
});
}
Ok(CantAtStation {
left: segment.start_cant_left,
right: segment.start_cant_right,
})
}
CantSegmentType::VienneseBend => {
let b = rail_head_distance.ok_or(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "VIENNESEBEND requires the parent IfcAlignmentCant.RailHeadDistance",
})?;
if !(b.is_finite() && b > 0.0) {
return Err(AlignmentError::InvalidUnits {
detail: "RailHeadDistance must be finite and positive",
});
}
let (end_left, end_right) = required_ends(segment)?;
let left = vienna_bend(segment, segment.start_cant_left, end_left, xi, b)?;
let right = vienna_bend(segment, segment.start_cant_right, end_right, xi, b)?;
Ok(CantAtStation { left, right })
}
other => {
let fraction =
shape_fraction(other, xi).ok_or_else(|| AlignmentError::Unsupported {
entity: segment.entity,
type_name: segment.predefined_type_name().to_owned(),
detail: "cant PredefinedType has no defined base formula",
})?;
let (end_left, end_right) = required_ends(segment)?;
Ok(CantAtStation {
left: segment.start_cant_left + fraction * (end_left - segment.start_cant_left),
right: segment.start_cant_right + fraction * (end_right - segment.start_cant_right),
})
}
}
}
fn required_ends(segment: &CantSegment) -> AlignmentResult<(f64, f64)> {
match (segment.end_cant_left, segment.end_cant_right) {
(Some(left), Some(right)) => Ok((left, right)),
_ => Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "transition cant segments require both end cant values",
}),
}
}
fn shape_fraction(kind: &CantSegmentType, xi: f64) -> Option<f64> {
match kind {
CantSegmentType::LinearTransition => Some(xi),
CantSegmentType::BlossCurve => Some((3.0 - 2.0 * xi) * xi * xi),
CantSegmentType::CosineCurve => Some(0.5 * (1.0 - (std::f64::consts::PI * xi).cos())),
CantSegmentType::SineCurve => Some(
xi - (2.0 * std::f64::consts::PI).recip() * (2.0 * std::f64::consts::PI * xi).sin(),
),
CantSegmentType::HelmertCurve => Some(if xi <= 0.5 {
2.0 * xi * xi
} else {
1.0 - 2.0 * (1.0 - xi) * (1.0 - xi)
}),
_ => None,
}
}
fn vienna_bend(
segment: &CantSegment,
start: f64,
end: f64,
xi: f64,
b: f64,
) -> AlignmentResult<f64> {
let ratio_start = start / b;
let ratio_end = end / b;
if !(-1.0..=1.0).contains(&ratio_start) || !(-1.0..=1.0).contains(&ratio_end) {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "VIENNESEBEND cant exceeds the rail head distance (|D| > b)",
});
}
let psi_start = ratio_start.asin();
let psi_end = ratio_end.asin();
let delta_psi = psi_end - psi_start;
let blend = xi.powi(4) * (35.0 - 84.0 * xi + 70.0 * xi * xi - 20.0 * xi * xi * xi);
let psi = psi_start + delta_psi * blend;
Ok(b * psi.sin())
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::EntityId;
fn segment(
predefined_type: CantSegmentType,
start_left: f64,
end_left: Option<f64>,
start_right: f64,
end_right: Option<f64>,
) -> CantSegment {
CantSegment {
entity: EntityId(1),
start_dist_along: 0.0,
horizontal_length: 100.0,
start_cant_left: start_left,
end_cant_left: end_left,
start_cant_right: start_right,
end_cant_right: end_right,
predefined_type,
}
}
#[test]
fn constant_cant_holds_its_value_across_the_whole_segment() {
let s = segment(CantSegmentType::ConstantCant, 0.15, None, -0.15, None);
for xi in [0.0, 0.25, 0.5, 1.0] {
let at = cant_at(&s, xi, None).expect("constant cant");
assert_eq!(
at,
CantAtStation {
left: 0.15,
right: -0.15
}
);
}
}
#[test]
fn constant_cant_refuses_a_declared_end_that_disagrees_with_the_start() {
let s = segment(CantSegmentType::ConstantCant, 0.15, Some(0.20), -0.15, None);
assert!(cant_at(&s, 0.5, None).is_err());
}
#[test]
fn linear_transition_interpolates_exactly() {
let s = segment(
CantSegmentType::LinearTransition,
0.0,
Some(0.20),
0.0,
Some(-0.20),
);
let at = cant_at(&s, 0.25, None).expect("linear cant");
assert!((at.left - 0.05).abs() < 1e-12);
assert!((at.right - (-0.05)).abs() < 1e-12);
}
#[test]
fn bloss_curve_matches_the_published_base_formula_at_the_quarter_point() {
let s = segment(CantSegmentType::BlossCurve, 0.0, Some(1.0), 0.0, Some(1.0));
let at = cant_at(&s, 0.25, None).expect("bloss cant");
assert!((at.left - 0.15625).abs() < 1e-12);
}
#[test]
fn bloss_curve_is_symmetric_boundary_exact() {
let s = segment(CantSegmentType::BlossCurve, 0.0, Some(1.0), 0.0, Some(1.0));
assert!((cant_at(&s, 0.0, None).unwrap().left - 0.0).abs() < 1e-12);
assert!((cant_at(&s, 1.0, None).unwrap().left - 1.0).abs() < 1e-12);
}
#[test]
fn cosine_curve_matches_the_published_base_formula_at_the_midpoint() {
let s = segment(CantSegmentType::CosineCurve, 0.0, Some(1.0), 0.0, Some(1.0));
let at = cant_at(&s, 0.5, None).expect("cosine cant");
assert!((at.left - 0.5).abs() < 1e-9);
}
#[test]
fn sine_curve_starts_and_ends_exactly_on_the_endpoints() {
let s = segment(CantSegmentType::SineCurve, 0.0, Some(1.0), 0.0, Some(1.0));
assert!((cant_at(&s, 0.0, None).unwrap().left - 0.0).abs() < 1e-12);
assert!((cant_at(&s, 1.0, None).unwrap().left - 1.0).abs() < 1e-9);
}
#[test]
fn helmert_curve_is_continuous_across_its_own_midpoint_seam() {
let s = segment(
CantSegmentType::HelmertCurve,
0.0,
Some(1.0),
0.0,
Some(1.0),
);
let just_below = cant_at(&s, 0.5 - 1e-9, None).unwrap().left;
let at_mid = cant_at(&s, 0.5, None).unwrap().left;
let just_above = cant_at(&s, 0.5 + 1e-9, None).unwrap().left;
assert!((just_below - at_mid).abs() < 1e-6);
assert!((just_above - at_mid).abs() < 1e-6);
}
#[test]
fn transitions_refuse_a_missing_end_value() {
let s = segment(CantSegmentType::LinearTransition, 0.0, None, 0.0, None);
assert!(cant_at(&s, 0.5, None).is_err());
}
#[test]
fn vienna_bend_reproduces_the_endpoints_exactly() {
let s = segment(
CantSegmentType::VienneseBend,
0.0,
Some(0.15),
0.0,
Some(-0.15),
);
let start = cant_at(&s, 0.0, Some(1.5)).expect("vienna bend start");
let end = cant_at(&s, 1.0, Some(1.5)).expect("vienna bend end");
assert!((start.left - 0.0).abs() < 1e-9);
assert!((end.left - 0.15).abs() < 1e-9);
assert!((start.right - 0.0).abs() < 1e-9);
assert!((end.right - (-0.15)).abs() < 1e-9);
}
#[test]
fn vienna_bend_refuses_without_a_rail_head_distance() {
let s = segment(
CantSegmentType::VienneseBend,
0.0,
Some(0.15),
0.0,
Some(-0.15),
);
assert!(cant_at(&s, 0.5, None).is_err());
}
#[test]
fn vienna_bend_refuses_cant_exceeding_the_rail_head_distance() {
let s = segment(
CantSegmentType::VienneseBend,
0.0,
Some(2.0),
0.0,
Some(-0.15),
);
assert!(cant_at(&s, 0.5, Some(1.5)).is_err());
}
#[test]
fn user_defined_predefined_type_is_a_typed_refusal_not_a_guess() {
let s = segment(CantSegmentType::UserDefined, 0.0, Some(1.0), 0.0, Some(1.0));
assert!(matches!(
cant_at(&s, 0.5, None),
Err(AlignmentError::Unsupported { .. })
));
}
}