ifc_alignment/referent/
station.rs1use ifc_model::value::Value;
12use ifc_model::{EntityId, Model};
13
14use crate::error::{AlignmentError, AlignmentResult};
15use crate::horizontal::AlignmentUnits;
16use crate::slot;
17
18#[derive(Debug, Clone, PartialEq)]
25#[non_exhaustive]
26pub struct StationEquation {
27 pub referent: EntityId,
29 pub distance_along: f64,
32 pub incoming_station: Option<f64>,
37 pub station: f64,
40 pub has_increasing_station: bool,
43}
44
45#[deprecated(
57 note = "mixes the referents of every alignment; use `Stationing::resolve` for one alignment"
58)]
59pub fn station_equations(
60 model: &Model,
61 units: AlignmentUnits,
62) -> AlignmentResult<Vec<StationEquation>> {
63 let mut out = Vec::new();
64 for (id, entity) in model.iter() {
65 if !entity.is_type("IFCREFERENT") {
66 continue;
67 }
68 if let Some(equation) = resolve_referent_stationing(model, id, units)? {
69 out.push(equation);
70 }
71 }
72 out.sort_by(|a, b| {
76 a.distance_along
77 .partial_cmp(&b.distance_along)
78 .unwrap_or(std::cmp::Ordering::Equal)
79 });
80 Ok(out)
81}
82
83pub(crate) fn resolve_referent_stationing(
86 model: &Model,
87 referent: EntityId,
88 units: AlignmentUnits,
89) -> AlignmentResult<Option<StationEquation>> {
90 let Some(pset) = find_pset_stationing(model, referent)? else {
91 return Ok(None);
92 };
93 let (station, incoming_station, has_increasing_station) =
94 read_stationing_properties(model, pset, referent)?;
95 let distance_along = referent_distance_along(model, referent, units)?;
96
97 Ok(Some(StationEquation {
98 referent,
99 distance_along,
100 incoming_station: incoming_station.map(|v| v * units.length_to_metres),
101 station: station * units.length_to_metres,
102 has_increasing_station,
103 }))
104}
105
106fn find_pset_stationing(model: &Model, referent: EntityId) -> AlignmentResult<Option<EntityId>> {
109 for (_id, entity) in model.iter() {
110 if !entity.is_type("IFCRELDEFINESBYPROPERTIES") {
111 continue;
112 }
113 let values = &entity.attributes;
117 let related = values
118 .get(slot::rel_defines_by_properties::RELATED_OBJECTS)
119 .and_then(Value::as_list)
120 .unwrap_or(&[]);
121 let targets_referent = related.iter().any(|v| v.as_ref_id() == Some(referent));
122 if !targets_referent {
123 continue;
124 }
125 let Some(pset_id) = values
126 .get(slot::rel_defines_by_properties::RELATING_PROPERTY_DEFINITION)
127 .and_then(Value::as_ref_id)
128 else {
129 continue;
130 };
131 let Some(pset_entity) = model.get(pset_id) else {
132 continue;
133 };
134 if !pset_entity.is_type("IFCPROPERTYSET") {
135 continue;
136 }
137 let is_stationing = pset_entity
140 .attributes
141 .get(slot::property_set::NAME)
142 .and_then(Value::as_text)
143 .is_some_and(|name| name == "Pset_Stationing");
144 if is_stationing {
145 return Ok(Some(pset_id));
146 }
147 }
148 Ok(None)
149}
150
151fn read_stationing_properties(
152 model: &Model,
153 pset: EntityId,
154 referent: EntityId,
155) -> AlignmentResult<(f64, Option<f64>, bool)> {
156 let pset_entity = model
157 .get(pset)
158 .ok_or(AlignmentError::MissingEntity { entity: pset })?;
159 let properties = pset_entity
160 .attributes
161 .get(slot::property_set::HAS_PROPERTIES)
162 .and_then(Value::as_list)
163 .unwrap_or(&[]);
164
165 let mut station = None;
166 let mut incoming_station = None;
167 let mut has_increasing_station = true; for property in properties {
170 let Some(property_id) = property.as_ref_id() else {
171 continue;
172 };
173 let Some(property_entity) = model.get(property_id) else {
174 continue;
175 };
176 if !property_entity.is_type("IFCPROPERTYSINGLEVALUE") {
177 continue;
178 }
179 let name = property_entity
183 .attributes
184 .get(slot::property_single_value::NAME)
185 .and_then(Value::as_text)
186 .ok_or(AlignmentError::InvalidAttribute {
187 entity: property_id,
188 index: slot::property_single_value::NAME,
189 name: "Name",
190 })?;
191 let nominal = property_entity
192 .attributes
193 .get(slot::property_single_value::NOMINAL_VALUE);
194 match name {
195 "Station" => {
196 station = nominal.and_then(|v| v.unwrap_typed().as_f64());
197 }
198 "IncomingStation" => {
199 incoming_station = nominal.and_then(|v| v.unwrap_typed().as_f64());
200 }
201 "HasIncreasingStation" => {
202 if let Some(value) = nominal.and_then(|v| v.unwrap_typed().as_bool()) {
203 has_increasing_station = value;
204 }
205 }
206 _ => {}
207 }
208 }
209
210 let station = station.ok_or(AlignmentError::SemanticViolation {
211 entity: Some(referent),
212 rule: "Pset_Stationing.Station is required to interpret a station equation",
213 })?;
214 Ok((station, incoming_station, has_increasing_station))
215}
216
217fn referent_distance_along(
222 model: &Model,
223 referent: EntityId,
224 units: AlignmentUnits,
225) -> AlignmentResult<f64> {
226 let entity = model
227 .get(referent)
228 .ok_or(AlignmentError::MissingEntity { entity: referent })?;
229 let placement_id = entity
230 .attributes
231 .get(slot::product::OBJECT_PLACEMENT)
232 .and_then(Value::as_ref_id)
233 .ok_or(AlignmentError::InvalidAttribute {
234 entity: referent,
235 index: slot::product::OBJECT_PLACEMENT,
236 name: "ObjectPlacement",
237 })?;
238 let placement = crate::placement::resolve_linear_placement(model, placement_id, units)?;
239 match placement.relative_placement.distance_along {
240 crate::placement::CurveMeasure::Length(value) => Ok(value),
241 crate::placement::CurveMeasure::Parameter(_) => Err(AlignmentError::Unsupported {
242 entity: referent,
243 type_name: "IFCPOINTBYDISTANCEEXPRESSION".to_owned(),
244 detail: "a station equation requires an absolute DistanceAlong, not a curve parameter",
245 }),
246 }
247}