ifc_alignment/referent/
station.rs1use ifc_model::value::Value;
12use ifc_model::{EntityId, Model};
13
14use crate::error::{AlignmentError, AlignmentResult};
15use crate::horizontal::AlignmentUnits;
16
17#[derive(Debug, Clone, PartialEq)]
24pub struct StationEquation {
25 pub referent: EntityId,
27 pub distance_along: f64,
30 pub incoming_station: Option<f64>,
35 pub station: f64,
38 pub has_increasing_station: bool,
41}
42
43pub fn station_equations(
51 model: &Model,
52 units: AlignmentUnits,
53) -> AlignmentResult<Vec<StationEquation>> {
54 let mut out = Vec::new();
55 for (id, entity) in model.iter() {
56 if !entity.is_type("IFCREFERENT") {
57 continue;
58 }
59 if let Some(equation) = resolve_referent_stationing(model, id, units)? {
60 out.push(equation);
61 }
62 }
63 out.sort_by(|a, b| {
67 a.distance_along
68 .partial_cmp(&b.distance_along)
69 .unwrap_or(std::cmp::Ordering::Equal)
70 });
71 Ok(out)
72}
73
74fn resolve_referent_stationing(
75 model: &Model,
76 referent: EntityId,
77 units: AlignmentUnits,
78) -> AlignmentResult<Option<StationEquation>> {
79 let Some(pset) = find_pset_stationing(model, referent)? else {
80 return Ok(None);
81 };
82 let (station, incoming_station, has_increasing_station) =
83 read_stationing_properties(model, pset, referent)?;
84 let distance_along = referent_distance_along(model, referent, units)?;
85
86 Ok(Some(StationEquation {
87 referent,
88 distance_along,
89 incoming_station: incoming_station.map(|v| v * units.length_to_metres),
90 station: station * units.length_to_metres,
91 has_increasing_station,
92 }))
93}
94
95fn find_pset_stationing(model: &Model, referent: EntityId) -> AlignmentResult<Option<EntityId>> {
98 for (_id, entity) in model.iter() {
99 if !entity.is_type("IFCRELDEFINESBYPROPERTIES") {
100 continue;
101 }
102 let values = &entity.attributes;
106 let related = values.get(4).and_then(Value::as_list).unwrap_or(&[]);
107 let targets_referent = related.iter().any(|v| v.as_ref_id() == Some(referent));
108 if !targets_referent {
109 continue;
110 }
111 let Some(pset_id) = values.get(5).and_then(Value::as_ref_id) else {
112 continue;
113 };
114 let Some(pset_entity) = model.get(pset_id) else {
115 continue;
116 };
117 if !pset_entity.is_type("IFCPROPERTYSET") {
118 continue;
119 }
120 let is_stationing = pset_entity
123 .attributes
124 .get(2)
125 .and_then(Value::as_text)
126 .is_some_and(|name| name == "Pset_Stationing");
127 if is_stationing {
128 return Ok(Some(pset_id));
129 }
130 }
131 Ok(None)
132}
133
134fn read_stationing_properties(
135 model: &Model,
136 pset: EntityId,
137 referent: EntityId,
138) -> AlignmentResult<(f64, Option<f64>, bool)> {
139 let pset_entity = model
140 .get(pset)
141 .ok_or(AlignmentError::MissingEntity { entity: pset })?;
142 let properties = pset_entity
143 .attributes
144 .get(4)
145 .and_then(Value::as_list)
146 .unwrap_or(&[]);
147
148 let mut station = None;
149 let mut incoming_station = None;
150 let mut has_increasing_station = true; for property in properties {
153 let Some(property_id) = property.as_ref_id() else {
154 continue;
155 };
156 let Some(property_entity) = model.get(property_id) else {
157 continue;
158 };
159 if !property_entity.is_type("IFCPROPERTYSINGLEVALUE") {
160 continue;
161 }
162 let name = property_entity
166 .attributes
167 .first()
168 .and_then(Value::as_text)
169 .ok_or(AlignmentError::InvalidAttribute {
170 entity: property_id,
171 index: 0,
172 name: "Name",
173 })?;
174 let nominal = property_entity.attributes.get(2);
175 match name {
176 "Station" => {
177 station = nominal.and_then(|v| v.unwrap_typed().as_f64());
178 }
179 "IncomingStation" => {
180 incoming_station = nominal.and_then(|v| v.unwrap_typed().as_f64());
181 }
182 "HasIncreasingStation" => {
183 if let Some(value) = nominal.and_then(|v| v.unwrap_typed().as_bool()) {
184 has_increasing_station = value;
185 }
186 }
187 _ => {}
188 }
189 }
190
191 let station = station.ok_or(AlignmentError::SemanticViolation {
192 entity: Some(referent),
193 rule: "Pset_Stationing.Station is required to interpret a station equation",
194 })?;
195 Ok((station, incoming_station, has_increasing_station))
196}
197
198fn referent_distance_along(
203 model: &Model,
204 referent: EntityId,
205 units: AlignmentUnits,
206) -> AlignmentResult<f64> {
207 let entity = model
208 .get(referent)
209 .ok_or(AlignmentError::MissingEntity { entity: referent })?;
210 let placement_id = entity.attributes.get(5).and_then(Value::as_ref_id).ok_or(
211 AlignmentError::InvalidAttribute {
212 entity: referent,
213 index: 5,
214 name: "ObjectPlacement",
215 },
216 )?;
217 let placement = crate::placement::resolve_linear_placement(model, placement_id, units)?;
218 match placement.relative_placement.distance_along {
219 crate::placement::CurveMeasure::Length(value) => Ok(value),
220 crate::placement::CurveMeasure::Parameter(_) => Err(AlignmentError::Unsupported {
221 entity: referent,
222 type_name: "IFCPOINTBYDISTANCEEXPRESSION".to_owned(),
223 detail: "a station equation requires an absolute DistanceAlong, not a curve parameter",
224 }),
225 }
226}