use core::f64::consts::FRAC_PI_2;
use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Frame2, Point2, Scalar, Vec2};
use axiolid_curve::{CurvatureLaw, ElevationLaw, Intrinsic2};
use crate::arc_length::intrinsic_point;
pub const INVERSION_TOLERANCE: Scalar = 1e-12;
const MAX_INVERSION_STEPS: usize = 100;
const MAX_BRACKET_ROUNDS: usize = 64;
const MAX_CERTIFY_DEPTH: u32 = 24;
const MAX_CERTIFY_PANELS: usize = 1 << 16;
const ACCEPT: Scalar = 15.0 / 16.0;
fn invalid(detail: &str) -> GeomError {
GeomError::InvalidInput(detail.to_owned())
}
pub fn elevation_height(law: &ElevationLaw, distance: Scalar) -> GeomResult<Scalar> {
match leaf(law, distance)? {
(
ElevationLaw::Intrinsic {
height,
grade,
curvature,
},
local,
) => Ok(intrinsic_reading(*height, *grade, curvature, local)?.height),
(closed, local) => closed
.height_at(local)
.ok_or_else(|| invalid("elevation law has no height at that distance")),
}
}
pub fn elevation_grade(law: &ElevationLaw, distance: Scalar) -> GeomResult<Scalar> {
match leaf(law, distance)? {
(
ElevationLaw::Intrinsic {
height,
grade,
curvature,
},
local,
) => Ok(intrinsic_reading(*height, *grade, curvature, local)?.grade),
(closed, local) => closed
.grade_at(local)
.ok_or_else(|| invalid("elevation law has no grade at that distance")),
}
}
fn leaf(law: &ElevationLaw, distance: Scalar) -> GeomResult<(&ElevationLaw, Scalar)> {
if !distance.is_finite() {
return Err(invalid("plan distance must be finite"));
}
law.piece_at(distance)
.ok_or_else(|| invalid("elevation law has no piece at that distance"))
}
struct Reading {
height: Scalar,
grade: Scalar,
arc: Scalar,
}
fn profile_curve(
height: Scalar,
grade: Scalar,
curvature: &CurvatureLaw,
span: Scalar,
) -> Intrinsic2 {
let norm = grade.hypot(1.0);
let (sin0, cos0) = (grade / norm, 1.0 / norm);
Intrinsic2::new(
Frame2 {
origin: Point2::new(0.0, height),
x: Vec2::new(cos0, sin0),
y: Vec2::new(-sin0, cos0),
},
curvature.clone(),
span,
)
}
fn checked(height: Scalar, grade: Scalar, curvature: &CurvatureLaw) -> GeomResult<()> {
if !(height.is_finite() && grade.is_finite()) {
return Err(invalid(
"intrinsic profile needs a finite start height and grade",
));
}
if !curvature.is_well_formed() {
return Err(invalid("intrinsic profile curvature law is malformed"));
}
Ok(())
}
fn intrinsic_reading(
height: Scalar,
grade: Scalar,
curvature: &CurvatureLaw,
d: Scalar,
) -> GeomResult<Reading> {
checked(height, grade, curvature)?;
if d < 0.0 {
return Err(invalid(
"an intrinsic profile is defined forward from its start only",
));
}
if d == 0.0 {
return Ok(Reading {
height,
grade,
arc: 0.0,
});
}
let theta0 = grade.atan();
let curve = profile_curve(height, grade, curvature, d);
let upper = bracket(&curve, theta0, d)?;
let (mut lo, mut hi) = (d, upper);
let mut s = d;
let tolerance = INVERSION_TOLERANCE * d.max(1.0);
for _ in 0..MAX_INVERSION_STEPS {
let point = intrinsic_point(&curve, s)?;
let theta = theta0 + heading(&curve, s)?;
let residual = point.x - d;
if residual.abs() <= tolerance {
let slope = theta.tan();
return Ok(Reading {
height: point.y - slope * residual,
grade: slope,
arc: s,
});
}
if residual < 0.0 {
lo = s;
} else {
hi = s;
}
let mut next = s - residual / theta.cos();
if !(next > lo && next < hi) {
next = 0.5 * (lo + hi);
}
if next == s {
break;
}
s = next;
}
Err(GeomError::BudgetExceeded {
resource: "intrinsic profile plan-distance inversion",
})
}
fn heading(curve: &Intrinsic2, s: Scalar) -> GeomResult<Scalar> {
curve
.heading_at(s)
.ok_or_else(|| invalid("curvature law does not integrate to that arc length"))
}
fn bracket(curve: &Intrinsic2, theta0: Scalar, d: Scalar) -> GeomResult<Scalar> {
cos_floor(curve, theta0, 0.0, d)?;
let tolerance = INVERSION_TOLERANCE * d.max(1.0);
let mut upper = d;
for _ in 0..MAX_BRACKET_ROUNDS {
let reached = intrinsic_point(curve, upper)?.x;
if reached >= d - tolerance {
return Ok(upper);
}
let cos = (theta0 + heading(curve, upper)?).cos();
let mut step = (d - reached) / cos * (1.0 + 1e-6) + Scalar::EPSILON * upper.max(1.0);
loop {
match cos_floor(curve, theta0, upper, upper + step) {
Ok(_) => break,
Err(error) => {
step *= 0.5;
if step <= Scalar::EPSILON * upper.max(1.0) {
return Err(error);
}
}
}
}
upper += step;
}
Err(GeomError::BudgetExceeded {
resource: "intrinsic profile plan-distance bracket",
})
}
fn cos_floor(curve: &Intrinsic2, theta0: Scalar, lo: Scalar, hi: Scalar) -> GeomResult<Scalar> {
let vertical = || invalid("the profile turns vertical before reaching that plan distance");
let mut floor: Scalar = 1.0;
let mut stack = vec![(lo, hi, 0_u32)];
let mut inspected = 0_usize;
while let Some((a, b, depth)) = stack.pop() {
inspected += 1;
if inspected > MAX_CERTIFY_PANELS {
return Err(GeomError::BudgetExceeded {
resource: "intrinsic profile below-vertical certificate",
});
}
let theta_a = theta0 + heading(curve, a)?;
if theta_a.is_nan() || theta_a.abs() >= FRAC_PI_2 {
return Err(vertical());
}
let shifted = curve
.curvature
.shifted(a)
.ok_or_else(|| invalid("curvature law cannot be read from that arc length"))?;
let variation = Intrinsic2::new(curve.start, shifted, b - a)
.turning_variation_bound(b - a)
.ok_or_else(|| invalid("curvature law does not integrate over the span"))?;
let reach = theta_a.abs() + variation;
let panel_floor = reach.cos();
let below_vertical = reach < FRAC_PI_2 && panel_floor > 0.0;
if below_vertical && (panel_floor >= ACCEPT * theta_a.cos() || depth >= MAX_CERTIFY_DEPTH) {
floor = floor.min(panel_floor);
continue;
}
if depth >= MAX_CERTIFY_DEPTH {
return Err(vertical());
}
let mid = 0.5 * (a + b);
stack.push((mid, b, depth + 1));
stack.push((a, mid, depth + 1));
}
Ok(floor)
}
#[must_use]
pub fn elevation_chord_bound(law: &ElevationLaw, a: Scalar, b: Scalar) -> Option<Scalar> {
if !(a.is_finite() && b.is_finite()) {
return None;
}
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
if lo == hi {
return Some(0.0);
}
let second = second_derivative_bound(law, lo, hi)?;
let h = hi - lo;
let bound = h * h * 0.125 * second;
Some(bound * (1.0 + 1e-12)).filter(|b| b.is_finite())
}
fn second_derivative_bound(law: &ElevationLaw, lo: Scalar, hi: Scalar) -> Option<Scalar> {
match law {
ElevationLaw::Polynomial { coefficients } => {
let m = lo.abs().max(hi.abs());
Some(
coefficients
.iter()
.enumerate()
.skip(2)
.map(|(i, c)| {
let i_f = i as Scalar;
i_f * (i_f - 1.0) * c.abs() * m.powi(i as i32 - 2)
})
.sum(),
)
.filter(|s: &Scalar| s.is_finite())
}
ElevationLaw::Piecewise { breaks, laws } => {
if laws.len() != breaks.len() + 1 {
return None;
}
let mid = 0.5 * (lo + hi);
let index = breaks.partition_point(|b| *b <= mid);
let start = if index == 0 { 0.0 } else { breaks[index - 1] };
let lower = if index == 0 {
Scalar::NEG_INFINITY
} else {
start
};
let upper = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
if lo < lower || hi > upper {
return None;
}
second_derivative_bound(laws.get(index)?, lo - start, hi - start)
}
ElevationLaw::CircularArc { grade, radius, .. } => {
if !(grade.is_finite() && radius.is_finite() && *radius != 0.0) {
return None;
}
let sin0 = grade / grade.hypot(1.0);
let reach = (sin0 + lo / radius).abs().max((sin0 + hi / radius).abs());
if reach.is_nan() || reach >= 1.0 {
return None;
}
let cos = ((1.0 - reach) * (1.0 + reach)).sqrt();
Some(1.0 / (radius.abs() * cos * cos * cos)).filter(|s| s.is_finite())
}
ElevationLaw::Intrinsic {
height,
grade,
curvature,
} => {
checked(*height, *grade, curvature).ok()?;
if lo < 0.0 {
return None;
}
let theta0 = grade.atan();
let curve = profile_curve(*height, *grade, curvature, hi);
let past = hi + 2.0 * INVERSION_TOLERANCE * hi.max(1.0);
let upper = bracket(&curve, theta0, past).ok()?;
let floor = cos_floor(&curve, theta0, lo, upper).ok()?;
let kappa = curvature_sup(&curvature.shifted(lo)?, upper - lo)?;
Some(kappa / (floor * floor * floor)).filter(|s| s.is_finite())
}
_ => None,
}
}
fn curvature_sup(law: &CurvatureLaw, span: Scalar) -> Option<Scalar> {
let polynomial = |coefficients: &[Scalar]| -> Scalar {
coefficients
.iter()
.enumerate()
.map(|(i, c)| c.abs() * span.powi(i as i32))
.sum()
};
let sup = match law {
CurvatureLaw::Constant { curvature } => curvature.abs(),
CurvatureLaw::Polynomial { coefficients } if coefficients.len() <= 2 => {
let start = coefficients.first().copied().unwrap_or(0.0);
let rate = coefficients.get(1).copied().unwrap_or(0.0);
start.abs().max((start + rate * span).abs())
}
CurvatureLaw::Polynomial { coefficients } => polynomial(coefficients),
CurvatureLaw::Sinusoid {
mean, amplitude, ..
} => mean.abs() + amplitude.abs(),
CurvatureLaw::Composite {
polynomial: terms,
harmonics,
} => polynomial(terms) + harmonics.iter().map(|h| h.amplitude.abs()).sum::<Scalar>(),
CurvatureLaw::Piecewise { breaks, laws } => {
if !law.is_well_formed() {
return None;
}
let mut sup: Scalar = 0.0;
let mut start = 0.0;
for (index, piece) in laws.iter().enumerate() {
let end = breaks.get(index).copied().unwrap_or(span).min(span);
sup = sup.max(curvature_sup(piece, (end - start).max(0.0))?);
if end >= span {
break;
}
start = end;
}
sup
}
_ => return None,
};
Some(sup).filter(|s| s.is_finite())
}
pub(crate) fn elevation_second(law: &ElevationLaw, distance: Scalar) -> GeomResult<Scalar> {
let value = match leaf(law, distance)? {
(ElevationLaw::Polynomial { coefficients }, d) => coefficients
.iter()
.enumerate()
.skip(2)
.rev()
.fold(0.0, |accumulated, (i, c)| {
accumulated * d + c * (i * (i - 1)) as Scalar
}),
(ElevationLaw::CircularArc { grade, radius, .. }, d) => {
let sin = arc_sin(*grade, *radius, d)
.ok_or_else(|| invalid("elevation law has no height at that distance"))?;
let cos = ((1.0 - sin) * (1.0 + sin)).sqrt();
1.0 / (radius * cos * cos * cos)
}
(
ElevationLaw::Intrinsic {
height,
grade,
curvature,
},
d,
) => {
let reading = intrinsic_reading(*height, *grade, curvature, d)?;
let kappa = crate::frenet::law_value(curvature, reading.arc)
.ok_or_else(|| invalid("intrinsic profile curvature is not defined there"))?;
let secant = reading.grade.hypot(1.0);
kappa * secant * secant * secant
}
_ => {
return Err(invalid(
"elevation law family has no second derivative here",
))
}
};
if value.is_finite() {
Ok(value)
} else {
Err(invalid("elevation second derivative is not finite"))
}
}
fn arc_sin(grade: Scalar, radius: Scalar, d: Scalar) -> Option<Scalar> {
if !(grade.is_finite() && radius.is_finite() && radius != 0.0 && d.is_finite()) {
return None;
}
let sin = grade / grade.hypot(1.0) + d / radius;
(sin.abs() < 1.0).then_some(sin)
}
pub(crate) fn profile_bounds(law: &ElevationLaw, lo: Scalar, hi: Scalar) -> Option<[Scalar; 3]> {
if !(lo.is_finite() && hi.is_finite()) || lo > hi {
return None;
}
let out = match law {
ElevationLaw::Polynomial { coefficients } => {
let m = lo.abs().max(hi.abs());
let term = |k: usize| -> Scalar {
coefficients
.iter()
.enumerate()
.skip(k)
.map(|(i, c)| {
let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
falling * c.abs() * m.powi((i - k) as i32)
})
.sum()
};
[term(1), term(2), term(3)]
}
ElevationLaw::Piecewise { breaks, laws } => {
if laws.len() != breaks.len() + 1 {
return None;
}
let mid = 0.5 * (lo + hi);
let index = breaks.partition_point(|b| *b <= mid);
let start = if index == 0 { 0.0 } else { breaks[index - 1] };
let lower = if index == 0 {
Scalar::NEG_INFINITY
} else {
start
};
let upper = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
if lo < lower || hi > upper {
return None;
}
return profile_bounds(laws.get(index)?, lo - start, hi - start);
}
ElevationLaw::CircularArc { grade, radius, .. } => {
if !(grade.is_finite() && radius.is_finite() && *radius != 0.0) {
return None;
}
let sin0 = grade / grade.hypot(1.0);
let reach = (sin0 + lo / radius).abs().max((sin0 + hi / radius).abs());
if reach.is_nan() || reach >= 1.0 {
return None;
}
let cos = ((1.0 - reach) * (1.0 + reach)).sqrt();
let r = radius.abs();
[
reach / cos,
1.0 / (r * cos.powi(3)),
3.0 * reach / (r * r * cos.powi(5)),
]
}
ElevationLaw::Intrinsic {
height,
grade,
curvature,
} => {
checked(*height, *grade, curvature).ok()?;
if lo < 0.0 {
return None;
}
let theta0 = grade.atan();
let profile = profile_curve(*height, *grade, curvature, hi);
let past = hi + 2.0 * INVERSION_TOLERANCE * hi.max(1.0);
let upper = bracket(&profile, theta0, past).ok()?;
if curvature
.seams_within(upper)
.iter()
.any(|&seam| seam > lo && seam < upper)
{
return None;
}
let floor = cos_floor(&profile, theta0, lo, upper).ok()?;
let kappa = curvature_sup(&curvature.shifted(lo)?, upper - lo)?;
let rate = curvature_sup(&curvature.derivative().shifted(lo)?, upper - lo)?;
let sin = ((1.0 - floor) * (1.0 + floor)).max(0.0).sqrt();
[
sin / floor,
kappa / floor.powi(3),
rate / floor.powi(4) + 3.0 * kappa * kappa * sin / floor.powi(5),
]
}
_ => return None,
};
let out = out.map(|v| v * (1.0 + 1e-12));
out.iter().all(|v| v.is_finite()).then_some(out)
}
pub(crate) fn profile_bounds4(law: &ElevationLaw, lo: Scalar, hi: Scalar) -> Option<[Scalar; 4]> {
if !(lo.is_finite() && hi.is_finite()) || lo > hi {
return None;
}
let out = match law {
ElevationLaw::Polynomial { coefficients } => {
let m = lo.abs().max(hi.abs());
let term = |k: usize| -> Scalar {
coefficients
.iter()
.enumerate()
.skip(k)
.map(|(i, c)| {
let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
falling * c.abs() * m.powi((i - k) as i32)
})
.sum()
};
[term(1), term(2), term(3), term(4)]
}
ElevationLaw::Piecewise { breaks, laws } => {
if laws.len() != breaks.len() + 1 {
return None;
}
let mid = 0.5 * (lo + hi);
let index = breaks.partition_point(|b| *b <= mid);
let start = if index == 0 { 0.0 } else { breaks[index - 1] };
let lower = if index == 0 {
Scalar::NEG_INFINITY
} else {
start
};
let upper = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
if lo < lower || hi > upper {
return None;
}
return profile_bounds4(laws.get(index)?, lo - start, hi - start);
}
ElevationLaw::CircularArc { grade, radius, .. } => {
if !(grade.is_finite() && radius.is_finite() && *radius != 0.0) {
return None;
}
let sin0 = grade / grade.hypot(1.0);
let reach = (sin0 + lo / radius).abs().max((sin0 + hi / radius).abs());
if reach.is_nan() || reach >= 1.0 {
return None;
}
let cos = ((1.0 - reach) * (1.0 + reach)).sqrt();
let r = radius.abs();
[
reach / cos,
1.0 / (r * cos.powi(3)),
3.0 * reach / (r * r * cos.powi(5)),
3.0 * (1.0 + 4.0 * reach * reach) / (r * r * r * cos.powi(7)),
]
}
_ => return None,
};
let out = out.map(|v| v * (1.0 + 1e-12));
out.iter().all(|v| v.is_finite()).then_some(out)
}
pub(crate) fn elevation_seams(law: &ElevationLaw) -> Vec<Scalar> {
fn collect(law: &ElevationLaw, offset: Scalar, out: &mut Vec<Scalar>) {
if let ElevationLaw::Piecewise { breaks, laws } = law {
for (index, piece) in laws.iter().enumerate() {
let start = if index == 0 { 0.0 } else { breaks[index - 1] };
collect(piece, offset + start, out);
}
out.extend(breaks.iter().map(|b| offset + b));
}
}
let mut out = Vec::new();
if law.is_well_formed() {
collect(law, 0.0, &mut out);
}
out.sort_by(Scalar::total_cmp);
out.dedup();
out
}
pub(crate) fn elevation_grade_before(law: &ElevationLaw, distance: Scalar) -> GeomResult<Scalar> {
match law {
ElevationLaw::Piecewise { breaks, laws } => {
if laws.len() != breaks.len() + 1 {
return Err(invalid("elevation law piece list is malformed"));
}
let index = breaks.partition_point(|b| *b < distance);
let start = if index == 0 { 0.0 } else { breaks[index - 1] };
elevation_grade_before(&laws[index], distance - start)
}
other => elevation_grade(other, distance),
}
}