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, Transaction, Value};
20
21pub use table::{ALL, IFCBRIDGE, IFCBRIDGEPART, IFCFACILITY, IFCFACILITYPARTCOMMON};
22pub use table::{IFCMARINEFACILITY, IFCMARINEPART, IFCRAILWAY, IFCRAILWAYPART};
23pub use table::{IFCROAD, IFCROADPART};
24
25/// One facility class and the slots that distinguish it.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Facility {
28    /// STEP type name, upper-case as stored.
29    pub type_name: &'static str,
30    /// Attribute count this class declares.
31    pub arity: usize,
32    /// Slot holding `PredefinedType`, when the class declares one.
33    pub predefined_slot: Option<usize>,
34    /// Slot holding the mandatory `UsageType`; parts only.
35    pub usage_slot: Option<usize>,
36    /// Permitted `PredefinedType` tokens for this exact class.
37    pub members: &'static [&'static str],
38}
39
40impl Facility {
41    /// Is this a facility part rather than a facility?
42    ///
43    /// Parts are the classes carrying a mandatory `UsageType`.
44    #[must_use]
45    pub const fn is_part(&self) -> bool {
46        self.usage_slot.is_some()
47    }
48}
49
50/// Why a facility was refused.
51#[derive(Debug, Clone, PartialEq, Eq)]
52#[non_exhaustive]
53pub enum FacilityError {
54    /// `GlobalId` is absent or not a 22-character IFC GUID.
55    MalformedGuid {
56        /// The class being authored.
57        entity: &'static str,
58        /// What the caller offered.
59        offered: String,
60    },
61    /// The token is not in this class's own enum.
62    UnknownToken {
63        /// The class being authored.
64        entity: &'static str,
65        /// Which attribute rejected it.
66        attribute: &'static str,
67        /// The offered token.
68        offered: String,
69    },
70    /// A class with no `PredefinedType` was offered one.
71    NoPredefinedType {
72        /// The class being authored.
73        entity: &'static str,
74    },
75    /// `UsageType` is mandatory on a part and was not supplied.
76    MissingUsageType {
77        /// The class being authored.
78        entity: &'static str,
79    },
80    /// `UsageType` was supplied for a class that declares none.
81    UnexpectedUsageType {
82        /// The class being authored.
83        entity: &'static str,
84    },
85    /// `USERDEFINED` was chosen without the name it promises.
86    UserDefinedWithoutObjectType {
87        /// The class being authored.
88        entity: &'static str,
89        /// Which attribute was `USERDEFINED`.
90        attribute: &'static str,
91    },
92}
93
94impl core::fmt::Display for FacilityError {
95    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
96        match self {
97            Self::MalformedGuid { entity, offered } => {
98                write!(f, "{entity}: GlobalId {offered:?} is not an IFC GUID")
99            }
100            Self::UnknownToken {
101                entity,
102                attribute,
103                offered,
104            } => {
105                write!(f, "{entity}: {offered:?} is not a {attribute} of {entity}")
106            }
107            Self::NoPredefinedType { entity } => {
108                write!(f, "{entity} declares no PredefinedType")
109            }
110            Self::MissingUsageType { entity } => {
111                write!(f, "{entity}: UsageType is mandatory on a facility part")
112            }
113            Self::UnexpectedUsageType { entity } => {
114                write!(f, "{entity} declares no UsageType")
115            }
116            Self::UserDefinedWithoutObjectType { entity, attribute } => {
117                write!(
118                    f,
119                    "{entity}: {attribute} is USERDEFINED without an ObjectType naming it"
120                )
121            }
122        }
123    }
124}
125
126impl std::error::Error for FacilityError {}
127
128/// Result of authoring a facility.
129pub type FacilityResult<T> = Result<T, FacilityError>;
130
131/// Attributes a facility shares with every spatial container.
132#[derive(Debug, Clone, Copy, Default)]
133pub struct FacilityDraft<'a> {
134    /// `IfcRoot.Name`, slot 2.
135    pub name: Option<&'a str>,
136    /// `IfcRoot.Description`, slot 3.
137    pub description: Option<&'a str>,
138    /// `IfcObject.ObjectType`, slot 4. Names a `USERDEFINED` token.
139    pub object_type: Option<&'a str>,
140    /// `IfcProduct.ObjectPlacement`, slot 5.
141    pub placement: Option<EntityId>,
142    /// `IfcSpatialElement.LongName`, slot 7.
143    pub long_name: Option<&'a str>,
144    /// `IfcSpatialStructureElement.CompositionType`, slot 8.
145    pub composition: Option<&'a str>,
146    /// `UsageType`, slot 9. Mandatory on a part, refused otherwise.
147    pub usage: Option<&'a str>,
148}
149
150const USAGE_TOKENS: &[&str] = &[
151    "LATERAL",
152    "LONGITUDINAL",
153    "REGION",
154    "VERTICAL",
155    "USERDEFINED",
156    "NOTDEFINED",
157];
158
159fn names_itself(draft: &FacilityDraft<'_>) -> bool {
160    draft.object_type.is_some_and(|s| !s.trim().is_empty())
161}
162
163/// Stage a facility or facility part.
164///
165/// `kind` carries its own slot layout and its own enum, so a part and a
166/// facility cannot be confused for one another.
167///
168/// # Errors
169///
170/// Refuses a malformed GlobalId; a `PredefinedType` or `UsageType` token
171/// outside this exact class's enum; a `PredefinedType` on a class that
172/// declares none; a missing `UsageType` on a part; a `UsageType` on a
173/// class that declares none; and `USERDEFINED` in either attribute
174/// without an `ObjectType` naming it.
175pub fn create_facility(
176    tx: &mut Transaction,
177    kind: Facility,
178    global_id: &str,
179    predefined_type: Option<&str>,
180    draft: FacilityDraft<'_>,
181) -> FacilityResult<EntityId> {
182    let entity = kind.type_name;
183    if Guid::parse(global_id).is_none() {
184        return Err(FacilityError::MalformedGuid {
185            entity,
186            offered: global_id.to_owned(),
187        });
188    }
189
190    let mut attributes = vec![Value::Null; kind.arity];
191    attributes[0] = Value::Text(global_id.into());
192    attributes[2] = text(draft.name);
193    attributes[3] = text(draft.description);
194    attributes[4] = text(draft.object_type);
195    attributes[5] = draft.placement.map_or(Value::Null, Value::Ref);
196    attributes[7] = text(draft.long_name);
197    attributes[8] = draft
198        .composition
199        .map_or(Value::Null, |t| Value::Enum(t.into()));
200
201    match (kind.usage_slot, draft.usage) {
202        (Some(slot), Some(token)) => {
203            if !USAGE_TOKENS.contains(&token) {
204                return Err(FacilityError::UnknownToken {
205                    entity,
206                    attribute: "UsageType",
207                    offered: token.to_owned(),
208                });
209            }
210            if token == "USERDEFINED" && !names_itself(&draft) {
211                return Err(FacilityError::UserDefinedWithoutObjectType {
212                    entity,
213                    attribute: "UsageType",
214                });
215            }
216            attributes[slot] = Value::Enum(token.into());
217        }
218        (Some(_), None) => return Err(FacilityError::MissingUsageType { entity }),
219        (None, Some(_)) => return Err(FacilityError::UnexpectedUsageType { entity }),
220        (None, None) => {}
221    }
222
223    match (kind.predefined_slot, predefined_type) {
224        (Some(slot), Some(token)) => {
225            if !kind.members.contains(&token) {
226                return Err(FacilityError::UnknownToken {
227                    entity,
228                    attribute: "PredefinedType",
229                    offered: token.to_owned(),
230                });
231            }
232            if token == "USERDEFINED" && !names_itself(&draft) {
233                return Err(FacilityError::UserDefinedWithoutObjectType {
234                    entity,
235                    attribute: "PredefinedType",
236                });
237            }
238            attributes[slot] = Value::Enum(token.into());
239        }
240        (None, Some(_)) => return Err(FacilityError::NoPredefinedType { entity }),
241        (Some(_), None) | (None, None) => {}
242    }
243
244    Ok(tx.create(Entity::new(entity, attributes)))
245}
246
247fn text(value: Option<&str>) -> Value {
248    value.map_or(Value::Null, |t| Value::Text(t.into()))
249}