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)]
30pub struct Facility {
31    /// STEP type name, upper-case as stored.
32    pub type_name: &'static str,
33    /// Attribute count this class declares.
34    pub arity: usize,
35    /// Slot holding `PredefinedType`, when the class declares one.
36    pub predefined_slot: Option<usize>,
37    /// Slot holding the mandatory `UsageType`; parts only.
38    pub usage_slot: Option<usize>,
39    /// Permitted `PredefinedType` tokens for this exact class.
40    pub members: &'static [&'static str],
41}
42
43impl Facility {
44    /// Is this a facility part rather than a facility?
45    ///
46    /// Parts are the classes carrying a mandatory `UsageType`.
47    #[must_use]
48    pub const fn is_part(&self) -> bool {
49        self.usage_slot.is_some()
50    }
51}
52
53/// Why a facility was refused.
54#[derive(Debug, Clone, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum FacilityError {
57    /// `GlobalId` is absent or not a 22-character IFC GUID.
58    MalformedGuid {
59        /// The class being authored.
60        entity: &'static str,
61        /// What the caller offered.
62        offered: String,
63    },
64    /// The token is not in this class's own enum.
65    UnknownToken {
66        /// The class being authored.
67        entity: &'static str,
68        /// Which attribute rejected it.
69        attribute: &'static str,
70        /// The offered token.
71        offered: String,
72    },
73    /// A class with no `PredefinedType` was offered one.
74    NoPredefinedType {
75        /// The class being authored.
76        entity: &'static str,
77    },
78    /// `UsageType` is mandatory on a part and was not supplied.
79    MissingUsageType {
80        /// The class being authored.
81        entity: &'static str,
82    },
83    /// `UsageType` was supplied for a class that declares none.
84    UnexpectedUsageType {
85        /// The class being authored.
86        entity: &'static str,
87    },
88    /// `USERDEFINED` was chosen without the name it promises.
89    UserDefinedWithoutObjectType {
90        /// The class being authored.
91        entity: &'static str,
92        /// Which attribute was `USERDEFINED`.
93        attribute: &'static str,
94    },
95    /// Refused against the model's declared release or its owner history,
96    /// by [`create_facility_with_owner_history`] (#202).
97    Authoring(SpatialAuthoringError),
98}
99
100impl core::fmt::Display for FacilityError {
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        match self {
103            Self::MalformedGuid { entity, offered } => {
104                write!(f, "{entity}: GlobalId {offered:?} is not an IFC GUID")
105            }
106            Self::UnknownToken {
107                entity,
108                attribute,
109                offered,
110            } => {
111                write!(f, "{entity}: {offered:?} is not a {attribute} of {entity}")
112            }
113            Self::NoPredefinedType { entity } => {
114                write!(f, "{entity} declares no PredefinedType")
115            }
116            Self::MissingUsageType { entity } => {
117                write!(f, "{entity}: UsageType is mandatory on a facility part")
118            }
119            Self::UnexpectedUsageType { entity } => {
120                write!(f, "{entity} declares no UsageType")
121            }
122            Self::Authoring(error) => error.fmt(f),
123            Self::UserDefinedWithoutObjectType { entity, attribute } => {
124                write!(
125                    f,
126                    "{entity}: {attribute} is USERDEFINED without an ObjectType naming it"
127                )
128            }
129        }
130    }
131}
132
133impl std::error::Error for FacilityError {}
134
135/// Result of authoring a facility.
136pub type FacilityResult<T> = Result<T, FacilityError>;
137
138/// Attributes a facility shares with every spatial container.
139#[derive(Debug, Clone, Copy, Default)]
140pub struct FacilityDraft<'a> {
141    /// `IfcRoot.Name`, slot 2.
142    pub name: Option<&'a str>,
143    /// `IfcRoot.Description`, slot 3.
144    pub description: Option<&'a str>,
145    /// `IfcObject.ObjectType`, slot 4. Names a `USERDEFINED` token.
146    pub object_type: Option<&'a str>,
147    /// `IfcProduct.ObjectPlacement`, slot 5.
148    pub placement: Option<EntityId>,
149    /// `IfcSpatialElement.LongName`, slot 7.
150    pub long_name: Option<&'a str>,
151    /// `IfcSpatialStructureElement.CompositionType`, slot 8.
152    pub composition: Option<&'a str>,
153    /// `UsageType`, slot 9. Mandatory on a part, refused otherwise.
154    pub usage: Option<&'a str>,
155}
156
157const USAGE_TOKENS: &[&str] = &[
158    "LATERAL",
159    "LONGITUDINAL",
160    "REGION",
161    "VERTICAL",
162    "USERDEFINED",
163    "NOTDEFINED",
164];
165
166fn names_itself(draft: &FacilityDraft<'_>) -> bool {
167    draft.object_type.is_some_and(|s| !s.trim().is_empty())
168}
169
170/// Stage a facility or facility part.
171///
172/// `kind` carries its own slot layout and its own enum, so a part and a
173/// facility cannot be confused for one another.
174///
175/// IFC4X3 only, as are the classes: it writes the IFC4X3 layout with
176/// `OwnerHistory` `$`. [`create_facility_with_owner_history`] binds the
177/// model's declared release and takes an `IfcOwnerHistory`.
178///
179/// # Errors
180///
181/// Refuses a malformed GlobalId; a `PredefinedType` or `UsageType` token
182/// outside this exact class's enum; a `PredefinedType` on a class that
183/// declares none; a missing `UsageType` on a part; a `UsageType` on a
184/// class that declares none; and `USERDEFINED` in either attribute
185/// without an `ObjectType` naming it.
186pub fn create_facility(
187    tx: &mut Transaction,
188    kind: Facility,
189    global_id: &str,
190    predefined_type: Option<&str>,
191    draft: FacilityDraft<'_>,
192) -> FacilityResult<EntityId> {
193    check_facility(kind, global_id, predefined_type, &draft)?;
194    let mut attributes = vec![Value::Null; kind.arity];
195    attributes[0] = Value::Text(global_id.into());
196    attributes[2] = text(draft.name);
197    attributes[3] = text(draft.description);
198    attributes[4] = text(draft.object_type);
199    attributes[5] = draft.placement.map_or(Value::Null, Value::Ref);
200    attributes[7] = text(draft.long_name);
201    attributes[8] = enumeration(draft.composition);
202    if let Some(slot) = kind.usage_slot {
203        attributes[slot] = enumeration(draft.usage);
204    }
205    if let Some(slot) = kind.predefined_slot {
206        attributes[slot] = enumeration(predefined_type);
207    }
208    Ok(tx.create(Entity::new(kind.type_name, attributes)))
209}
210
211/// [`create_facility`] in the model's declared release, with a
212/// caller-supplied `IfcOwnerHistory` (#202).
213///
214/// The record is laid out by attribute name from the release's table.
215/// Only IFC4X3 declares the facility classes, so an IFC2X3 or IFC4 model
216/// is refused with `EntityNotInSchema`.
217///
218/// # Errors
219///
220/// Those of [`create_facility`], and [`FacilityError::Authoring`] wrapping
221/// the release and owner-history refusals of
222/// [`aggregate_with_owner_history`](crate::aggregate_with_owner_history).
223/// Nothing is staged on an error.
224pub fn create_facility_with_owner_history(
225    tx: &mut Transaction,
226    model: &Model,
227    kind: Facility,
228    global_id: &str,
229    predefined_type: Option<&str>,
230    draft: FacilityDraft<'_>,
231    owner_history: EntityId,
232) -> FacilityResult<EntityId> {
233    check_facility(kind, global_id, predefined_type, &draft)?;
234    let values = vec![
235        ("GlobalId", Value::Text(global_id.into())),
236        ("Name", text(draft.name)),
237        ("Description", text(draft.description)),
238        ("ObjectType", text(draft.object_type)),
239        (
240            "ObjectPlacement",
241            draft.placement.map_or(Value::Null, Value::Ref),
242        ),
243        ("LongName", text(draft.long_name)),
244        ("CompositionType", enumeration(draft.composition)),
245        ("UsageType", enumeration(draft.usage)),
246        ("PredefinedType", enumeration(predefined_type)),
247    ];
248    stage(tx, model, kind.type_name, values, Some(owner_history)).map_err(FacilityError::Authoring)
249}
250
251/// The checks of [`create_facility`].
252fn check_facility(
253    kind: Facility,
254    global_id: &str,
255    predefined_type: Option<&str>,
256    draft: &FacilityDraft<'_>,
257) -> FacilityResult<()> {
258    let entity = kind.type_name;
259    if Guid::parse(global_id).is_none() {
260        return Err(FacilityError::MalformedGuid {
261            entity,
262            offered: global_id.to_owned(),
263        });
264    }
265    match (kind.usage_slot, draft.usage) {
266        (Some(_), Some(token)) => {
267            if !USAGE_TOKENS.contains(&token) {
268                return Err(FacilityError::UnknownToken {
269                    entity,
270                    attribute: "UsageType",
271                    offered: token.to_owned(),
272                });
273            }
274            if token == "USERDEFINED" && !names_itself(draft) {
275                return Err(FacilityError::UserDefinedWithoutObjectType {
276                    entity,
277                    attribute: "UsageType",
278                });
279            }
280        }
281        (Some(_), None) => return Err(FacilityError::MissingUsageType { entity }),
282        (None, Some(_)) => return Err(FacilityError::UnexpectedUsageType { entity }),
283        (None, None) => {}
284    }
285    match (kind.predefined_slot, predefined_type) {
286        (Some(_), Some(token)) => {
287            if !kind.members.contains(&token) {
288                return Err(FacilityError::UnknownToken {
289                    entity,
290                    attribute: "PredefinedType",
291                    offered: token.to_owned(),
292                });
293            }
294            if token == "USERDEFINED" && !names_itself(draft) {
295                return Err(FacilityError::UserDefinedWithoutObjectType {
296                    entity,
297                    attribute: "PredefinedType",
298                });
299            }
300        }
301        (None, Some(_)) => return Err(FacilityError::NoPredefinedType { entity }),
302        (Some(_), None) | (None, None) => {}
303    }
304    Ok(())
305}
306
307fn text(value: Option<&str>) -> Value {
308    value.map_or(Value::Null, |t| Value::Text(t.into()))
309}
310
311fn enumeration(value: Option<&str>) -> Value {
312    value.map_or(Value::Null, |t| Value::Enum(t.into()))
313}