ifc-systems 0.2.3

Distribution systems, ports, and connectivity between elements.
Documentation
//! The three named system classifications.
//!
//! # Why these are not `create_system`
//!
//! `IfcSystem` is the unclassified group. These three add a
//! `PredefinedType` naming what kind of system it is, and they do not
//! agree on where it sits: `IfcBuildingSystem` and `IfcBuiltSystem`
//! put it at slot 5 with `LongName` at 6, while
//! `IfcDistributionCircuit` puts `LongName` at 5 and the type at 6.
//! Writing one layout for all three files a circuit's name into its
//! classification slot, producing a system whose kind is its name.
//!
//! # CorrectPredefinedType
//!
//! `IfcBuildingSystem` and `IfcBuiltSystem` both state:
//!
//! ```text
//! NOT(EXISTS(PredefinedType)) OR (PredefinedType <> USERDEFINED) OR
//! ((PredefinedType = USERDEFINED) AND EXISTS(SELF\\IfcObject.ObjectType))
//! ```
//!
//! `USERDEFINED` says the enum has no token for this kind and the name
//! is given as `ObjectType`. Without it the record asserts a name
//! exists and then withholds it.

use ifc_model::guid::Guid;
use ifc_model::{Entity, EntityId, Model, Transaction, Value};
use ifc_schema::Schema;

use super::release::bind;
use super::{invalid, SystemAuthoringResult};

/// `IfcBuildingSystemTypeEnum`, IFC4 and IFC4X3.
const BUILDING: &[&str] = &[
    "FENESTRATION",
    "FOUNDATION",
    "LOADBEARING",
    "OUTERSHELL",
    "SHADING",
    "TRANSPORT",
    "USERDEFINED",
    "NOTDEFINED",
];

/// `IfcBuiltSystemTypeEnum`, IFC4X3 only.
///
/// A superset of the building-system tokens: IFC4X3 renamed the
/// entity and widened the list to cover civil works.
const BUILT: &[&str] = &[
    "EROSIONPREVENTION",
    "FENESTRATION",
    "FOUNDATION",
    "LOADBEARING",
    "MOORING",
    "OUTERSHELL",
    "PRESTRESSING",
    "RAILWAYLINE",
    "RAILWAYTRACK",
    "REINFORCING",
    "SHADING",
    "TRACKCIRCUIT",
    "TRANSPORT",
    "USERDEFINED",
    "NOTDEFINED",
];

/// `IfcDistributionSystemEnum`.
///
/// The IFC4X3 list, a superset of IFC4's. Accepting the superset on
/// both schemas would let an IFC4 file name a system kind IFC4 does
/// not declare, so the schema in hand selects the slice.
const DISTRIBUTION_X3: &[&str] = &[
    "AIRCONDITIONING",
    "AUDIOVISUAL",
    "CATENARY_SYSTEM",
    "CHEMICAL",
    "CHILLEDWATER",
    "COMMUNICATION",
    "COMPRESSEDAIR",
    "CONDENSERWATER",
    "CONTROL",
    "CONVEYING",
    "DATA",
    "DISPOSAL",
    "DOMESTICCOLDWATER",
    "DOMESTICHOTWATER",
    "DRAINAGE",
    "EARTHING",
    "ELECTRICAL",
    "ELECTROACOUSTIC",
    "EXHAUST",
    "FIREPROTECTION",
    "FIXEDTRANSMISSIONNETWORK",
    "FUEL",
    "GAS",
    "HAZARDOUS",
    "HEATING",
    "LIGHTING",
    "LIGHTNINGPROTECTION",
    "MOBILENETWORK",
    "MONITORINGSYSTEM",
    "MUNICIPALSOLIDWASTE",
    "OIL",
    "OPERATIONAL",
    "OPERATIONALTELEPHONYSYSTEM",
    "OVERHEAD_CONTACTLINE_SYSTEM",
    "POWERGENERATION",
    "RAINWATER",
    "REFRIGERATION",
    "RETURN_CIRCUIT",
    "SECURITY",
    "SEWAGE",
    "SIGNAL",
    "STORMWATER",
    "TELEPHONE",
    "TV",
    "VACUUM",
    "VENT",
    "VENTILATION",
    "WASTEWATER",
    "WATERSUPPLY",
    "USERDEFINED",
    "NOTDEFINED",
];

