Skip to main content

ifc_alignment/referent/
stationing.rs

1//! One alignment's stationing, and station to distance-along lookup.
2//!
3//! An alignment states its stationing through the `IfcReferent`s it nests
4//! (`IfcRelNests`, "Object Nesting") or positions (`IfcRelPositions`), each
5//! carrying `Pset_Stationing`. Referents of other alignments never enter
6//! the table, which is what keeps two alignments' chainages apart.
7//!
8//! # The mapping
9//!
10//! Sorted by distance along, referent `i` at distance `d_i` with
11//! `Station = s_i` governs `[d_i, d_{i+1})` (the last one is open-ended):
12//!
13//! ```text
14//! station(d) = s_i + k_i * (d - d_i),   k_i = +1 if HasIncreasingStation else -1
15//! ```
16//!
17//! At an equation the referent's `Station` is the value ahead of it and
18//! its `IncomingStation` the value the previous range reaches there. Both
19//! are checked: `IncomingStation` must equal `s_{i-1} + k_{i-1} * (d_i -
20//! d_{i-1})`, and a referent without one must continue that value, within
21//! [`STATION_TOLERANCE`]. A contradiction is refused rather than resolved
22//! in favour of either number.
23//!
24//! The inverse treats each range as closed at its end, so the station a
25//! range reaches at an equation (the incoming value) maps back to that
26//! equation's distance. A station that occurs in several ranges, as after
27//! an equation that steps back, is reported by [`Stationing::distances_at`]
28//! and refused by [`Stationing::distance_at`].
29
30use ifc_model::{EntityId, Model};
31
32use super::station::{resolve_referent_stationing, StationEquation};
33use crate::error::{AlignmentError, AlignmentResult};
34use crate::horizontal::AlignmentUnits;
35use crate::view::AlignmentView;
36
37/// How far, in metres, a referent's stated station may differ from the
38/// value carried from the previous referent before the two are treated as
39/// contradictory.
40///
41/// One millimetre: stations and distances are authored independently and
42/// commonly rounded to the millimetre, so a tighter bound would refuse
43/// sound files, while any real equation is a deliberate, larger jump that
44/// is stated through `IncomingStation`.
45pub const STATION_TOLERANCE: f64 = 1e-3;
46
47/// The stationing of one `IfcAlignment`: its station equations in distance
48/// order, and lookups across them.
49#[derive(Debug, Clone, PartialEq)]
50#[non_exhaustive]
51pub struct Stationing {
52    /// The `IfcAlignment` this stationing belongs to.
53    pub alignment: EntityId,
54    equations: Vec<StationEquation>,
55}
56
57impl Stationing {
58    /// Resolve the stationing of `alignment` from the referents it nests
59    /// or positions.
60    ///
61    /// Referents without `Pset_Stationing` (a bridge pier marker, say) are
62    /// not part of the stationing and are skipped. An alignment with no
63    /// stationing referent resolves to an empty table, whose lookups are
64    /// refused.
65    ///
66    /// # Errors
67    ///
68    /// Refuses a model that is not IFC4X3, an `alignment` that is not an
69    /// `IfcAlignment`, a malformed relationship or referent, two stationing
70    /// referents at one distance, a non-finite station, and a station
71    /// that contradicts the one carried from the previous referent.
72    pub fn resolve(
73        model: &Model,
74        alignment: EntityId,
75        units: AlignmentUnits,
76    ) -> AlignmentResult<Self> {
77        let view = AlignmentView::for_model(model)?;
78        let hierarchy = view.hierarchy(alignment)?;
79        let mut referents = hierarchy.referents;
80        for id in view.filter_is_a(hierarchy.positioned, "IfcReferent") {
81            if !referents.contains(&id) {
82                referents.push(id);
83            }
84        }
85        let mut equations = Vec::with_capacity(referents.len());
86        for referent in referents {
87            if let Some(equation) = resolve_referent_stationing(model, referent, units)? {
88                equations.push(equation);
89            }
90        }
91        Self::from_equations(alignment, equations)
92    }
93
94    fn from_equations(
95        alignment: EntityId,
96        mut equations: Vec<StationEquation>,
97    ) -> AlignmentResult<Self> {
98        for equation in &equations {
99            let finite = equation.distance_along.is_finite()
100                && equation.station.is_finite()
101                && equation.incoming_station.is_none_or(f64::is_finite);
102            if !finite {
103                return Err(AlignmentError::SemanticViolation {
104                    entity: Some(equation.referent),
105                    rule: "a stationing referent's distance and stations must be finite",
106                });
107            }
108        }
109        equations.sort_by(|a, b| a.distance_along.total_cmp(&b.distance_along));
110        for pair in equations.windows(2) {
111            let [previous, next] = pair else {
112                unreachable!("windows(2) yields pairs")
113            };
114            if next.distance_along - previous.distance_along <= STATION_TOLERANCE {
115                return Err(AlignmentError::SemanticViolation {
116                    entity: Some(next.referent),
117                    rule:
118                        "two stationing referents of one alignment sit at the same distance along",
119                });
120            }
121            let carried = station_in(previous, next.distance_along);
122            let stated = next.incoming_station.unwrap_or(next.station);
123            if (stated - carried).abs() > STATION_TOLERANCE {
124                return Err(AlignmentError::SemanticViolation {
125                    entity: Some(next.referent),
126                    rule: if next.incoming_station.is_some() {
127                        "IncomingStation disagrees with the station carried from the previous referent"
128                    } else {
129                        "a referent without IncomingStation must continue the previous station"
130                    },
131                });
132            }
133        }
134        Ok(Self {
135            alignment,
136            equations,
137        })
138    }
139
140    /// The stationing referents, ascending in distance along.
141    #[must_use]
142    pub fn equations(&self) -> &[StationEquation] {
143        &self.equations
144    }
145
146    /// The station at `distance_along` (metres along the alignment).
147    ///
148    /// At an equation the station ahead of it applies.
149    ///
150    /// # Errors
151    ///
152    /// [`AlignmentError::OutOfRange`] for a non-finite distance, an empty
153    /// table, or a distance before the first stationing referent.
154    pub fn station_at(&self, distance_along: f64) -> AlignmentResult<f64> {
155        let out_of_range = AlignmentError::OutOfRange {
156            entity: self.alignment,
157            quantity: "distance along",
158            value: distance_along,
159        };
160        if !distance_along.is_finite() {
161            return Err(out_of_range);
162        }
163        let governing = self
164            .equations
165            .iter()
166            .rev()
167            .find(|equation| equation.distance_along <= distance_along)
168            .ok_or(out_of_range)?;
169        Ok(station_in(governing, distance_along))
170    }
171
172    /// Every distance along labelled `station`, ascending.
173    ///
174    /// Usually one. Several when an equation steps the station back, so
175    /// that the same value occurs in two ranges.
176    ///
177    /// # Errors
178    ///
179    /// [`AlignmentError::OutOfRange`] for a non-finite station and a
180    /// station no range reaches.
181    pub fn distances_at(&self, station: f64) -> AlignmentResult<Vec<f64>> {
182        let out_of_range = AlignmentError::OutOfRange {
183            entity: self.alignment,
184            quantity: "station",
185            value: station,
186        };
187        if !station.is_finite() {
188            return Err(out_of_range);
189        }
190        let mut distances: Vec<f64> = Vec::new();
191        for (index, equation) in self.equations.iter().enumerate() {
192            let span = self.equations.get(index + 1).map_or(f64::INFINITY, |next| {
193                next.distance_along - equation.distance_along
194            });
195            let offset = (station - equation.station) * direction(equation);
196            let tolerance = 1e-9 * station.abs().max(1.0);
197            if offset < -tolerance || offset > span + tolerance {
198                continue;
199            }
200            let distance = equation.distance_along + offset.clamp(0.0, span);
201            let duplicate = distances
202                .last()
203                .is_some_and(|last| (distance - last).abs() <= tolerance);
204            if !duplicate {
205                distances.push(distance);
206            }
207        }
208        if distances.is_empty() {
209            return Err(out_of_range);
210        }
211        Ok(distances)
212    }
213
214    /// The one distance along labelled `station`.
215    ///
216    /// # Errors
217    ///
218    /// As [`Self::distances_at`], and [`AlignmentError::AmbiguousStation`]
219    /// when the station labels several distances.
220    pub fn distance_at(&self, station: f64) -> AlignmentResult<f64> {
221        let distances = self.distances_at(station)?;
222        match distances.as_slice() {
223            [only] => Ok(*only),
224            _ => Err(AlignmentError::AmbiguousStation {
225                alignment: self.alignment,
226                station,
227                distances,
228            }),
229        }
230    }
231}
232
233/// `+1` for increasing stationing, `-1` for decreasing.
234fn direction(equation: &StationEquation) -> f64 {
235    if equation.has_increasing_station {
236        1.0
237    } else {
238        -1.0
239    }
240}
241
242/// The station `equation`'s range assigns to `distance_along`.
243fn station_in(equation: &StationEquation, distance_along: f64) -> f64 {
244    equation.station + direction(equation) * (distance_along - equation.distance_along)
245}