-
Notifications
You must be signed in to change notification settings - Fork 92
/
Copy pathgenerate_pset_templates.py
327 lines (283 loc) · 15.2 KB
/
generate_pset_templates.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# 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 = True
import ifcopenshell.api
import ifcopenshell.util.attribute
from pathlib import Path
from lxml import etree
import glob
import sys
if not RUN_FROM_DEV_REPO:
import zipfile
import shutil
BASE_MODULE_PATH = Path(__file__).parent
IFC4x3_HTML_LOCATION = BASE_MODULE_PATH / "IFC4.3-html-iso-release"
if not RUN_FROM_DEV_REPO:
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
# 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 RUN_FROM_DEV_REPO:
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
if not RUN_FROM_DEV_REPO:
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)
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 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()
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),
)
# 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
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)
pset_property.PrimaryMeasureType = primary_measure_type
if __name__ == "__main__":
templates_generator = PsetTemplatesGenerator()
templates_generator.parse_ifc4x3_data()
if not RUN_FROM_DEV_REPO:
templates_generator.parse_ifc2x3_data()