use ifc_model::{EntityId, Model};
use super::station::{resolve_referent_stationing, StationEquation};
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::AlignmentUnits;
use crate::view::AlignmentView;
pub const STATION_TOLERANCE: f64 = 1e-3;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Stationing {
pub alignment: EntityId,
equations: Vec<StationEquation>,
}
impl Stationing {
pub fn resolve(
model: &Model,
alignment: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<Self> {
let view = AlignmentView::for_model(model)?;
let hierarchy = view.hierarchy(alignment)?;
let mut referents = hierarchy.referents;
for id in view.filter_is_a(hierarchy.positioned, "IfcReferent") {
if !referents.contains(&id) {
referents.push(id);
}
}
let mut equations = Vec::with_capacity(referents.len());
for referent in referents {
if let Some(equation) = resolve_referent_stationing(model, referent, units)? {
equations.push(equation);
}
}
Self::from_equations(alignment, equations)
}
fn from_equations(
alignment: EntityId,
mut equations: Vec<StationEquation>,
) -> AlignmentResult<Self> {
for equation in &equations {
let finite = equation.distance_along.is_finite()
&& equation.station.is_finite()
&& equation.incoming_station.is_none_or(f64::is_finite);
if !finite {
return Err(AlignmentError::SemanticViolation {
entity: Some(equation.referent),
rule: "a stationing referent's distance and stations must be finite",
});
}
}
equations.sort_by(|a, b| a.distance_along.total_cmp(&b.distance_along));
for pair in equations.windows(2) {
let [previous, next] = pair else {
unreachable!("windows(2) yields pairs")
};
if next.distance_along - previous.distance_along <= STATION_TOLERANCE {
return Err(AlignmentError::SemanticViolation {
entity: Some(next.referent),
rule:
"two stationing referents of one alignment sit at the same distance along",
});
}
let carried = station_in(previous, next.distance_along);
let stated = next.incoming_station.unwrap_or(next.station);
if (stated - carried).abs() > STATION_TOLERANCE {
return Err(AlignmentError::SemanticViolation {
entity: Some(next.referent),
rule: if next.incoming_station.is_some() {
"IncomingStation disagrees with the station carried from the previous referent"
} else {
"a referent without IncomingStation must continue the previous station"
},
});
}
}
Ok(Self {
alignment,
equations,
})
}
#[must_use]
pub fn equations(&self) -> &[StationEquation] {
&self.equations
}
pub fn station_at(&self, distance_along: f64) -> AlignmentResult<f64> {
let out_of_range = AlignmentError::OutOfRange {
entity: self.alignment,
quantity: "distance along",
value: distance_along,
};
if !distance_along.is_finite() {
return Err(out_of_range);
}
let governing = self
.equations
.iter()
.rev()
.find(|equation| equation.distance_along <= distance_along)
.ok_or(out_of_range)?;
Ok(station_in(governing, distance_along))
}
pub fn distances_at(&self, station: f64) -> AlignmentResult<Vec<f64>> {
let out_of_range = AlignmentError::OutOfRange {
entity: self.alignment,
quantity: "station",
value: station,
};
if !station.is_finite() {
return Err(out_of_range);
}
let mut distances: Vec<f64> = Vec::new();
for (index, equation) in self.equations.iter().enumerate() {
let span = self.equations.get(index + 1).map_or(f64::INFINITY, |next| {
next.distance_along - equation.distance_along
});
let offset = (station - equation.station) * direction(equation);
let tolerance = 1e-9 * station.abs().max(1.0);
if offset < -tolerance || offset > span + tolerance {
continue;
}
let distance = equation.distance_along + offset.clamp(0.0, span);
let duplicate = distances
.last()
.is_some_and(|last| (distance - last).abs() <= tolerance);
if !duplicate {
distances.push(distance);
}
}
if distances.is_empty() {
return Err(out_of_range);
}
Ok(distances)
}
pub fn distance_at(&self, station: f64) -> AlignmentResult<f64> {
let distances = self.distances_at(station)?;
match distances.as_slice() {
[only] => Ok(*only),
_ => Err(AlignmentError::AmbiguousStation {
alignment: self.alignment,
station,
distances,
}),
}
}
}
fn direction(equation: &StationEquation) -> f64 {
if equation.has_increasing_station {
1.0
} else {
-1.0
}
}
fn station_in(equation: &StationEquation, distance_along: f64) -> f64 {
equation.station + direction(equation) * (distance_along - equation.distance_along)
}