/// The IFC4 subset of [`DISTRIBUTION_X3`].
const DISTRIBUTION_IFC4: &[&str] = &[
    "AIRCONDITIONING",
    "AUDIOVISUAL",
    "CHEMICAL",
    "CHILLEDWATER",
    "COMMUNICATION",
    "COMPRESSEDAIR",
    "CONDENSERWATER",
    "CONTROL",
    "CONVEYING",
    "DATA",
    "DISPOSAL",
    "DOMESTICCOLDWATER",
    "DOMESTICHOTWATER",
    "DRAINAGE",
    "EARTHING",
    "ELECTRICAL",
    "ELECTROACOUSTIC",
    "EXHAUST",
    "FIREPROTECTION",
    "FUEL",
    "GAS",
    "HAZARDOUS",
    "HEATING",
    "LIGHTING",
    "LIGHTNINGPROTECTION",
    "MUNICIPALSOLIDWASTE",
    "OIL",
    "OPERATIONAL",
    "POWERGENERATION",
    "RAINWATER",
    "REFRIGERATION",
    "SECURITY",
    "SEWAGE",
    "SIGNAL",
    "STORMWATER",
    "TELEPHONE",
    "TV",
    "VACUUM",
    "VENT",
    "VENTILATION",
    "WASTEWATER",
    "WATERSUPPLY",
    "USERDEFINED",
    "NOTDEFINED",
];

/// Which classified system is being staged.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SystemKind {
    /// `IfcBuildingSystem`. Superseded by [`SystemKind::Built`] in
    /// IFC4X3, which still declares it.
    Building,
    /// `IfcBuiltSystem`, IFC4X3 only.
    Built,
    /// `IfcDistributionCircuit`: a branch of a distribution system.
    DistributionCircuit,
    /// `IfcDistributionSystem`: the system a circuit branches from.
    DistributionSystem,
}

impl SystemKind {
    /// STEP type name, upper-case as the model stores it.
    const fn type_name(self) -> &'static str {
        match self {
            Self::Building => "IFCBUILDINGSYSTEM",
            Self::Built => "IFCBUILTSYSTEM",
            Self::DistributionCircuit => "IFCDISTRIBUTIONCIRCUIT",
            Self::DistributionSystem => "IFCDISTRIBUTIONSYSTEM",
        }
    }

    /// Tokens this entity's enum declares under `schema`.
    ///
    /// `IfcDistributionSystemEnum` is the only one that differs
    /// between the two shipped schemas.
    fn members(self, schema: &Schema) -> &'static [&'static str] {
        match self {
            Self::Building => BUILDING,
            Self::Built => BUILT,
            Self::DistributionCircuit | Self::DistributionSystem => {
                if schema.name().eq_ignore_ascii_case("IFC4") {
                    DISTRIBUTION_IFC4
                } else {
                    DISTRIBUTION_X3
                }
            }
        }
    }
    /// Slot holding `PredefinedType`.
    ///
    /// A circuit puts `LongName` first; the other two put the type
    /// first. This is the whole reason the three cannot share a path.
    const fn predefined_slot(self) -> usize {
        match self {
            Self::Building | Self::Built => 5,
            Self::DistributionCircuit | Self::DistributionSystem => 6,
        }
    }

    /// Slot holding `LongName`, the mirror of `predefined_slot`.
    const fn long_name_slot(self) -> usize {
        match self {
            Self::Building | Self::Built => 6,
            Self::DistributionCircuit | Self::DistributionSystem => 5,
        }
    }
}

