First Commit
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
# 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/>.
|
||||
|
||||
RUN_FROM_DEV_REPO = False
|
||||
|
||||
import glob
|
||||
import sys
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from lxml import etree
|
||||
|
||||
import ifcopenshell.api.project
|
||||
import ifcopenshell.api.unit
|
||||
import ifcopenshell.guid
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
|
||||
if not RUN_FROM_DEV_REPO:
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
BASE_MODULE_PATH = Path(__file__).parent
|
||||
|
||||
if not RUN_FROM_DEV_REPO:
|
||||
IFC4x3_HTML_ZIP_LOCATION = BASE_MODULE_PATH / "annex-a-psd.zip"
|
||||
IFC4x3_OUTPUT_PATH = BASE_MODULE_PATH / "schema/Pset_IFC4X3.ifc"
|
||||
else:
|
||||
IFC4x3_PSD_LOCATION = BASE_MODULE_PATH / "../output/psd"
|
||||
try:
|
||||
IFC4x3_OUTPUT_PATH = sys.argv[1]
|
||||
except:
|
||||
IFC4x3_OUTPUT_PATH = BASE_MODULE_PATH / "../output/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
|
||||
# which is actually more P_REFERENCEVALUE value, not P_SINGLEVALUE
|
||||
# because it utilizes IfcTimeSeries
|
||||
"TypeSimpleProperty": ("P_REFERENCEVALUE", "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 RUN_FROM_DEV_REPO:
|
||||
if not IFC4x3_HTML_ZIP_LOCATION.is_file():
|
||||
raise Exception(
|
||||
f'ISO release for Ifc4.3.2.0 expected to be located in "{IFC4x3_HTML_ZIP_LOCATION.resolve()}"\n'
|
||||
"For generating ifc pset library please either setup docs as described above \n"
|
||||
"or change IFC4x3_HTML_ZIP_LOCATION in the script accordingly.\n"
|
||||
"You can download docs from the repository: \n"
|
||||
"https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/annex-a-psd.zip"
|
||||
)
|
||||
# unzip the data
|
||||
pset_data_location = BASE_MODULE_PATH / "temp/annex-a-psd"
|
||||
with zipfile.ZipFile(IFC4x3_HTML_ZIP_LOCATION, "r") as fi_zip:
|
||||
fi_zip.extractall(pset_data_location)
|
||||
else:
|
||||
if not IFC4x3_PSD_LOCATION.is_dir():
|
||||
raise Exception(
|
||||
f'Psets xmls files expected to be in folder "{IFC4x3_PSD_LOCATION.resolve()}\\"\n'
|
||||
"For generating ifc pset library please either setup docs as described above \n"
|
||||
"or change IFC4x3_PSD_LOCATION in the script accordingly."
|
||||
)
|
||||
pset_data_location = IFC4x3_PSD_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))
|
||||
|
||||
if not RUN_FROM_DEV_REPO:
|
||||
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 located in "{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.project.create_file(version=schema_name)
|
||||
self.units = dict()
|
||||
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(self.ifc_file.schema_identifier)
|
||||
|
||||
derived_unit_enum = schema.declaration_by_name("IfcDerivedUnitEnum").as_enumeration_type()
|
||||
assert derived_unit_enum
|
||||
self.ifc_derived_unit_enum = derived_unit_enum.enumeration_items()
|
||||
|
||||
ifc_unit_enum = schema.declaration_by_name("IfcUnitEnum").as_enumeration_type()
|
||||
assert ifc_unit_enum
|
||||
self.ifc_unit_enum = ifc_unit_enum.enumeration_items()
|
||||
|
||||
value_select = schema.declaration_by_name("IfcValue").as_select_type()
|
||||
assert value_select
|
||||
select_types = cast(tuple[W.select_type, ...], value_select.select_list())
|
||||
self.ifc_value_types = [t.name() for t in chain(*[select_type.select_list() for select_type in select_types])]
|
||||
|
||||
project = self.ifc_entity("IfcProject", Name=project_name, GlobalId=ifcopenshell.guid.new())
|
||||
psets_list = []
|
||||
if schema_name == "IFC2X3":
|
||||
rel = None
|
||||
else:
|
||||
rel = self.ifc_entity(
|
||||
"IfcRelDeclares",
|
||||
RelatedDefinitions=psets_list,
|
||||
RelatingContext=project,
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
)
|
||||
|
||||
# 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 references
|
||||
# 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).strip(),
|
||||
)
|
||||
if project_name.startswith("IFC2X3"):
|
||||
pset.TemplateType = self.get_pset_template_type_ifc2x3(pset)
|
||||
else:
|
||||
pset.TemplateType = root_xml.get("templatetype")
|
||||
|
||||
# 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 get_unit(self, unit_type):
|
||||
# to define USERDEFINED unit type we'd need more info
|
||||
# like UserDefinedType name and elements for IfcDerivedUnit
|
||||
# which is not provided in .xmls
|
||||
if unit_type != "USERDEFINED":
|
||||
return None
|
||||
|
||||
if unit_type in self.units:
|
||||
return self.units[unit_type]
|
||||
|
||||
unit_entity = None
|
||||
if unit_type in self.ifc_derived_unit_enum:
|
||||
# TODO: define derived units if there will be someday api like `ifcopenshell.api.unit.add_derived_unit(...)`
|
||||
# since creating IfcDerivedUnit is more complex and requiring settting up elements it consists of
|
||||
# and related IfcNamedUnits
|
||||
# unit_entity = ifcopenshell.api.unit.add_derived_unit(self.ifc_file, unit_type=unit_type)
|
||||
pass
|
||||
elif unit_type in self.ifc_unit_enum:
|
||||
unit_entity = ifcopenshell.api.unit.add_si_unit(self.ifc_file, unit_type=unit_type)
|
||||
elif unit_type == "IFCMONETARYUNIT":
|
||||
self.ifc_entity("IFCMONETARYUNIT", Currency="")
|
||||
else:
|
||||
print(f"WARNING. Wasn't able to find units {unit_type} in schema.")
|
||||
self.units[unit_type] = unit_entity
|
||||
return unit_entity
|
||||
|
||||
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":
|
||||
if property_type_tag == "TypeSimpleProperty":
|
||||
type_xml = property_type_xml.find("DataType")
|
||||
primary_measure_type = type_xml.get(property_type_parse)
|
||||
unit_type = property_type_xml.find("UnitType")
|
||||
secondary_measure_type = (
|
||||
unit_type.get(property_type_parse).strip() if unit_type is not None else None
|
||||
)
|
||||
|
||||
if primary_measure_type != "IfcTimeSeries":
|
||||
unit = self.get_unit(secondary_measure_type)
|
||||
secondary_measure_type = primary_measure_type
|
||||
primary_measure_type = "IfcTimeSeries"
|
||||
pset_property.PrimaryUnit = unit
|
||||
|
||||
pset_property.PrimaryMeasureType = primary_measure_type
|
||||
pset_property.SecondaryMeasureType = secondary_measure_type
|
||||
else:
|
||||
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()
|
||||
unit_entity = self.get_unit(unit_type)
|
||||
pset_property.PrimaryUnit = unit_entity
|
||||
|
||||
type_xml = property_type_xml.find(property_type_node)
|
||||
if property_type_node == "ListValue":
|
||||
# for ListValue data type is contained in a single <DataType>
|
||||
primary_measure_type = type_xml.find("DataType").get(property_type_parse)
|
||||
else:
|
||||
primary_measure_type = type_xml.get(property_type_parse)
|
||||
|
||||
primary_measure_type = primary_measure_type.strip()
|
||||
if property_type == "P_SINGLEVALUE" and primary_measure_type not in self.ifc_value_types:
|
||||
print(
|
||||
f"Error assinging {primary_measure_type} as PrimaryMeasureType - it's not IfcValue, {property_type_tag}"
|
||||
)
|
||||
|
||||
pset_property.PrimaryMeasureType = primary_measure_type
|
||||
|
||||
def get_pset_template_type_ifc2x3(self, pset_template: ifcopenshell.entity_instance) -> str:
|
||||
def declaration_is_a(declaration: ifcopenshell_wrapper.declaration, ifc_class: str) -> bool:
|
||||
if declaration.name() == ifc_class:
|
||||
return True
|
||||
super_type = declaration.supertype()
|
||||
if not super_type:
|
||||
return False
|
||||
return declaration_is_a(super_type, ifc_class)
|
||||
|
||||
name = pset_template.Name
|
||||
applicability = pset_template.ApplicableEntity
|
||||
schema = ifcopenshell.schema_by_name("IFC2X3")
|
||||
|
||||
if "PHistory" in name:
|
||||
return "PSET_PERFORMANCEDRIVEN"
|
||||
applicable_types = applicability.replace(", ", ",").split(",")
|
||||
for applicable_type in applicable_types:
|
||||
if not applicable_type:
|
||||
continue
|
||||
parts = applicable_type.split("/")
|
||||
assert 3 > len(parts) > 0
|
||||
if parts[0].isupper(): # IFC2X3 thing
|
||||
applicable_type = parts[1]
|
||||
else:
|
||||
applicable_type = parts[0]
|
||||
applicable_type = applicable_type.strip()
|
||||
|
||||
declaration = schema.declaration_by_name(applicable_type)
|
||||
if declaration_is_a(declaration, "IfcTypeObject"):
|
||||
return "PSET_TYPEDRIVENOVERRIDE"
|
||||
# ifc4x3+
|
||||
elif declaration_is_a(declaration, "IfcProfileDef"):
|
||||
return "PSET_PROFILEDRIVEN"
|
||||
elif declaration_is_a(declaration, "IfcMaterialDefinition"):
|
||||
return "PSET_MATERIALDRIVEN"
|
||||
return "PSET_OCCURRENCEDRIVEN"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
templates_generator = PsetTemplatesGenerator()
|
||||
templates_generator.parse_ifc4x3_data()
|
||||
if not RUN_FROM_DEV_REPO:
|
||||
templates_generator.parse_ifc2x3_data()
|
||||
Reference in New Issue
Block a user