Files
IfcOpenShell/src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py
T
2023-03-03 11:08:56 +05:00

285 lines
14 KiB
Python

# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 @Andrej730
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
import ifcopenshell.util.attribute
from pathlib import Path
from lxml import etree
import zipfile
import shutil
import glob
BASE_MODULE_PATH = Path(__file__).parent
IFC4x3_HTML_LOCATION = BASE_MODULE_PATH / "IFC4.3-html-iso-release"
IFC4x3_OUTPUT_PATH = BASE_MODULE_PATH / "schema/Pset_IFC4X3.ifc"
IFC2x3_HTML_ZIP_LOCATION = BASE_MODULE_PATH / "IFC2x3_TC1_HTML_distribution-pset_errata.zip"
IFC2x3_OUTPUT_PATH = BASE_MODULE_PATH / "schema/Pset_IFC2X3.ifc"
PROPERTY_TYPES_DICT = {
"TypePropertySingleValue": ("P_SINGLEVALUE", "type"),
# in IFC2X3 weirdly xmls have TypeSimpleProperty
# no difference with TypePropertySingleValue though
"TypeSimpleProperty": ("P_SINGLEVALUE", "type"),
"TypePropertyListValue": ("P_LISTVALUE", "type"),
"TypePropertyBoundedValue": ("P_BOUNDEDVALUE", "type"),
"TypePropertyReferenceValue": ("P_REFERENCEVALUE", "reftype"),
"TypePropertyEnumeratedValue": ("P_ENUMERATEDVALUE", ""),
"TypePropertyTableValue": ("P_TABLEVALUE", "type"),
"TypeComplexProperty": ("P_COMPLEX", ""),
}
class PsetTemplatesGenerator:
def parse_ifc4x3_data(self):
print("Starting parsing data for IFC4X3...")
if not IFC4x3_HTML_LOCATION.is_dir():
raise Exception(
f'ISO release for Ifc4.3.0.1 expected to be in folder "{IFC4x3_HTML_LOCATION.resolve()}\\"\n'
"For generating ifc pset library please either setup docs as described above \n"
"or change IFC4x3_HTML_LOCATION in the script accordingly.\n"
"You can download docs from the repository: \n"
"https://github.com/buildingSMART/IFC4.3-html/releases/tag/sep-13-release"
)
# unzip the data
pset_data_zip = IFC4x3_HTML_LOCATION / "IFC/RELEASE/IFC4x3/HTML/annex-a-psd.zip"
pset_data_location = BASE_MODULE_PATH / "temp/annex-a-psd"
with zipfile.ZipFile(pset_data_zip, "r") as fi_zip:
fi_zip.extractall(pset_data_location)
pset_data_glob = f"{pset_data_location}/*.xml"
self.parse_psets_data("IFC4X3", pset_data_glob, "IFC4X3 Property Set Templates", str(IFC4x3_OUTPUT_PATH))
shutil.rmtree(pset_data_location)
def parse_ifc2x3_data(self):
print("Starting parsing data for IFC2X3...")
if not IFC2x3_HTML_ZIP_LOCATION.is_file():
raise Exception(
f'ISO release for IFC2x3 TC1 expected to be in folder "{IFC2x3_HTML_ZIP_LOCATION.resolve()}\\"\n'
"For doc extraction please either setup docs as described above \n"
"or change IFC2x3_HTML_LOCATION in the script accordingly.\n"
"You can download docs from the url: \n"
"https://standards.buildingsmart.org/IFC/RELEASE/IFC2x3/TC1/IFC2x3_TC1_HTML_distribution-pset_errata.zip"
)
# unzip the data
pset_data_location = BASE_MODULE_PATH / "temp/ifc2x3_html"
with zipfile.ZipFile(IFC2x3_HTML_ZIP_LOCATION, "r") as fi_zip:
fi_zip.extractall(pset_data_location)
pset_data_glob = f"{pset_data_location}/**/pset*.xml"
# we're using IFC4X3 since IfcPropertySetTemplate and IfcRelDeclares
# are not available in IFC2X3
self.parse_psets_data("IFC4X3", pset_data_glob, "IFC2X3 Property Set Templates", str(IFC2x3_OUTPUT_PATH))
shutil.rmtree(pset_data_location)
def ifc_entity(self, entity_name, **kwargs):
entity = self.ifc_file.create_entity(entity_name, **kwargs)
return entity
def parse_psets_data(self, schema_name, pset_data_glob, project_name, ifc_output_path):
schema_name = schema_name.upper()
self.ifc_file = ifcopenshell.api.run("project.create_file", version=schema_name)
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
self.ifc_derived_unit_enum = (
schema.declaration_by_name("IfcDerivedUnitEnum").as_enumeration_type().enumeration_items()
)
self.ifc_unit_enum = schema.declaration_by_name("IfcUnitEnum").as_enumeration_type().enumeration_items()
self.ifc_entity("IfcProject", Name=project_name)
psets_list = []
rel = self.ifc_entity("IfcRelDeclares", RelatedDefinitions=psets_list) if schema_name != "IFC2X3" else None
# in IFC4 there is also
# IFCLIBRARYREFERENCE after each (sometimes multiple of them if there are several languages involved)
# 1) enumeration item (also with IFCRELASSOCIATESLIBRARY)
# 2) enumeration
# 3) property set (also with IFCRELASSOCIATESLIBRARY)
# 4) property definition (also with IFCRELASSOCIATESLIBRARY)
# but in ifc4x3 there is no data for those library refernces
# TODO: need to add it to .ifc for IFC4X3 too
# if https://github.com/buildingSMART/IFC4.3.x-development/issues/587 is resolved
# iterate through all xmls files
for pset_path in glob.iglob(pset_data_glob, recursive=True):
pset_path = Path(pset_path)
with open(pset_path, "r", encoding="utf-8") as fi:
root_xml = etree.fromstring(fi.read())
pset_name = root_xml.find("Name").text
# pset / qset
pset_type = True if pset_name.split("_")[0] == "Pset" else False
# TODO: if guids provided in xml should use them instead
pset_guid = ifcopenshell.guid.new()
applicable_entities = [i.text for i in root_xml.find("ApplicableClasses").findall("ClassName")]
pset = self.ifc_entity(
"IfcPropertySetTemplate",
GlobalId=pset_guid,
TemplateType=root_xml.get("templatetype"),
Name=pset_name,
Description=root_xml.find("Definition").text,
ApplicableEntity=",".join(applicable_entities),
)
# NOTE: there is also Applicability tag
# but it's seems always empty in ifc4 and ifc4x3
# in ifc2x3 it's present but contains just applicability string description
# e.g. "IfcElectricMotorType, IfcFlowMovingDeviceType entities." for "Pset_ElectricMotorTypeCommon"
pdef_entities = []
for pdef in root_xml.find("PropertyDefs" if pset_type else "QtoDefs").getchildren():
pset_property = self.get_property_from_pdef(pset_name, pset_type, pdef)
if not pset_property:
continue
pdef_entities.append(pset_property)
pset.HasPropertyTemplates = pdef_entities
psets_list.append(pset)
print(f"{len(psets_list)} psets parsed.")
if rel:
rel.RelatedDefinitions = psets_list
self.ifc_file.write(ifc_output_path)
def get_property_from_pdef(self, pset_name, pset_type, pdef):
pset_prop_guid = ifcopenshell.guid.new()
pset_property_name = pdef.find("Name").text
# figuring pset property types and related values
if not pset_type:
property_type_tag = None
else:
try:
assert len(pdef.find("PropertyType").getchildren()) == 1, "Not implemented case"
except AssertionError:
warning_message = (
f"WARNING: met not handles case with multiple property types "
f"- {pset_name}/{pset_property_name}."
"This property may have incorrect data parsed."
)
print(warning_message)
property_type_tag = pdef.find("PropertyType").getchildren()[0].tag
if property_type_tag not in PROPERTY_TYPES_DICT:
warning_message = (
f"WARNING: not implemented property type "
f"{property_type_tag} ({pset_name}/{pset_property_name}). "
"This property will be skipped. Need to rework the code to support that type of property."
)
print(warning_message)
return
# NOTE: there is also ValueDef tag
# which could have two subtags - MinValue and MaxValue
# but it's seems to be present only in ifc2x3 and everywhere values are either "", "0", "?"
# so just ignoring it
# create pset property
if not pset_type or property_type_tag != "TypeComplexProperty":
pset_property = self.ifc_entity("IfcSimplePropertyTemplate")
# tested on IFC4_ADD2.ifc - all props are READWRITE by default
pset_property.AccessState = "READWRITE"
else:
pset_property = self.ifc_entity("IfcComplexPropertyTemplate")
complex_prop_xml = pdef.find("PropertyType/TypeComplexProperty")
child_properties = [
self.get_property_from_pdef(pset_name, pset_type, child_pdef)
for child_pdef in complex_prop_xml.findall("PropertyDef")
]
pset_property.UsageName = complex_prop_xml.get("name")
pset_property.HasPropertyTemplates = child_properties
pset_property.GlobalId = pset_prop_guid
pset_property.Description = pdef.find("Definition").text
pset_property.Name = pset_property_name
self.add_prop_type_params_to_prop(pset_type, pdef, pset_property, property_type_tag)
return pset_property
def add_prop_type_params_to_prop(self, pset_type, pdef, pset_property, property_type_tag):
if not pset_type:
property_type = pdef.find("QtoType").text
else:
property_type, property_type_parse = PROPERTY_TYPES_DICT[property_type_tag]
pset_property.TemplateType = property_type
if not pset_type or property_type == "P_COMPLEX":
# qsets and complex props don't have measure types
return
property_type_path = f"PropertyType/{property_type_tag}"
property_type_xml = pdef.find(property_type_path)
# usually it's only for P_TABLEVALUE
expression_xml = property_type_xml.find("Expression")
if expression_xml is not None and expression_xml.text != "-":
pset_property.Expression = expression_xml.text
# figure measure type for property sets
if property_type == "P_ENUMERATEDVALUE":
# always create new enumeration for the new properties
# even if the same enumeration was already used before
# example of reoccuring enumeration in .ifc for ifc4 - PEnum_ElementStatus
enum_items = [i.text for i in property_type_xml.findall(f"EnumList/EnumItem")]
enum_items = [self.ifc_entity("IfcLabel", wrappedValue=i) for i in enum_items]
prop_enumeration = self.ifc_entity(
"IfcPropertyEnumeration",
Name=property_type_xml.find("EnumList").get("name"),
EnumerationValues=enum_items,
)
pset_property.Enumerators = prop_enumeration
pset_property.PrimaryMeasureType = "IfcLabel"
elif property_type == "P_TABLEVALUE":
pset_property.PrimaryMeasureType = property_type_xml.find("DefiningValue/DataType").get("type")
pset_property.SecondaryMeasureType = property_type_xml.find("DefinedValue/DataType").get("type")
else:
if property_type == "P_REFERENCEVALUE":
pset_property.PrimaryMeasureType = property_type_xml.get(property_type_parse)
# TODO: ifc4add2 seems to have some secondary measure types
# need to add it in ifc4x3 too
# if https://github.com/buildingSMART/IFC4.3.x-development/issues/586 is resolved
else:
if property_type == "P_LISTVALUE":
property_type_node = "ListValue"
if property_type in ("P_SINGLEVALUE", "P_BOUNDEDVALUE"):
property_type_node = "DataType"
# only used only in ifc2x3, omitted in ifc4 and ifc4x3
unit_type_xml = property_type_xml.find("UnitType")
if unit_type_xml is not None:
unit_type = unit_type_xml.get(property_type_parse).strip()
if unit_type.lower().startswith("ifc"):
unit_entity = self.ifc_entity(unit_type)
else:
if unit_type in self.ifc_derived_unit_enum:
unit_entity = self.ifc_entity("IfcDerivedUnit", UnitType=unit_type)
elif unit_type in self.ifc_unit_enum:
unit_entity = self.ifc_entity("IfcNamedUnit", UnitType=unit_type)
else:
print("WARNING. Wasn't able to find units {unit_type} in schema.")
pset_property.PrimaryUnit = unit_entity
pset_property.PrimaryMeasureType = property_type_xml.find(property_type_node).get(property_type_parse)
if __name__ == "__main__":
templates_generator = PsetTemplatesGenerator()
templates_generator.parse_ifc4x3_data()
templates_generator.parse_ifc2x3_data()