Skip to main content

ifc_systems/authoring/
system_kind.rs

1//! The three named system classifications.
2//!
3//! # Why these are not `create_system`
4//!
5//! `IfcSystem` is the unclassified group. These three add a
6//! `PredefinedType` naming what kind of system it is, and they do not
7//! agree on where it sits: `IfcBuildingSystem` and `IfcBuiltSystem`
8//! put it at slot 5 with `LongName` at 6, while
9//! `IfcDistributionCircuit` puts `LongName` at 5 and the type at 6.
10//! Writing one layout for all three files a circuit's name into its
11//! classification slot, producing a system whose kind is its name.
12//!
13//! # CorrectPredefinedType
14//!
15//! `IfcBuildingSystem` and `IfcBuiltSystem` both state:
16//!
17//! ```text
18//! NOT(EXISTS(PredefinedType)) OR (PredefinedType <> USERDEFINED) OR
19//! ((PredefinedType = USERDEFINED) AND EXISTS(SELF\\IfcObject.ObjectType))
20//! ```
21//!
22//! `USERDEFINED` says the enum has no token for this kind and the name
23//! is given as `ObjectType`. Without it the record asserts a name
24//! exists and then withholds it.
25
26use ifc_model::guid::Guid;
27use ifc_model::{Entity, EntityId, Model, Transaction, Value};
28use ifc_schema::Schema;
29
30use super::release::bind;
31use super::{invalid, SystemAuthoringResult};
32
33/// `IfcBuildingSystemTypeEnum`, IFC4 and IFC4X3.
34const BUILDING: &[&str] = &[
35    "FENESTRATION",
36    "FOUNDATION",
37    "LOADBEARING",
38    "OUTERSHELL",
39    "SHADING",
40    "TRANSPORT",
41    "USERDEFINED",
42    "NOTDEFINED",
43];
44
45/// `IfcBuiltSystemTypeEnum`, IFC4X3 only.
46///
47/// A superset of the building-system tokens: IFC4X3 renamed the
48/// entity and widened the list to cover civil works.
49const BUILT: &[&str] = &[
50    "EROSIONPREVENTION",
51    "FENESTRATION",
52    "FOUNDATION",
53    "LOADBEARING",
54    "MOORING",
55    "OUTERSHELL",
56    "PRESTRESSING",
57    "RAILWAYLINE",
58    "RAILWAYTRACK",
59    "REINFORCING",
60    "SHADING",
61    "TRACKCIRCUIT",
62    "TRANSPORT",
63    "USERDEFINED",
64    "NOTDEFINED",
65];
66
67/// `IfcDistributionSystemEnum`.
68///
69/// The IFC4X3 list, a superset of IFC4's. Accepting the superset on
70/// both schemas would let an IFC4 file name a system kind IFC4 does
71/// not declare, so the schema in hand selects the slice.
72const DISTRIBUTION_X3: &[&str] = &[
73    "AIRCONDITIONING",
74    "AUDIOVISUAL",
75    "CATENARY_SYSTEM",
76    "CHEMICAL",
77    "CHILLEDWATER",
78    "COMMUNICATION",
79    "COMPRESSEDAIR",
80    "CONDENSERWATER",
81    "CONTROL",
82    "CONVEYING",
83    "DATA",
84    "DISPOSAL",
85    "DOMESTICCOLDWATER",
86    "DOMESTICHOTWATER",
87    "DRAINAGE",
88    "EARTHING",
89    "ELECTRICAL",
90    "ELECTROACOUSTIC",
91    "EXHAUST",
92    "FIREPROTECTION",
93    "FIXEDTRANSMISSIONNETWORK",
94    "FUEL",
95    "GAS",
96    "HAZARDOUS",
97    "HEATING",
98    "LIGHTING",
99    "LIGHTNINGPROTECTION",
100    "MOBILENETWORK",
101    "MONITORINGSYSTEM",
102    "MUNICIPALSOLIDWASTE",
103    "OIL",
104    "OPERATIONAL",
105    "OPERATIONALTELEPHONYSYSTEM",
106    "OVERHEAD_CONTACTLINE_SYSTEM",
107    "POWERGENERATION",
108    "RAINWATER",
109    "REFRIGERATION",
110    "RETURN_CIRCUIT",
111    "SECURITY",
112    "SEWAGE",
113    "SIGNAL",
114    "STORMWATER",
115    "TELEPHONE",
116    "TV",
117    "VACUUM",
118    "VENT",
119    "VENTILATION",
120    "WASTEWATER",
121    "WATERSUPPLY",
122    "USERDEFINED",
123    "NOTDEFINED",
124];
125
126/// The IFC4 subset of [`DISTRIBUTION_X3`].
127const DISTRIBUTION_IFC4: &[&str] = &[
128    "AIRCONDITIONING",
129    "AUDIOVISUAL",
130    "CHEMICAL",
131    "CHILLEDWATER",
132    "COMMUNICATION",
133    "COMPRESSEDAIR",
134    "CONDENSERWATER",
135    "CONTROL",
136    "CONVEYING",
137    "DATA",
138    "DISPOSAL",
139    "DOMESTICCOLDWATER",
140    "DOMESTICHOTWATER",
141    "DRAINAGE",
142    "EARTHING",
143    "ELECTRICAL",
144    "ELECTROACOUSTIC",
145    "EXHAUST",
146    "FIREPROTECTION",
147    "FUEL",
148    "GAS",
149    "HAZARDOUS",
150    "HEATING",
151    "LIGHTING",
152    "LIGHTNINGPROTECTION",
153    "MUNICIPALSOLIDWASTE",
154    "OIL",
155    "OPERATIONAL",
156    "POWERGENERATION",
157    "RAINWATER",
158    "REFRIGERATION",
159    "SECURITY",
160    "SEWAGE",
161    "SIGNAL",
162    "STORMWATER",
163    "TELEPHONE",
164    "TV",
165    "VACUUM",
166    "VENT",
167    "VENTILATION",
168    "WASTEWATER",
169    "WATERSUPPLY",
170    "USERDEFINED",
171    "NOTDEFINED",
172];
173
174/// Which classified system is being staged.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176#[non_exhaustive]
177pub enum SystemKind {
178    /// `IfcBuildingSystem`. Superseded by [`SystemKind::Built`] in
179    /// IFC4X3, which still declares it.
180    Building,
181    /// `IfcBuiltSystem`, IFC4X3 only.
182    Built,
183    /// `IfcDistributionCircuit`: a branch of a distribution system.
184    DistributionCircuit,
185    /// `IfcDistributionSystem`: the system a circuit branches from.
186    DistributionSystem,
187}
188
189impl SystemKind {
190    /// STEP type name, upper-case as the model stores it.
191    const fn type_name(self) -> &'static str {
192        match self {
193            Self::Building => "IFCBUILDINGSYSTEM",
194            Self::Built => "IFCBUILTSYSTEM",
195            Self::DistributionCircuit => "IFCDISTRIBUTIONCIRCUIT",
196            Self::DistributionSystem => "IFCDISTRIBUTIONSYSTEM",
197        }
198    }
199
200    /// Tokens this entity's enum declares under `schema`.
201    ///
202    /// `IfcDistributionSystemEnum` is the only one that differs
203    /// between the two shipped schemas.
204    fn members(self, schema: &Schema) -> &'static [&'static str] {
205        match self {
206            Self::Building => BUILDING,
207            Self::Built => BUILT,
208            Self::DistributionCircuit | Self::DistributionSystem => {
209                if schema.name().eq_ignore_ascii_case("IFC4") {
210                    DISTRIBUTION_IFC4
211                } else {
212                    DISTRIBUTION_X3
213                }
214            }
215        }
216    }
217    /// Slot holding `PredefinedType`.
218    ///
219    /// A circuit puts `LongName` first; the other two put the type
220    /// first. This is the whole reason the three cannot share a path.
221    const fn predefined_slot(self) -> usize {
222        match self {
223            Self::Building | Self::Built => 5,
224            Self::DistributionCircuit | Self::DistributionSystem => 6,
225        }
226    }
227
228    /// Slot holding `LongName`, the mirror of `predefined_slot`.
229    const fn long_name_slot(self) -> usize {
230        match self {
231            Self::Building | Self::Built => 6,
232            Self::DistributionCircuit | Self::DistributionSystem => 5,
233        }
234    }
235}
236
237/// Attributes of a classified system beyond `GlobalId` and `Name`.
238#[derive(Debug, Clone, Copy, Default)]
239pub struct ClassifiedSystemDraft<'a> {
240    /// `Description`.
241    pub description: Option<&'a str>,
242    /// `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
243    pub object_type: Option<&'a str>,
244    /// `PredefinedType`, a token of this entity's own enum.
245    pub predefined_type: Option<&'a str>,
246    /// `LongName`: the full name where `Name` is an abbreviation.
247    pub long_name: Option<&'a str>,
248}
249/// Stage a classified system.
250///
251/// `predefined_type` must be a token the entity's own enum declares,
252/// resolved against `schema`: `IfcDistributionSystemEnum` gained seven
253/// members in IFC4X3, and accepting those on IFC4 would name a system
254/// kind that schema does not have.
255///
256/// # Errors
257///
258/// Refuses a malformed GlobalId; an entity the schema does not declare
259/// (`IfcBuiltSystem` on IFC4); a token outside the entity's enum for
260/// the schema in hand; and `USERDEFINED` without `object_type`
261/// (CorrectPredefinedType).
262///
263/// `OwnerHistory` is written `$`. IFC2X3 declares none of the four
264/// entities, so the record is IFC4/IFC4X3 only either way; use
265/// [`create_classified_system_with_owner_history`] to bind the model's
266/// release and set an owner history.
267pub fn create_classified_system(
268    tx: &mut Transaction,
269    schema: &Schema,
270    kind: SystemKind,
271    global_id: &str,
272    name: Option<&str>,
273    draft: ClassifiedSystemDraft<'_>,
274) -> SystemAuthoringResult<EntityId> {
275    let entity = kind.type_name();
276    if Guid::parse(global_id).is_none() {
277        return Err(invalid(entity, "GlobalId", global_id));
278    }
279
280    // Arity comes from the schema, which also answers whether this
281    // schema declares the entity at all.
282    let declared = schema.attributes(entity);
283    if declared.is_empty() {
284        return Err(invalid(entity, "Entity", schema.name().to_owned()));
285    }
286    check_predefined(schema, kind, draft)?;
287
288    let mut attributes = vec![Value::Null; declared.len()];
289    attributes[0] = Value::Text(global_id.into());
290    attributes[2] = text(name);
291    attributes[3] = text(draft.description);
292    attributes[4] = text(draft.object_type);
293    attributes[kind.predefined_slot()] = draft
294        .predefined_type
295        .map_or(Value::Null, |t| Value::Enum(t.into()));
296    attributes[kind.long_name_slot()] = text(draft.long_name);
297    Ok(tx.create(Entity::new(entity, attributes)))
298}
299
300fn text(value: Option<&str>) -> Value {
301    value.map_or(Value::Null, |v| Value::Text(v.into()))
302}
303
304fn blank(value: Option<&str>) -> bool {
305    value.is_none_or(|v| v.trim().is_empty())
306}
307
308/// A token of the entity's own enum for `schema`, and CorrectPredefinedType.
309fn check_predefined(
310    schema: &Schema,
311    kind: SystemKind,
312    draft: ClassifiedSystemDraft<'_>,
313) -> SystemAuthoringResult<()> {
314    let entity = kind.type_name();
315    if let Some(token) = draft.predefined_type {
316        if !kind.members(schema).contains(&token) {
317            return Err(invalid(entity, "PredefinedType", token));
318        }
319        if token == "USERDEFINED" && blank(draft.object_type) {
320            return Err(invalid(entity, "ObjectType", "required by USERDEFINED"));
321        }
322    }
323    Ok(())
324}
325
326/// [`create_classified_system`] in `model`'s declared release, with a
327/// caller-supplied `IfcOwnerHistory` (#202).
328///
329/// The release is bound from `FILE_SCHEMA` (none binds IFC4) instead of
330/// taken from a schema argument, and the record is laid out by attribute
331/// name from its table. IFC2X3 declares none of the four entities and
332/// IFC4 no `IfcBuiltSystem`. The owner history is never invented.
333///
334/// # Errors
335///
336/// Those of [`create_classified_system`] (an undeclared entity is
337/// [`EntityNotInSchema`](super::SystemAuthoringError::EntityNotInSchema)
338/// here), and the release and owner-history refusals of
339/// [`create_system_with_owner_history`](super::create_system_with_owner_history).
340/// Nothing is staged on an error.
341pub fn create_classified_system_with_owner_history(
342    tx: &mut Transaction,
343    model: &Model,
344    kind: SystemKind,
345    global_id: &str,
346    name: Option<&str>,
347    draft: ClassifiedSystemDraft<'_>,
348    owner_history: EntityId,
349) -> SystemAuthoringResult<EntityId> {
350    let release = bind(model)?;
351    let entity = kind.type_name();
352    if Guid::parse(global_id).is_none() {
353        return Err(invalid(entity, "GlobalId", global_id));
354    }
355    release.require_entity(entity)?;
356    check_predefined(release.schema(), kind, draft)?;
357    let values = vec![
358        ("GlobalId", Value::Text(global_id.into())),
359        ("Name", text(name)),
360        ("Description", text(draft.description)),
361        ("ObjectType", text(draft.object_type)),
362        (
363            "PredefinedType",
364            draft
365                .predefined_type
366                .map_or(Value::Null, |t| Value::Enum(t.into())),
367        ),
368        ("LongName", text(draft.long_name)),
369    ];
370    release.stage(tx, model, entity, values, owner_history)
371}