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)]
239#[non_exhaustive]
240pub struct ClassifiedSystemDraft<'a> {
241    /// `Description`.
242    pub description: Option<&'a str>,
243    /// `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
244    pub object_type: Option<&'a str>,
245    /// `PredefinedType`, a token of this entity's own enum.
246    pub predefined_type: Option<&'a str>,
247    /// `LongName`: the full name where `Name` is an abbreviation.
248    pub long_name: Option<&'a str>,
249}
250
251impl<'a> ClassifiedSystemDraft<'a> {
252    /// Starts an empty draft with every attribute unset.
253    #[must_use]
254    pub fn new() -> Self {
255        Self::default()
256    }
257
258    /// Sets `Description`.
259    #[must_use]
260    pub const fn description(mut self, value: &'a str) -> Self {
261        self.description = Some(value);
262        self
263    }
264
265    /// Sets `ObjectType`, required when `predefined_type` is `USERDEFINED`.
266    #[must_use]
267    pub const fn object_type(mut self, value: &'a str) -> Self {
268        self.object_type = Some(value);
269        self
270    }
271
272    /// Sets `PredefinedType`, a token of the entity's own enum.
273    #[must_use]
274    pub const fn predefined_type(mut self, value: &'a str) -> Self {
275        self.predefined_type = Some(value);
276        self
277    }
278
279    /// Sets `LongName`.
280    #[must_use]
281    pub const fn long_name(mut self, value: &'a str) -> Self {
282        self.long_name = Some(value);
283        self
284    }
285}
286/// Stage a classified system.
287///
288/// `predefined_type` must be a token the entity's own enum declares,
289/// resolved against `schema`: `IfcDistributionSystemEnum` gained seven
290/// members in IFC4X3, and accepting those on IFC4 would name a system
291/// kind that schema does not have.
292///
293/// # Errors
294///
295/// Refuses a malformed GlobalId; an entity the schema does not declare
296/// (`IfcBuiltSystem` on IFC4); a token outside the entity's enum for
297/// the schema in hand; and `USERDEFINED` without `object_type`
298/// (CorrectPredefinedType).
299///
300/// `OwnerHistory` is written `$`. IFC2X3 declares none of the four
301/// entities, so the record is IFC4/IFC4X3 only either way; use
302/// [`create_classified_system_with_owner_history`] to bind the model's
303/// release and set an owner history.
304pub fn create_classified_system(
305    tx: &mut Transaction,
306    schema: &Schema,
307    kind: SystemKind,
308    global_id: &str,
309    name: Option<&str>,
310    draft: ClassifiedSystemDraft<'_>,
311) -> SystemAuthoringResult<EntityId> {
312    let entity = kind.type_name();
313    if Guid::parse(global_id).is_none() {
314        return Err(invalid(entity, "GlobalId", global_id));
315    }
316
317    // Arity comes from the schema, which also answers whether this
318    // schema declares the entity at all.
319    let declared = schema.attributes(entity);
320    if declared.is_empty() {
321        return Err(invalid(entity, "Entity", schema.name().to_owned()));
322    }
323    check_predefined(schema, kind, draft)?;
324
325    let mut attributes = vec![Value::Null; declared.len()];
326    attributes[0] = Value::Text(global_id.into());
327    attributes[2] = text(name);
328    attributes[3] = text(draft.description);
329    attributes[4] = text(draft.object_type);
330    attributes[kind.predefined_slot()] = draft
331        .predefined_type
332        .map_or(Value::Null, |t| Value::Enum(t.into()));
333    attributes[kind.long_name_slot()] = text(draft.long_name);
334    Ok(tx.create(Entity::new(entity, attributes)))
335}
336
337fn text(value: Option<&str>) -> Value {
338    value.map_or(Value::Null, |v| Value::Text(v.into()))
339}
340
341fn blank(value: Option<&str>) -> bool {
342    value.is_none_or(|v| v.trim().is_empty())
343}
344
345/// A token of the entity's own enum for `schema`, and CorrectPredefinedType.
346fn check_predefined(
347    schema: &Schema,
348    kind: SystemKind,
349    draft: ClassifiedSystemDraft<'_>,
350) -> SystemAuthoringResult<()> {
351    let entity = kind.type_name();
352    if let Some(token) = draft.predefined_type {
353        if !kind.members(schema).contains(&token) {
354            return Err(invalid(entity, "PredefinedType", token));
355        }
356        if token == "USERDEFINED" && blank(draft.object_type) {
357            return Err(invalid(entity, "ObjectType", "required by USERDEFINED"));
358        }
359    }
360    Ok(())
361}
362
363/// [`create_classified_system`] in `model`'s declared release, with a
364/// caller-supplied `IfcOwnerHistory` (#202).
365///
366/// The release is bound from `FILE_SCHEMA` (none binds IFC4) instead of
367/// taken from a schema argument, and the record is laid out by attribute
368/// name from its table. IFC2X3 declares none of the four entities and
369/// IFC4 no `IfcBuiltSystem`. The owner history is never invented.
370///
371/// # Errors
372///
373/// Those of [`create_classified_system`] (an undeclared entity is
374/// [`EntityNotInSchema`](super::SystemAuthoringError::EntityNotInSchema)
375/// here), and the release and owner-history refusals of
376/// [`create_system_with_owner_history`](super::create_system_with_owner_history).
377/// Nothing is staged on an error.
378pub fn create_classified_system_with_owner_history(
379    tx: &mut Transaction,
380    model: &Model,
381    kind: SystemKind,
382    global_id: &str,
383    name: Option<&str>,
384    draft: ClassifiedSystemDraft<'_>,
385    owner_history: EntityId,
386) -> SystemAuthoringResult<EntityId> {
387    let release = bind(model)?;
388    let entity = kind.type_name();
389    if Guid::parse(global_id).is_none() {
390        return Err(invalid(entity, "GlobalId", global_id));
391    }
392    release.require_entity(entity)?;
393    check_predefined(release.schema(), kind, draft)?;
394    let values = vec![
395        ("GlobalId", Value::Text(global_id.into())),
396        ("Name", text(name)),
397        ("Description", text(draft.description)),
398        ("ObjectType", text(draft.object_type)),
399        (
400            "PredefinedType",
401            draft
402                .predefined_type
403                .map_or(Value::Null, |t| Value::Enum(t.into())),
404        ),
405        ("LongName", text(draft.long_name)),
406    ];
407    release.stage(tx, model, entity, values, owner_history)
408}