1mod table;
17
18use ifc_model::guid::Guid;
19use ifc_model::{Entity, EntityId, Model, Transaction, Value};
20
21use crate::authoring::release::stage;
22use crate::authoring::SpatialAuthoringError;
23
24pub use table::{ALL, IFCBRIDGE, IFCBRIDGEPART, IFCFACILITY, IFCFACILITYPARTCOMMON};
25pub use table::{IFCMARINEFACILITY, IFCMARINEPART, IFCRAILWAY, IFCRAILWAYPART};
26pub use table::{IFCROAD, IFCROADPART};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct Facility {
32 pub type_name: &'static str,
34 pub arity: usize,
36 pub predefined_slot: Option<usize>,
38 pub usage_slot: Option<usize>,
40 pub members: &'static [&'static str],
42}
43
44impl Facility {
45 #[must_use]
49 pub const fn is_part(&self) -> bool {
50 self.usage_slot.is_some()
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56#[non_exhaustive]
57pub enum FacilityError {
58 MalformedGuid {
60 entity: &'static str,
62 offered: String,
64 },
65 UnknownToken {
67 entity: &'static str,
69 attribute: &'static str,
71 offered: String,
73 },
74 NoPredefinedType {
76 entity: &'static str,
78 },
79 MissingUsageType {
81 entity: &'static str,
83 },
84 UnexpectedUsageType {
86 entity: &'static str,
88 },
89 UserDefinedWithoutObjectType {
91 entity: &'static str,
93 attribute: &'static str,
95 },
96 Authoring(SpatialAuthoringError),
99}
100
101impl core::fmt::Display for FacilityError {
102 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103 match self {
104 Self::MalformedGuid { entity, offered } => {
105 write!(f, "{entity}: GlobalId {offered:?} is not an IFC GUID")
106 }
107 Self::UnknownToken {
108 entity,
109 attribute,
110 offered,
111 } => {
112 write!(f, "{entity}: {offered:?} is not a {attribute} of {entity}")
113 }
114 Self::NoPredefinedType { entity } => {
115 write!(f, "{entity} declares no PredefinedType")
116 }
117 Self::MissingUsageType { entity } => {
118 write!(f, "{entity}: UsageType is mandatory on a facility part")
119 }
120 Self::UnexpectedUsageType { entity } => {
121 write!(f, "{entity} declares no UsageType")
122 }
123 Self::Authoring(error) => error.fmt(f),
124 Self::UserDefinedWithoutObjectType { entity, attribute } => {
125 write!(
126 f,
127 "{entity}: {attribute} is USERDEFINED without an ObjectType naming it"
128 )
129 }
130 }
131 }
132}
133
134impl std::error::Error for FacilityError {}
135
136pub type FacilityResult<T> = Result<T, FacilityError>;
138
139#[derive(Debug, Clone, Copy, Default)]
144#[non_exhaustive]
145pub struct FacilityDraft<'a> {
146 pub name: Option<&'a str>,
148 pub description: Option<&'a str>,
150 pub object_type: Option<&'a str>,
152 pub placement: Option<EntityId>,
154 pub long_name: Option<&'a str>,
156 pub composition: Option<&'a str>,
158 pub usage: Option<&'a str>,
160}
161
162impl<'a> FacilityDraft<'a> {
163 #[must_use]
165 pub fn new() -> Self {
166 Self::default()
167 }
168
169 #[must_use]
171 pub fn name(mut self, value: &'a str) -> Self {
172 self.name = Some(value);
173 self
174 }
175
176 #[must_use]
178 pub fn description(mut self, value: &'a str) -> Self {
179 self.description = Some(value);
180 self
181 }
182
183 #[must_use]
185 pub fn object_type(mut self, value: &'a str) -> Self {
186 self.object_type = Some(value);
187 self
188 }
189
190 #[must_use]
192 pub fn placement(mut self, value: EntityId) -> Self {
193 self.placement = Some(value);
194 self
195 }
196
197 #[must_use]
199 pub fn long_name(mut self, value: &'a str) -> Self {
200 self.long_name = Some(value);
201 self
202 }
203
204 #[must_use]
206 pub fn composition(mut self, value: &'a str) -> Self {
207 self.composition = Some(value);
208 self
209 }
210
211 #[must_use]
213 pub fn usage(mut self, value: &'a str) -> Self {
214 self.usage = Some(value);
215 self
216 }
217}
218
219const USAGE_TOKENS: &[&str] = &[
220 "LATERAL",
221 "LONGITUDINAL",
222 "REGION",
223 "VERTICAL",
224 "USERDEFINED",
225 "NOTDEFINED",
226];
227
228fn names_itself(draft: &FacilityDraft<'_>) -> bool {
229 draft.object_type.is_some_and(|s| !s.trim().is_empty())
230}
231
232pub fn create_facility(
249 tx: &mut Transaction,
250 kind: Facility,
251 global_id: &str,
252 predefined_type: Option<&str>,
253 draft: FacilityDraft<'_>,
254) -> FacilityResult<EntityId> {
255 check_facility(kind, global_id, predefined_type, &draft)?;
256 let mut attributes = vec![Value::Null; kind.arity];
257 attributes[0] = Value::Text(global_id.into());
258 attributes[2] = text(draft.name);
259 attributes[3] = text(draft.description);
260 attributes[4] = text(draft.object_type);
261 attributes[5] = draft.placement.map_or(Value::Null, Value::Ref);
262 attributes[7] = text(draft.long_name);
263 attributes[8] = enumeration(draft.composition);
264 if let Some(slot) = kind.usage_slot {
265 attributes[slot] = enumeration(draft.usage);
266 }
267 if let Some(slot) = kind.predefined_slot {
268 attributes[slot] = enumeration(predefined_type);
269 }
270 Ok(tx.create(Entity::new(kind.type_name, attributes)))
271}
272
273pub fn create_facility_with_owner_history(
287 tx: &mut Transaction,
288 model: &Model,
289 kind: Facility,
290 global_id: &str,
291 predefined_type: Option<&str>,
292 draft: FacilityDraft<'_>,
293 owner_history: EntityId,
294) -> FacilityResult<EntityId> {
295 check_facility(kind, global_id, predefined_type, &draft)?;
296 let values = vec![
297 ("GlobalId", Value::Text(global_id.into())),
298 ("Name", text(draft.name)),
299 ("Description", text(draft.description)),
300 ("ObjectType", text(draft.object_type)),
301 (
302 "ObjectPlacement",
303 draft.placement.map_or(Value::Null, Value::Ref),
304 ),
305 ("LongName", text(draft.long_name)),
306 ("CompositionType", enumeration(draft.composition)),
307 ("UsageType", enumeration(draft.usage)),
308 ("PredefinedType", enumeration(predefined_type)),
309 ];
310 stage(tx, model, kind.type_name, values, Some(owner_history)).map_err(FacilityError::Authoring)
311}
312
313fn check_facility(
315 kind: Facility,
316 global_id: &str,
317 predefined_type: Option<&str>,
318 draft: &FacilityDraft<'_>,
319) -> FacilityResult<()> {
320 let entity = kind.type_name;
321 if Guid::parse(global_id).is_none() {
322 return Err(FacilityError::MalformedGuid {
323 entity,
324 offered: global_id.to_owned(),
325 });
326 }
327 match (kind.usage_slot, draft.usage) {
328 (Some(_), Some(token)) => {
329 if !USAGE_TOKENS.contains(&token) {
330 return Err(FacilityError::UnknownToken {
331 entity,
332 attribute: "UsageType",
333 offered: token.to_owned(),
334 });
335 }
336 if token == "USERDEFINED" && !names_itself(draft) {
337 return Err(FacilityError::UserDefinedWithoutObjectType {
338 entity,
339 attribute: "UsageType",
340 });
341 }
342 }
343 (Some(_), None) => return Err(FacilityError::MissingUsageType { entity }),
344 (None, Some(_)) => return Err(FacilityError::UnexpectedUsageType { entity }),
345 (None, None) => {}
346 }
347 match (kind.predefined_slot, predefined_type) {
348 (Some(_), Some(token)) => {
349 if !kind.members.contains(&token) {
350 return Err(FacilityError::UnknownToken {
351 entity,
352 attribute: "PredefinedType",
353 offered: token.to_owned(),
354 });
355 }
356 if token == "USERDEFINED" && !names_itself(draft) {
357 return Err(FacilityError::UserDefinedWithoutObjectType {
358 entity,
359 attribute: "PredefinedType",
360 });
361 }
362 }
363 (None, Some(_)) => return Err(FacilityError::NoPredefinedType { entity }),
364 (Some(_), None) | (None, None) => {}
365 }
366 Ok(())
367}
368
369fn text(value: Option<&str>) -> Value {
370 value.map_or(Value::Null, |t| Value::Text(t.into()))
371}
372
373fn enumeration(value: Option<&str>) -> Value {
374 value.map_or(Value::Null, |t| Value::Enum(t.into()))
375}