use axiolid_curve::ElevationLaw;
use ifc_model::{EntityId, Model};
use super::terminal::split_closing;
use super::tolerance::SeamTolerance;
use crate::error::{AlignmentError, AlignmentResult, ProfileSeam};
use crate::horizontal::AlignmentUnits;
use crate::vertical::{read_vertical_segment, VerticalSegment, VerticalSegmentType};
use crate::view::AlignmentView;
pub fn elevation_law(segment: &VerticalSegment) -> AlignmentResult<ElevationLaw> {
match segment.predefined_type {
VerticalSegmentType::ConstantGradient => constant_gradient(segment),
VerticalSegmentType::ParabolicArc => parabolic_arc(segment),
VerticalSegmentType::CircularArc => circular_arc(segment),
ref kind => Err(AlignmentError::Unsupported {
entity: segment.entity,
type_name: kind.source_name().to_owned(),
detail: refusal(kind),
}),
}
}
fn refusal(kind: &VerticalSegmentType) -> &'static str {
match kind {
VerticalSegmentType::Clothoid => {
"no determined elevation law: a vertical clothoid's curvature runs linearly along its \
3D arc length, but IfcAlignmentVerticalSegment states neither its start nor its end \
curvature (RadiusOfCurvature is defined for arcs and parabolas only)"
}
_ => "no exact elevation law: the vertical PredefinedType defines no curve law",
}
}
fn constant_gradient(segment: &VerticalSegment) -> AlignmentResult<ElevationLaw> {
let curved = segment.radius_of_curvature.is_some_and(|r| r != 0.0);
if curved
|| !SeamTolerance::strict().same_gradient(segment.start_gradient, segment.end_gradient)
{
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CONSTANTGRADIENT requires equal gradients and no non-zero 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)
}
fn circular_arc(segment: &VerticalSegment) -> AlignmentResult<ElevationLaw> {
let invalid = |detail| {
Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail,
})
};
if segment.horizontal_length <= 0.0 {
return invalid("CIRCULARARC requires a positive horizontal length");
}
let Some(radius) = segment
.radius_of_curvature
.filter(|r| r.is_finite() && *r != 0.0)
else {
return invalid("CIRCULARARC requires a finite, non-zero RadiusOfCurvature");
};
let change = segment.end_gradient - segment.start_gradient;
let turns =
!SeamTolerance::strict().same_gradient(segment.start_gradient, segment.end_gradient);
if turns && change.signum() != radius.signum() {
return invalid(
"CIRCULARARC RadiusOfCurvature turns against the authored change of gradient \
(a positive radius is a sag and gains grade)",
);
}
let law = ElevationLaw::circular_arc(segment.start_height, segment.start_gradient, radius);
if !law.is_well_formed() || law.height_at(segment.horizontal_length).is_none() {
return invalid(
"CIRCULARARC turns vertical before its end: |sin t0 + L / R| reaches 1, so the arc \
has no height there",
);
}
Ok(law)
}
pub fn profile_law(segments: &[VerticalSegment]) -> AlignmentResult<ElevationLaw> {
profile_law_within(segments, SeamTolerance::strict())
}
pub fn profile_law_within(
segments: &[VerticalSegment],
tolerance: SeamTolerance,
) -> AlignmentResult<ElevationLaw> {
let (body, closing) = split_closing(segments, |s| s.horizontal_length, |s| s.entity)?;
let Some(first) = body.first() else {
return Err(AlignmentError::SemanticViolation {
entity: None,
rule: "a vertical profile must have at least one segment",
});
};
let mut laws: Vec<ElevationLaw> = Vec::with_capacity(body.len());
let mut breaks = Vec::with_capacity(body.len() - 1);
let start = first.start_dist_along;
for (index, segment) in body.iter().enumerate() {
let law = elevation_law(segment)?;
if let (Some(previous), Some(previous_law)) =
(index.checked_sub(1).map(|i| &body[i]), laws.last())
{
check_seam(previous, previous_law, segment, tolerance)?;
breaks.push(segment.start_dist_along - start);
}
laws.push(law);
}
if let (Some(closing), Some(last), Some(last_law)) = (closing, body.last(), laws.last()) {
check_seam(last, last_law, closing, tolerance)?;
}
Ok(match laws.len() {
1 => laws.remove(0),
_ => ElevationLaw::Piecewise { breaks, laws },
})
}
fn check_seam(
previous: &VerticalSegment,
previous_law: &ElevationLaw,
segment: &VerticalSegment,
tolerance: SeamTolerance,
) -> AlignmentResult<()> {
let expected = previous.start_dist_along + previous.horizontal_length;
if !tolerance.same_length(segment.start_dist_along, expected) {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "vertical segments must be contiguous and ascending in StartDistAlong",
});
}
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",
},
)?;
if !tolerance.same_length(segment.start_height, end_height) {
return Err(AlignmentError::ProfileDiscontinuity {
entity: segment.entity,
previous: previous.entity,
seam: ProfileSeam::Height,
expected: end_height,
actual: segment.start_height,
});
}
Ok(())
}
pub fn vertical_profile_law(
model: &Model,
entity: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<ElevationLaw> {
let view = AlignmentView::for_model(model)?;
let layout = model
.get(entity)
.ok_or(AlignmentError::MissingEntity { entity })?;
if !view.schema.is_a(&layout.type_name, "IfcAlignmentVertical") {
return Err(AlignmentError::WrongType {
entity,
expected: "IfcAlignmentVertical",
actual: layout.type_name.to_string(),
});
}
let tolerance = SeamTolerance::for_model(model, units)?;
let segments = view
.segment_chain(entity, "IfcAlignmentVerticalSegment")?
.into_iter()
.map(|id| read_vertical_segment(model, id, units))
.collect::<AlignmentResult<Vec<_>>>()?;
if segments.is_empty() {
return Err(AlignmentError::SemanticViolation {
entity: Some(entity),
rule: "IfcAlignmentVertical must nest at least one IfcAlignmentSegment",
});
}
profile_law_within(&segments, tolerance)
}
pub(crate) fn indexed_from_plan_start(
law: ElevationLaw,
start: f64,
end: f64,
plan_length: f64,
vertical: EntityId,
tolerance: SeamTolerance,
) -> AlignmentResult<ElevationLaw> {
let starts_late = start > 0.0 && !tolerance.same_length(start, 0.0);
let ends_early = end < plan_length && !tolerance.same_length(end, plan_length);
if starts_late || ends_early {
return Err(AlignmentError::Unsupported {
entity: vertical,
type_name: "IfcAlignmentVertical".to_owned(),
detail: "the vertical profile does not cover the whole plan; the composed curve has \
no domain to leave stations outside the profile without heights",
});
}
if start >= 0.0 {
return Ok(law);
}
let offset = -start;
let malformed = || AlignmentError::InvalidSegment {
entity: vertical,
detail: "vertical profile law is not a run of polynomial and circular pieces",
};
let (breaks, laws) = match law {
ElevationLaw::Piecewise { breaks, laws } => (breaks, laws),
single => (Vec::new(), vec![single]),
};
let index = breaks.partition_point(|b| *b <= offset);
let piece_start = if index == 0 { 0.0 } else { breaks[index - 1] };
let mut laws = laws.into_iter().skip(index);
let local = offset - piece_start;
let first = match laws.next() {
Some(ElevationLaw::Polynomial { coefficients }) => taylor_shift(&coefficients, local),
Some(arc @ ElevationLaw::CircularArc { radius, .. }) => {
match (arc.height_at(local), arc.grade_at(local)) {
(Some(height), Some(grade)) => ElevationLaw::circular_arc(height, grade, radius),
_ => return Err(malformed()),
}
}
_ => return Err(malformed()),
};
let mut shifted = vec![first];
shifted.extend(laws);
Ok(match shifted.len() {
1 => shifted.remove(0),
_ => ElevationLaw::Piecewise {
breaks: breaks[index..].iter().map(|b| b - offset).collect(),
laws: shifted,
},
})
}
fn taylor_shift(coefficients: &[f64], offset: f64) -> ElevationLaw {
let shifted = (0..coefficients.len())
.map(|k| {
let mut binomial = 1.0;
let mut power = 1.0;
let mut sum = 0.0;
for (j, coefficient) in coefficients.iter().enumerate().skip(k) {
if j > k {
binomial = binomial * j as f64 / (j - k) as f64;
power *= offset;
}
sum += binomial * coefficient * power;
}
sum
})
.collect();
ElevationLaw::Polynomial {
coefficients: shifted,
}
}