use axiolid_core::{Frame2, Vec2};
use axiolid_curve::{CurvatureLaw, Curve2, Intrinsic2};
use crate::cant::CantLayout;
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::HorizontalSegment;
fn curvature_of(radius: f64) -> f64 {
if radius == 0.0 {
0.0
} else {
1.0 / radius
}
}
fn transition_law(name: &str, start: f64, end: f64, length: f64) -> Option<CurvatureLaw> {
let delta = end - start;
match name {
"CLOTHOID" => Some(CurvatureLaw::clothoid(start, end, length)),
"BLOSSCURVE" => Some(CurvatureLaw::Polynomial {
coefficients: vec![
start,
0.0,
3.0 * delta / (length * length),
-2.0 * delta / (length * length * length),
],
}),
"COSINECURVE" => Some(CurvatureLaw::Sinusoid {
mean: start + delta / 2.0,
amplitude: delta / 2.0,
angular_frequency: core::f64::consts::PI / length,
phase: -core::f64::consts::FRAC_PI_2,
}),
"HELMERTCURVE" => Some(CurvatureLaw::piecewise(
vec![length / 2.0],
vec![
CurvatureLaw::Polynomial {
coefficients: vec![start, 0.0, 2.0 * delta / (length * length)],
},
CurvatureLaw::Polynomial {
coefficients: vec![
start + delta / 2.0,
2.0 * delta / length,
-2.0 * delta / (length * length),
],
},
],
)),
"SINECURVE" => Some(CurvatureLaw::sine_corrected_transition(start, end, length)),
_ => None,
}
}
fn viennese_law(
start: f64,
end: f64,
length: f64,
gravity_height: f64,
roll_swing: f64,
) -> CurvatureLaw {
let delta = end - start;
let l = length;
let hp = gravity_height * roll_swing;
let c2 = -420.0 * hp / l.powi(4);
let c3 = 1680.0 * hp / l.powi(5);
let c4 = 35.0 * (l * l * delta - 60.0 * hp) / l.powi(6);
let c5 = 84.0 * (-(l * l) * delta + 10.0 * hp) / l.powi(7);
let c6 = 70.0 * delta / l.powi(6);
let c7 = -20.0 * delta / l.powi(7);
CurvatureLaw::Polynomial {
coefficients: vec![start, 0.0, c2, c3, c4, c5, c6, c7],
}
}
pub fn is_exactly_lowerable(name: &str, has_cant: bool) -> bool {
match name {
"CLOTHOID" | "BLOSSCURVE" | "COSINECURVE" | "HELMERTCURVE" | "SINECURVE" => true,
"VIENNESEBEND" => has_cant,
_ => false,
}
}
pub fn spiral_curve(
segment: &HorizontalSegment,
name: &str,
cant: Option<&CantLayout>,
start_distance: f64,
) -> AlignmentResult<Curve2> {
if !(segment.segment_length.is_finite() && segment.segment_length > 0.0) {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "a transition spiral requires a finite, positive segment length",
});
}
let start = curvature_of(segment.start_radius);
let end = curvature_of(segment.end_radius);
if !start.is_finite() || !end.is_finite() {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "transition spiral endpoint curvatures must be finite",
});
}
let law = if name == "VIENNESEBEND" {
viennese_curvature(segment, start, end, cant, start_distance)?
} else {
transition_law(name, start, end, segment.segment_length).ok_or_else(|| {
AlignmentError::Unsupported {
entity: segment.entity,
type_name: name.to_owned(),
detail: "no single closed-form curvature law reconstructs this family \
from endpoint radii alone",
}
})?
};
let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
let frame = Frame2 {
origin: segment.start_point,
x: direction,
y: Vec2::new(-direction.y, direction.x),
};
let curve = Intrinsic2::new(frame, law, segment.segment_length);
if curve.total_turning().is_none_or(|turn| !turn.is_finite()) {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "transition spiral turning integral is not finite",
});
}
Ok(Curve2::Intrinsic(curve))
}
fn viennese_curvature(
segment: &HorizontalSegment,
start: f64,
end: f64,
cant: Option<&CantLayout>,
start_distance: f64,
) -> AlignmentResult<CurvatureLaw> {
let Some(layout) = cant else {
return Err(AlignmentError::Unsupported {
entity: segment.entity,
type_name: "VIENNESEBEND".to_owned(),
detail: "a Viennese bend needs the IfcAlignmentCant layout: its \\
curvature law depends on the superelevation swing",
});
};
let Some(height) = segment.gravity_center_line_height else {
return Err(AlignmentError::Unsupported {
entity: segment.entity,
type_name: "VIENNESEBEND".to_owned(),
detail: "a Viennese bend needs GravityCenterLineHeight: without \\
it the cant correction is undetermined, not zero",
});
};
if !height.is_finite() {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "GravityCenterLineHeight must be finite",
});
}
let swing = roll_swing(segment, layout, start_distance)?;
Ok(viennese_law(
start,
end,
segment.segment_length,
height,
swing,
))
}
fn roll_swing(
segment: &HorizontalSegment,
layout: &CantLayout,
start_distance: f64,
) -> AlignmentResult<f64> {
if !start_distance.is_finite() || start_distance < 0.0 {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "a Viennese bend needs a finite, non-negative station",
});
}
let end_distance = start_distance + segment.segment_length;
let at_start = layout.cant_at_distance(start_distance)?;
let at_end = layout.cant_at_distance(end_distance)?;
let b = layout.rail_head_distance;
if !(b.is_finite() && b > 0.0) {
return Err(AlignmentError::InvalidSegment {
entity: layout.entity,
detail: "RailHeadDistance must be finite and positive to convert \\
cant into a roll angle",
});
}
let angle = |station: &crate::cant::CantAtStation| {
let d = station.left - station.right;
(d / b).asin()
};
let swing = angle(&at_end) - angle(&at_start);
if !swing.is_finite() {
return Err(AlignmentError::InvalidSegment {
entity: layout.entity,
detail: "the cant swing across this segment is not finite",
});
}
Ok(swing)
}