/// Attributes of a classified system beyond `GlobalId` and `Name`.
#[derive(Debug, Clone, Copy, Default)]
pub struct ClassifiedSystemDraft<'a> {
    /// `Description`.
    pub description: Option<&'a str>,
    /// `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
    pub object_type: Option<&'a str>,
    /// `PredefinedType`, a token of this entity's own enum.
    pub predefined_type: Option<&'a str>,
    /// `LongName`: the full name where `Name` is an abbreviation.
    pub long_name: Option<&'a str>,
}
/// Stage a classified system.
///
/// `predefined_type` must be a token the entity's own enum declares,
/// resolved against `schema`: `IfcDistributionSystemEnum` gained seven
/// members in IFC4X3, and accepting those on IFC4 would name a system
/// kind that schema does not have.
///
/// # Errors
///
/// Refuses a malformed GlobalId; an entity the schema does not declare
/// (`IfcBuiltSystem` on IFC4); a token outside the entity's enum for
/// the schema in hand; and `USERDEFINED` without `object_type`
/// (CorrectPredefinedType).
///
/// `OwnerHistory` is written `$`. IFC2X3 declares none of the four
/// entities, so the record is IFC4/IFC4X3 only either way; use
/// [`create_classified_system_with_owner_history`] to bind the model's
/// release and set an owner history.
pub fn create_classified_system(
    tx: &mut Transaction,
    schema: &Schema,
    kind: SystemKind,
    global_id: &str,
    name: Option<&str>,
    draft: ClassifiedSystemDraft<'_>,
) -> SystemAuthoringResult<EntityId> {
    let entity = kind.type_name();
    if Guid::parse(global_id).is_none() {
        return Err(invalid(entity, "GlobalId", global_id));
    }

    // Arity comes from the schema, which also answers whether this
    // schema declares the entity at all.
    let declared = schema.attributes(entity);
    if declared.is_empty() {
        return Err(invalid(entity, "Entity", schema.name().to_owned()));
    }
    check_predefined(schema, kind, draft)?;

    let mut attributes = vec![Value::Null; declared.len()];
    attributes[0] = Value::Text(global_id.into());
    attributes[2] = text(name);
    attributes[3] = text(draft.description);
    attributes[4] = text(draft.object_type);
    attributes[kind.predefined_slot()] = draft
        .predefined_type
        .map_or(Value::Null, |t| Value::Enum(t.into()));
    attributes[kind.long_name_slot()] = text(draft.long_name);
    Ok(tx.create(Entity::new(entity, attributes)))
}

fn text(value: Option<&str>) -> Value {
    value.map_or(Value::Null, |v| Value::Text(v.into()))
}

fn blank(value: Option<&str>) -> bool {
    value.is_none_or(|v| v.trim().is_empty())
}

/// A token of the entity's own enum for `schema`, and CorrectPredefinedType.
fn check_predefined(
    schema: &Schema,
    kind: SystemKind,
    draft: ClassifiedSystemDraft<'_>,
) -> SystemAuthoringResult<()> {
    let entity = kind.type_name();
    if let Some(token) = draft.predefined_type {
        if !kind.members(schema).contains(&token) {
            return Err(invalid(entity, "PredefinedType", token));
        }
        if token == "USERDEFINED" && blank(draft.object_type) {
            return Err(invalid(entity, "ObjectType", "required by USERDEFINED"));
        }
    }
    Ok(())
}

/// [`create_classified_system`] in `model`'s declared release, with a
/// caller-supplied `IfcOwnerHistory` (#202).
///
/// The release is bound from `FILE_SCHEMA` (none binds IFC4) instead of
/// taken from a schema argument, and the record is laid out by attribute
/// name from its table. IFC2X3 declares none of the four entities and
/// IFC4 no `IfcBuiltSystem`. The owner history is never invented.
///
/// # Errors
///
/// Those of [`create_classified_system`] (an undeclared entity is
/// [`EntityNotInSchema`](super::SystemAuthoringError::EntityNotInSchema)
/// here), and the release and owner-history refusals of
/// [`create_system_with_owner_history`](super::create_system_with_owner_history).
/// Nothing is staged on an error.
pub fn create_classified_system_with_owner_history(
    tx: &mut Transaction,
    model: &Model,
    kind: SystemKind,
    global_id: &str,
    name: Option<&str>,
    draft: ClassifiedSystemDraft<'_>,
    owner_history: EntityId,
) -> SystemAuthoringResult<EntityId> {
    let release = bind(model)?;
    let entity = kind.type_name();
    if Guid::parse(global_id).is_none() {
        return Err(invalid(entity, "GlobalId", global_id));
    }
    release.require_entity(entity)?;
    check_predefined(release.schema(), kind, draft)?;
    let values = vec![
        ("GlobalId", Value::Text(global_id.into())),
        ("Name", text(name)),
        ("Description", text(draft.description)),
        ("ObjectType", text(draft.object_type)),
        (
            "PredefinedType",
            draft
                .predefined_type
                .map_or(Value::Null, |t| Value::Enum(t.into())),
        ),
        ("LongName", text(draft.long_name)),
    ];
    release.stage(tx, model, entity, values, owner_history)
}