use axiolid_curve::ElevationLaw;
use crate::error::{AlignmentError, AlignmentResult, ProfileSeam};
use crate::vertical::{VerticalSegment, VerticalSegmentType};
pub fn elevation_law(segment: &VerticalSegment) -> AlignmentResult<ElevationLaw> {
match segment.predefined_type {
VerticalSegmentType::ConstantGradient => constant_gradient(segment),
VerticalSegmentType::ParabolicArc => parabolic_arc(segment),
ref kind => Err(AlignmentError::Unsupported {
entity: segment.entity,
type_name: kind.source_name().to_owned(),
detail: "no exact elevation law: only constant gradient and parabolic arc are polynomial in plan distance",
}),
}
}
fn constant_gradient(segment: &VerticalSegment) -> AlignmentResult<ElevationLaw> {
if segment.radius_of_curvature.is_some() || segment.start_gradient != segment.end_gradient {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CONSTANTGRADIENT requires equal gradients and no curvature radius",
});
}
Ok(ElevationLaw::constant_grade(
segment.start_height,
segment.start_gradient,
))
}
fn parabolic_arc(segment: &VerticalSegment) -> AlignmentResult<ElevationLaw> {
if segment.horizontal_length <= 0.0 {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "PARABOLICARC requires a positive horizontal length",
});
}
let law = ElevationLaw::parabolic(
segment.start_height,
segment.start_gradient,
segment.end_gradient,
segment.horizontal_length,
);
if !law.is_well_formed() {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "PARABOLICARC parameters do not form a finite elevation law",
});
}
Ok(law)
}
pub fn profile_law(segments: &[VerticalSegment]) -> AlignmentResult<ElevationLaw> {
let Some(first) = segments.first() else {
return Err(AlignmentError::SemanticViolation {
entity: None,
rule: "a vertical profile must have at least one segment",
});
};
if segments.len() == 1 {
return elevation_law(first);
}
let mut laws: Vec<ElevationLaw> = Vec::with_capacity(segments.len());
let mut breaks = Vec::with_capacity(segments.len() - 1);
let start = first.start_dist_along;
for (index, segment) in segments.iter().enumerate() {
let law = elevation_law(segment)?;
if let (Some(previous), Some(previous_law)) =
(index.checked_sub(1).map(|i| &segments[i]), laws.last())
{
let expected = previous.start_dist_along + previous.horizontal_length;
if !approximately(segment.start_dist_along, expected) {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "vertical segments must be contiguous and ascending in StartDistAlong",
});
}
check_seam(previous, previous_law, segment)?;
breaks.push(segment.start_dist_along - start);
}
laws.push(law);
}
Ok(ElevationLaw::Piecewise { breaks, laws })
}
fn check_seam(
previous: &VerticalSegment,
previous_law: &ElevationLaw,
segment: &VerticalSegment,
) -> AlignmentResult<()> {
let end_height = previous_law.height_at(previous.horizontal_length).ok_or(
AlignmentError::InvalidSegment {
entity: previous.entity,
detail: "vertical segment has no finite end height",
},
)?;
let seams = [
(ProfileSeam::Height, end_height, segment.start_height),
(
ProfileSeam::Gradient,
previous.end_gradient,
segment.start_gradient,
),
];
for (seam, expected, actual) in seams {
if !approximately(actual, expected) {
return Err(AlignmentError::ProfileDiscontinuity {
entity: segment.entity,
previous: previous.entity,
seam,
expected,
actual,
});
}
}
Ok(())
}
fn approximately(left: f64, right: f64) -> bool {
let scale = left.abs().max(right.abs()).max(1.0);
(left - right).abs() <= 1e-9 * scale
}