Skip to main content

ifc_spatial/facility/
mod.rs

1//! Facilities and facility parts: the non-building spatial containers.
2//!
3//! `IfcBuilding` is one facility among several. IFC4X3 added bridges,
4//! roads, railways and marine facilities as siblings, each with its own
5//! `PredefinedType` enum, and a parallel family of parts.
6//!
7//! Two slot layouts, and the difference is not cosmetic:
8//!
9//! * A facility carries an optional `PredefinedType` at slot 9.
10//! * A part carries a **mandatory** `UsageType` at 9 and pushes
11//!   `PredefinedType` to 10.
12//!
13//! Writing a part as though it were a facility puts the usage token in
14//! the predefined slot, which parses and means something else.
15
16mod 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/// One facility class and the slots that distinguish it.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct Facility {
32    /// STEP type name, upper-case as stored.
33    pub type_name: &'static str,
34    /// Attribute count this class declares.
35    pub arity: usize,
36    /// Slot holding `PredefinedType`, when the class declares one.
37    pub predefined_slot: Option<usize>,
38    /// Slot holding the mandatory `UsageType`; parts only.
39    pub usage_slot: Option<usize>,
40    /// Permitted `PredefinedType` tokens for this exact class.
41    pub members: &'static [&'static str],
42}
43
44impl Facility {
45    /// Is this a facility part rather than a facility?
46    ///
47    /// Parts are the classes carrying a mandatory `UsageType`.
48    #[must_use]
49    pub const fn is_part(&self) -> bool {
50        self.usage_slot.is_some()
51    }
52}
53
54/// Why a facility was refused.
55#[derive(Debug, Clone, PartialEq, Eq)]
56#[non_exhaustive]
57pub enum FacilityError {
58    /// `GlobalId` is absent or not a 22-character IFC GUID.
59    MalformedGuid {
60        /// The class being authored.
61        entity: &'static str,
62        /// What the caller offered.
63        offered: String,
64    },
65    /// The token is not in this class's own enum.
66    UnknownToken {
67        /// The class being authored.
68        entity: &'static str,
69        /// Which attribute rejected it.
70        attribute: &'static str,
71        /// The offered token.
72        offered: String,
73    },
74    /// A class with no `PredefinedType` was offered one.
75    NoPredefinedType {
76        /// The class being authored.
77        entity: &'static str,
78    },
79    /// `UsageType` is mandatory on a part and was not supplied.
80    MissingUsageType {
81        /// The class being authored.
82        entity: &'static str,
83    },
84    /// `UsageType` was supplied for a class that declares none.
85    UnexpectedUsageType {
86        /// The class being authored.
87        entity: &'static str,
88    },
89    /// `USERDEFINED` was chosen without the name it promises.
90    UserDefinedWithoutObjectType {
91        /// The class being authored.
92        entity: &'static str,
93        /// Which attribute was `USERDEFINED`.
94        attribute: &'static str,
95    },
96    /// Refused against the model's declared release or its owner history,
97    /// by [`create_facility_with_owner_history`] (#202).
98    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
136/// Result of authoring a facility.
137pub type FacilityResult<T> = Result<T, FacilityError>;
138
139/// Attributes a facility shares with every spatial container.
140///
141/// `#[non_exhaustive]`: build it with [`FacilityDraft::new`] and the
142/// setters.
143#[derive(Debug, Clone, Copy, Default)]
144#[non_exhaustive]
145pub struct FacilityDraft<'a> {
146    /// `IfcRoot.Name`, slot 2.
147    pub name: Option<&'a str>,
148    /// `IfcRoot.Description`, slot 3.
149    pub description: Option<&'a str>,
150    /// `IfcObject.ObjectType`, slot 4. Names a `USERDEFINED` token.
151    pub object_type: Option<&'a str>,
152    /// `IfcProduct.ObjectPlacement`, slot 5.
153    pub placement: Option<EntityId>,
154    /// `IfcSpatialElement.LongName`, slot 7.
155    pub long_name: Option<&'a str>,
156    /// `IfcSpatialStructureElement.CompositionType`, slot 8.
157    pub composition: Option<&'a str>,
158    /// `UsageType`, slot 9. Mandatory on a part, refused otherwise.
159    pub usage: Option<&'a str>,
160}
161
162impl<'a> FacilityDraft<'a> {
163    /// An empty draft: every attribute unset.
164    #[must_use]
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    /// Set `IfcRoot.Name`.
170    #[must_use]
171    pub fn name(mut self, value: &'a str) -> Self {
172        self.name = Some(value);
173        self
174    }
175
176    /// Set `IfcRoot.Description`.
177    #[must_use]
178    pub fn description(mut self, value: &'a str) -> Self {
179        self.description = Some(value);
180        self
181    }
182
183    /// Set `IfcObject.ObjectType`, which names a `USERDEFINED` token.
184    #[must_use]
185    pub fn object_type(mut self, value: &'a str) -> Self {
186        self.object_type = Some(value);
187        self
188    }
189
190    /// Set `IfcProduct.ObjectPlacement`.
191    #[must_use]
192    pub fn placement(mut self, value: EntityId) -> Self {
193        self.placement = Some(value);
194        self
195    }
196
197    /// Set `LongName`.
198    #[must_use]
199    pub fn long_name(mut self, value: &'a str) -> Self {
200        self.long_name = Some(value);
201        self
202    }
203
204    /// Set `CompositionType`, an `IfcElementCompositionEnum` token.
205    #[must_use]
206    pub fn composition(mut self, value: &'a str) -> Self {
207        self.composition = Some(value);
208        self
209    }
210
211    /// Set `UsageType`; mandatory on a facility part, refused otherwise.
212    #[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
232/// Stage a facility or facility part.
233///
234/// `kind` carries its own slot layout and its own enum, so a part and a
235/// facility cannot be confused for one another.
236///
237/// IFC4X3 only, as are the classes: it writes the IFC4X3 layout with
238/// `OwnerHistory` `$`. [`create_facility_with_owner_history`] binds the
239/// model's declared release and takes an `IfcOwnerHistory`.
240///
241/// # Errors
242///
243/// Refuses a malformed GlobalId; a `PredefinedType` or `UsageType` token
244/// outside this exact class's enum; a `PredefinedType` on a class that
245/// declares none; a missing `UsageType` on a part; a `UsageType` on a
246/// class that declares none; and `USERDEFINED` in either attribute
247/// without an `ObjectType` naming it.
248pub 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
273/// [`create_facility`] in the model's declared release, with a
274/// caller-supplied `IfcOwnerHistory` (#202).
275///
276/// The record is laid out by attribute name from the release's table.
277/// Only IFC4X3 declares the facility classes, so an IFC2X3 or IFC4 model
278/// is refused with `EntityNotInSchema`.
279///
280/// # Errors
281///
282/// Those of [`create_facility`], and [`FacilityError::Authoring`] wrapping
283/// the release and owner-history refusals of
284/// [`aggregate_with_owner_history`](crate::aggregate_with_owner_history).
285/// Nothing is staged on an error.
286pub 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
313/// The checks of [`create_facility`].
314fn 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}