From 5438331fe16598e392685984e75b6334a342b763 Mon Sep 17 00:00:00 2001 From: Massimo Fabbro Date: Wed, 27 Sep 2023 18:51:37 +0200 Subject: [PATCH 01/15] fix generate spaces from walls bug --- src/blenderbim/blenderbim/core/spatial.py | 8 +++++--- src/blenderbim/blenderbim/core/tool.py | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index 7f2beb3aba..109168847b 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -125,7 +125,9 @@ def select_decomposed_elements(spatial): #HERE STARTS SPATIAL TOOL def generate_spaces_from_walls(ifc, spatial, collector): - container, active_obj = spatial.get_container_and_active_obj() + active_obj = bpy.context.active_object + element = ifc.get_entity(active_obj) + container = spatial.get_container(element) if not active_obj: self.report({"ERROR"}, "No active object. Please select a wall") @@ -147,12 +149,12 @@ def generate_spaces_from_walls(ifc, spatial, collector): h = active_obj.dimensions.z selected_objects = bpy.context.selected_objects - union = spatial.get_union_shape_from_selected_objects(selected_objects) + union = spatial.get_union_shape_from_selected_objects() for i, linear_ring in enumerate(union.interiors): poly = spatial.get_buffered_poly_from_linear_ring(linear_ring) - bm = spatial.get_bmesh_from_polygon(poly, mat, h) + bm = spatial.get_bmesh_from_polygon(poly, h) name = "Space" + str(i) mesh = bpy.data.meshes.new(name=name) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 0557e0144d..de5d5f31cb 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -810,8 +810,7 @@ class Spatial: def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass def show_scene_objects(cls): pass #HERE STARTS SPATIAL TOOL -# def get_container_and_active_obj(cls): pass - def get_union_shape_from_selected_objects(cls, selected_objects): pass + def get_union_shape_from_selected_objects(cls): pass def get_boundary_elements(cls, selected_objects): pass def get_polygons(cls, boundary_elements): pass def get_obj_base_points(cls, obj): pass From 24170a8a164a49315e2eef7ecb19e26e83c4b9dc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 28 Sep 2023 23:07:21 +1000 Subject: [PATCH 02/15] Ship with fallback IFC2X3 georeferencing pset --- .../bim/data/pset/ePSet_Georeferencing.ifc | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/blenderbim/blenderbim/bim/data/pset/ePSet_Georeferencing.ifc diff --git a/src/blenderbim/blenderbim/bim/data/pset/ePSet_Georeferencing.ifc b/src/blenderbim/blenderbim/bim/data/pset/ePSet_Georeferencing.ifc new file mode 100644 index 0000000000..1b6bb5ab6c --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/pset/ePSet_Georeferencing.ifc @@ -0,0 +1,23 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2023-09-28T23:00:50',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROPERTYSETTEMPLATE('1TLQmgUKf0I8Zi07bm9Jqn',$,'ePSet_ProjectedCRS','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#2,#3,#4,#5,#6,#7)); +#2=IFCSIMPLEPROPERTYTEMPLATE('2cH53PtVT3v88uf_uGFblG',$,'Name','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +#3=IFCSIMPLEPROPERTYTEMPLATE('3JsReA6vXDqR7aNC$haRYW',$,'Description','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#4=IFCSIMPLEPROPERTYTEMPLATE('0u0uEsT5HFtuX5GvozO9ba',$,'GeodeticDatum','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); +#5=IFCSIMPLEPROPERTYTEMPLATE('1JgEX9Fsf0$ggxYltdkqiY',$,'VerticalDatum','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); +#6=IFCSIMPLEPROPERTYTEMPLATE('3ahTZjYrD0MAOo5IH6QuGH',$,'MapProjection','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); +#7=IFCSIMPLEPROPERTYTEMPLATE('3hqJR2ghD0GAti7q4sXKj7',$,'MapZone','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); +#8=IFCPROPERTYSETTEMPLATE('1jGHTycOj0vA8xHfRl4TNs',$,'ePSet_MapConversion','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#9,#10,#11,#12,#13,#15)); +#9=IFCSIMPLEPROPERTYTEMPLATE('0nxsULXo17JRrAvNteYtNR',$,'Eastings','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); +#10=IFCSIMPLEPROPERTYTEMPLATE('26RfaGJs5CqemN0Xin12XL',$,'Northings','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); +#11=IFCSIMPLEPROPERTYTEMPLATE('0ziCn3nZv3m850VEUwWH42',$,'OrthogonalHeight','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); +#12=IFCSIMPLEPROPERTYTEMPLATE('3d4BOt4In8WR119ms1SaAe',$,'XAxisAbscissa','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#13=IFCSIMPLEPROPERTYTEMPLATE('1Q4Voqj2j84RQRI8l67m28',$,'XAxisOrdinate','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#15=IFCSIMPLEPROPERTYTEMPLATE('0O1Hc0$XnDNOG_jPBmbW5r',$,'Scale','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +ENDSEC; +END-ISO-10303-21; From a847b8f4e47a20d841e6437deb0ac74773c9ee9e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 28 Sep 2023 18:50:47 +0500 Subject: [PATCH 03/15] bim.remove_opening not to force Model/Body/Model_view Now it won't be forcing Model/Body/Model_view representation on the objects if some other context is active for the object. Indirectly solves #3817 --- src/blenderbim/blenderbim/bim/module/void/operator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/void/operator.py b/src/blenderbim/blenderbim/bim/module/void/operator.py index b3b914b256..78b9c619f7 100644 --- a/src/blenderbim/blenderbim/bim/module/void/operator.py +++ b/src/blenderbim/blenderbim/bim/module/void/operator.py @@ -147,14 +147,12 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator): for building_element in decomposed_building_elements: building_obj = tool.Ifc.get_object(building_element) if building_obj and building_obj.data: - body = ifcopenshell.util.representation.get_representation( - building_element, "Model", "Body", "MODEL_VIEW" - ) + representation = tool.Geometry.get_active_representation(building_obj) blenderbim.core.geometry.switch_representation( tool.Ifc, tool.Geometry, obj=building_obj, - representation=body, + representation=representation, should_reload=True, is_global=True, should_sync_changes_first=False, From 3faaee80ec0a1b01f5e2c20958d3396a0a23c661 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Sep 2023 09:29:25 +0500 Subject: [PATCH 04/15] copy_deep to always pass exclude_callback to the recursion It wasn't passing the exclude_callback if attribute of the copied element wasn't a tuple, resulting in duplicated named profiles in case if you'd copy a IfcBooleanClippingResult #3810 For example, how copy_deep of IfcProductDefinitionShape with boolean clipping previously would work: V IfcShapeRepresentation - copied with exlude_callback (part of .Representations[]) V IfcBooleanClippingResult - copied with exlude_callback (part of .Items[]) X IfcExtrudedAreaSolid - copied without exlude_callback becuase it's part of .FirstOperand (not an array) and we have IfcIShapeProfileDef duplicated Case without booleanclippings: V IfcShapeRepresentation - copied with exlude_callback (part of .Representations[]) V IfcExtrudedAreaSolid - copied with exlude_callback, since it's part of .Items[], IfcIShapeProfileDef not duplicated --- src/ifcopenshell-python/ifcopenshell/util/element.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index ac84cc7046..4852ff067c 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -1022,7 +1022,13 @@ def copy_deep(ifc_file, element, exclude=None, exclude_callback=None, copied_ent elif exclude_callback and exclude_callback(attribute): pass else: - attribute = copy_deep(ifc_file, attribute, exclude=exclude, copied_entities=copied_entities) + attribute = copy_deep( + ifc_file, + attribute, + exclude=exclude, + copied_entities=copied_entities, + exclude_callback=exclude_callback, + ) elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance): if exclude and any([attribute[0].is_a(e) for e in exclude]): pass From 78aba1c7e4f7e408ec72236ba640454dabe69021 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Sep 2023 15:26:57 +0500 Subject: [PATCH 05/15] Workaround for #3778 --- src/blenderbim/blenderbim/bim/operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index a8a675ef91..7b491e58be 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -212,7 +212,7 @@ class FileAssociate(bpy.types.Operator): self.layout.label(text="On the next step to create file association ") self.layout.label(text="the system console will be opened ") self.layout.label(text=f"and you will be asked to type command") - self.layout.label(text=f'"{command}"') + self.layout.label(text=f"{command}") self.layout.label(text="to create an association.") def invoke(self, context, event): @@ -241,7 +241,7 @@ class FileAssociate(bpy.types.Operator): ps_script_path = os.path.join(src_dir, "windows_bbim_association.ps1") # NOTE: call powershell with RunAs to get admin rights from user - subprocess.run(["powershell", "-file", ps_script_path, binary_path], shell=True) + subprocess.run(["powershell", "-ExecutionPolicy", "Bypass", "-File", ps_script_path, binary_path], shell=True) def install_desktop_linux(self, src_dir=None, destdir="/tmp", binary_path="/usr/bin/blender"): """Creates linux file assocations and launcher icon""" From 5ed54d9e7c3ba6bab855364ab3bb80a6d66942b4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 29 Sep 2023 12:56:53 +1000 Subject: [PATCH 06/15] Revise COBie 2.4 contact table mapping in collaboration with Emma Hooper --- src/ifcfm/ifcfm/cobie24.py | 105 ++- src/ifcfm/ifcfm/cobie24legacy.py | 1509 ++++++++++++++++++++++++++++++ 2 files changed, 1562 insertions(+), 52 deletions(-) create mode 100644 src/ifcfm/ifcfm/cobie24legacy.py diff --git a/src/ifcfm/ifcfm/cobie24.py b/src/ifcfm/ifcfm/cobie24.py index 6f385f1278..5af1274cc0 100644 --- a/src/ifcfm/ifcfm/cobie24.py +++ b/src/ifcfm/ifcfm/cobie24.py @@ -35,7 +35,7 @@ import ifcopenshell.util.classification def get_contacts(ifc_file): - return ifc_file.by_type("IfcPersonAndOrganization") + return ifc_file.by_type("IfcActor") def get_facilities(ifc_file): @@ -265,51 +265,55 @@ def get_attributes(ifc_file): def get_contact_data(ifc_file, element): - email = get_email_from_pao(element) + the_actor = element.Theactor - history = get_history(ifc_file) + if the_actor.is_a("IfcPerson"): + pao = None + person = the_actor + organization = None + elif the_actor.is_a("IfcOrganization"): + pao = None + person = None + organization = the_actor + elif the_actor.is_a("IfcPersonAndOrganization"): + pao = the_actor + person = the_actor.ThePerson + organization = the_actor.TheOrganization - roles = [] - for actor in [element, element.ThePerson, element.TheOrganization]: + email = get_email_from_pao(person, organization) + + roles = set() + for actor in [pao, person, organization]: + if not actor: + continue for role in actor.Roles or []: if role.Role == "USERDEFINED": if role.UserDefinedRole: - roles.append(role.UserDefinedRole) + roles.add(role.UserDefinedRole) else: - roles.append(role.Role) - - organization = element.TheOrganization - person = element.ThePerson - - department = get_pao_address(element, "InternalLocation") - if not department: - for rel in organization.Relates: - for org in rel.RelatedOrganizations: - if val(org.Name): - department = org.Name + roles.add(role.Role) return { "key": email, "Email": email, - "CreatedBy": get_email_from_history(history) if history else None, - "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), "Category": ",".join(roles), "Company": getattr(organization, "Name", None), - "Phone": get_pao_address(element, "TelephoneNumbers"), - "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None, + "Phone": get_pao_address(person, organisation, "TelephoneNumbers"), + "ExternalSystem": get_external_system(element), "ExternalObject": element.is_a(), - "ExternalIdentifier": email, - "Department": department, - "OrganizationCode": getattr(organization, "Id", getattr(organization, "Identification", None)) - or organization.Name, + "ExternalIdentifier": element.GlobalId, + "Department": get_pao_address(person, organisation, "InternalLocation"), + "OrganizationCode": getattr(organization, "Id", getattr(organization, "Identification", None)), "GivenName": getattr(person, "GivenName", None), "FamilyName": getattr(person, "FamilyName", None), - "Street": get_pao_address(element, "AddressLines"), - "PostalBox": get_pao_address(element, "PostalBox"), - "Town": get_pao_address(element, "Town"), - "StateRegion": get_pao_address(element, "Region"), - "PostalCode": get_pao_address(element, "PostalCode"), - "Country": get_pao_address(element, "Country"), + "Street": get_pao_address(person, organisation, "AddressLines"), + "PostalBox": get_pao_address(person, organisation, "PostalBox"), + "Town": get_pao_address(person, organisation, "Town"), + "StateRegion": get_pao_address(person, organisation, "Region"), + "PostalCode": get_pao_address(person, organisation, "PostalCode"), + "Country": get_pao_address(person, organisation, "Country"), } @@ -961,28 +965,23 @@ def get_created_by(element): def get_email_from_history(element): pao = element.OwningUser if pao.is_a("IfcPersonAndOrganization"): - return get_email_from_pao(pao) + return get_email_from_pao(pao.ThePerson, pao.TheOrganization) + elif pao.is_a("IfcPerson"): + return get_email_from_pao(pao, None) + elif pao.is_a("IfcOrganization"): + return get_email_from_pao(None, pao) -def get_email_from_pao(pao): - for address in pao.ThePerson.Addresses or []: - if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: - return address.ElectronicMailAddresses[0] +def get_email_from_pao(person, organization): + if organization: + for address in organization.Addresses or []: + if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: + return address.ElectronicMailAddresses[0] - for address in pao.TheOrganization.Addresses or []: - if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: - return address.ElectronicMailAddresses[0] - - person_id = getattr(pao.ThePerson, "Identification", getattr(pao.ThePerson, "Id", None)) - if person_id: - return person_id - - organization_id = getattr(pao.TheOrganization, "Identification", getattr(pao.TheOrganization, "Id", None)) - if organization_id: - return organization_id - - if pao.ThePerson.GivenName and pao.ThePerson.FamilyName and pao.TheOrganization.Name: - return pao.ThePerson.GivenName + pao.ThePerson.FamilyName + "@" + pao.TheOrganization.Name + ".com" + if person: + for address in person.Addresses or []: + if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: + return address.ElectronicMailAddresses[0] def get_owner_name(element): @@ -1076,8 +1075,10 @@ def get_category(element): return val(getattr(element, "ObjectType", None)) -def get_pao_address(element, name): - for actor in [element.ThePerson, element.TheOrganization]: +def get_pao_address(person, organization, name): + for actor in [organization, person]: + if not actor: + continue for address in actor.Addresses or []: if hasattr(address, name) and getattr(address, name, None): result = getattr(address, name) diff --git a/src/ifcfm/ifcfm/cobie24legacy.py b/src/ifcfm/ifcfm/cobie24legacy.py new file mode 100644 index 0000000000..6f385f1278 --- /dev/null +++ b/src/ifcfm/ifcfm/cobie24legacy.py @@ -0,0 +1,1509 @@ +# IfcFM - IFC for facility management +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcFM. +# +# IfcFM 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. +# +# IfcFM 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 IfcFM. If not, see . + +import ifcopenshell +import ifcopenshell.util.fm +import ifcopenshell.util.date +import ifcopenshell.util.system +import ifcopenshell.util.placement +import ifcopenshell.util.classification + + +# The original BIMServer plugin has a function called ifcToCOBie: +# https://github.com/opensourceBIM/COBie-plugins/blob/master/COBieShared/src/org/bimserver/cobie/shared/serialization/COBieTabSerializer.java#L54 +# This calls various serialisers here: +# https://github.com/opensourceBIM/COBie-plugins/tree/master/COBieShared/src/org/bimserver/cobie/shared/serialization/util +# Some settings are also defined here: +# https://github.com/opensourceBIM/COBie-plugins/blob/master/COBiePlugins/lib/IfcToCobieConfig.xml +# Note that the following categories are not implemented in the BIMServer COBie-Plugins: +# Impact, Coordinate, Issue, Picklist + + +def get_contacts(ifc_file): + return ifc_file.by_type("IfcPersonAndOrganization") + + +def get_facilities(ifc_file): + return ifc_file.by_type("IfcBuilding") + + +def get_floors(ifc_file): + return [ + e + for e in ifc_file.by_type("IfcBuildingStorey") + if ifcopenshell.util.element.get_aggregate(e).is_a("IfcBuilding") + ] + + +def get_spaces(ifc_file): + return ifc_file.by_type("IfcSpace") + + +def get_zones(ifc_file): + results = [] + zones = ifc_file.by_type("IfcZone") + for zone in zones or []: + has_space = False + for rel in zone.IsGroupedBy: + items = [(zone, space) for space in rel.RelatedObjects if space.is_a("IfcSpace") and val(space.Name)] + if items: + results.extend(items) + has_space = True + if not has_space: + results.append((zone, None)) + if zones: + return results + + zone_spaces = {} + for space in ifc_file.by_type("IfcSpace"): + for _, props in ifcopenshell.util.element.get_psets(space).items(): + for name, value in props.items(): + if "ZoneName" in name: + zone_name = val(value) + space_name = val(space.Name) + category = name + zone_key = str(name) + "," + str(category) + zone_spaces.setdefault(zone_key, []) + if space_name not in zone_spaces[zone_key]: + zone_spaces[zone_key].append(space_name) + results.append(((zone_name, category), space_name)) + return results + + +def get_types(ifc_file): + return ifcopenshell.util.fm.get_cobie_types(ifc_file) + + +def get_components(ifc_file): + elements = set() + for element_type in get_types(ifc_file): + elements.update(ifcopenshell.util.element.get_types(element_type)) + return elements + + +def get_systems(ifc_file): + results = [] + components = get_components(ifc_file) + if ifc_file.schema == "IFC2X3": + systems = ifc_file.by_type("IfcSystem", include_subtypes=False) + else: + systems = ifc_file.by_type("IfcSystem", include_subtypes=False) + ifc_file.by_type("IfcDistributionSystem") + for system in systems: + for element in ifcopenshell.util.system.get_system_elements(system): + if element in components: + results.append((system, element)) + return results + + +def get_assemblies(ifc_file): + results = [] + layer_sets = ifc_file.by_type("IfcMaterialLayerSet") + layer_sets = [] # This is temporarily overridden because it is unclear exactly how this is stored in Type. + for layer_set in layer_sets: + for layer in layer_set.MaterialLayers: + results.append((None, layer_set, layer.Material)) + rels = ifc_file.by_type("IfcRelAggregates") + ifc_file.by_type("IfcRelNests") + types = get_types(ifc_file) + components = get_components(ifc_file) + for rel in rels: + if rel.RelatingObject.is_a("IfcSpace"): + for related_object in rel.RelatedObjects: + if related_object.is_a("IfcSpace"): + results.append((rel, rel.RelatingObject, related_object)) + elif rel.RelatingObject.is_a("IfcZone"): + for related_object in rel.RelatedObjects: + if related_object.is_a("IfcZone"): + results.append((rel, rel.RelatingObject, related_object)) + elif rel.RelatingObject in types: + for related_object in rel.RelatedObjects: + if related_object in types: + results.append((rel, rel.RelatingObject, related_object)) + elif rel.RelatingObject in components: + for related_object in rel.RelatedObjects: + if related_object in components: + results.append((rel, rel.RelatingObject, related_object)) + elif rel.RelatingObject.is_a() in ("IfcSystem", "IfcDistributionSystem"): + for related_object in rel.RelatedObjects: + if related_object.is_a() in ("IfcSystem", "IfcDistributionSystem"): + results.append((rel, rel.RelatingObject, related_object)) + return results + + +def get_connections(ifc_file): + return ifc_file.by_type("IfcRelConnectsPorts") + + +def get_spares(ifc_file): + return ifc_file.by_type("IfcConstructionProductResource") + + +def get_resources(ifc_file): + return ifc_file.by_type("IfcConstructionEquipmentResource") + + +def get_jobs(ifc_file): + return ifc_file.by_type("IfcTask") + + +def get_documents(ifc_file): + # The original COBie-Plugins assumes a single related object per rel. I think this was wrong. + results = [] + for rel in ifc_file.by_type("IfcRelAssociatesDocument"): + doc = rel.RelatingDocument + if doc.is_a("IfcDocumentInformation"): + for related_object in rel.RelatedObjects: + results.append((rel, doc, related_object)) + elif doc.is_a("IfcDocumentReference") and doc.ReferencedDocument: + for related_object in rel.RelatedObjects: + results.append((rel, doc.ReferencedDocument, related_object)) + return results + + +def get_attributes(ifc_file): + results = [] + history = get_history(ifc_file) + created_by = get_email_from_history(history) if history else None + created_on = ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None + external_system = history.OwningApplication.ApplicationFullName if history else None + get_sheets = { + "Facility": get_facilities, + "Floor": get_floors, + "Space": get_spaces, + "Type": get_types, + "Component": get_components, + } + + # COBie-Plugins includes what seems like a whole bunch of arbitrary "at the + # time it seemed to help" exclusion names. I don't like that strategy. If + # you've got garbage in your model, clean it out first. + + # fmt: off + excluded_names = { + "Manufacturer", + "ModelNumber", "ArticleNumber", "ModelLabel", + "WarrantyGuarantorParts", "PointOfContact", + "WarrantyGuarantorLabor", "PointOfContact", + "WarrantyDescription", "WarrantyIdentifier", + "ReplacementCost", "Replacement Cost", "Replacement", "Cost", + "NominalLength", "OverallLength", + "NominalWidth", "Width", + "NominalHeight", "Height", + "ModelReference", "Reference", + "Shape", + "Size", + "Color", "Colour", + "Finish", + "Grade", + "Material", + "Constituents", "Parts", + "Features", + "AccessibilityPerformance", "Access", + "CodePerformance", "Regulation", + "SustainabilityPerformance", "Environmental", + "SerialNumber", "InstallationDate", "WarrantyStartDate", "TagNumber", "BarCode", "AssetIdentifier" + } + # fmt: on + + for sheet_name, get_sheet in get_sheets.items(): + for element in get_sheet(ifc_file): + for pset_name, props in ifcopenshell.util.element.get_psets(element).items(): + pset = ifc_file.by_id(props["id"]) + pset_created_by = get_created_by(pset) or created_by + pset_created_on = get_created_on(pset) or created_on + pset_external_system = get_external_system(element) or external_system + pset_description = val(pset.Description) or pset_name + category = get_category(pset) + for name, value in props.items(): + if value == "default" or not val(value): + continue + elif name in excluded_names: + continue + elif name == "id": + continue + + unit = None + if isinstance(value, (int, float)): + unit = get_property_unit(props["id"], name) + + allowed_values = None + if isinstance(value, (tuple, list)): + allowed_values = get_property_unit(props["id"], name) + + data = { + "key": str(val(name)) + str(sheet_name) + str(val(element.Name)), + "Name": val(name), + "CreatedBy": pset_created_by, + "CreatedOn": pset_created_on, + "Category": category, + "SheetName": sheet_name, + "RowName": val(element.Name), + "Value": value, + "Unit": unit, + "ExternalSystem": pset_external_system, + "ExternalObject": pset_name, + "ExternalIdentifier": pset.GlobalId, + "Description": pset_description, + "AllowedValues": allowed_values, + } + results.append(data) + return results + + +def get_contact_data(ifc_file, element): + email = get_email_from_pao(element) + + history = get_history(ifc_file) + + roles = [] + for actor in [element, element.ThePerson, element.TheOrganization]: + for role in actor.Roles or []: + if role.Role == "USERDEFINED": + if role.UserDefinedRole: + roles.append(role.UserDefinedRole) + else: + roles.append(role.Role) + + organization = element.TheOrganization + person = element.ThePerson + + department = get_pao_address(element, "InternalLocation") + if not department: + for rel in organization.Relates: + for org in rel.RelatedOrganizations: + if val(org.Name): + department = org.Name + + return { + "key": email, + "Email": email, + "CreatedBy": get_email_from_history(history) if history else None, + "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, + "Category": ",".join(roles), + "Company": getattr(organization, "Name", None), + "Phone": get_pao_address(element, "TelephoneNumbers"), + "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None, + "ExternalObject": element.is_a(), + "ExternalIdentifier": email, + "Department": department, + "OrganizationCode": getattr(organization, "Id", getattr(organization, "Identification", None)) + or organization.Name, + "GivenName": getattr(person, "GivenName", None), + "FamilyName": getattr(person, "FamilyName", None), + "Street": get_pao_address(element, "AddressLines"), + "PostalBox": get_pao_address(element, "PostalBox"), + "Town": get_pao_address(element, "Town"), + "StateRegion": get_pao_address(element, "Region"), + "PostalCode": get_pao_address(element, "PostalCode"), + "Country": get_pao_address(element, "Country"), + } + + +def get_facility_data(ifc_file, element): + site = get_facility_parent(element, "IfcSite") + site_name = None + site_description = None + if site: + site_name = val(site.Name) or val(site.LongName) or site.GlobalId + site_description = val(site.Description) or val(site.LongName) or val(site.Name) + + project = None + project_name = None + project_description = None + try: + project = ifc_file.by_type("IfcProject")[0] + project_name = val(project.Name) or val(project.LongName) or project.GlobalId + project_description = val(project.Description) or val(project.LongName) or val(project.Name) + except: + pass + + name = val(element.Name) or val(element.LongName) + if not name: + name = val(project.Name) or val(project.LongName) + if not name and site: + name = val(site.Name) or val(site.LongName) + + return { + "key": name, + "Name": name, + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "ProjectName": project_name, + "SiteName": site_name, + "LinearUnits": get_unit_type_name(ifc_file, "LENGTHUNIT"), + "AreaUnits": get_unit_type_name(ifc_file, "AREAUNIT"), + "VolumeUnits": get_unit_type_name(ifc_file, "VOLUMEUNIT"), + "CurrencyUnit": get_unit_type_name(ifc_file, "IfcMonetaryUnit"), + "AreaMeasurement": get_area_measurement(element), + "ExternalSystem": get_external_system(element), + "ExternalProjectObject": "IfcProject", + "ExternalProjectIdentifier": project.GlobalId if project else ifcopenshell.guid.new(), + "ExternalSiteObject": "IfcSite", + "ExternalSiteIdentifier": site.GlobalId if site else ifcopenshell.guid.new(), + "ExternalFacilityObject": "IfcBuilding", + "ExternalFacilityIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "ProjectDescription": project_description, + "SiteDescription": site_description, + "Phase": val(project.Phase) if project else None, + } + + +def get_floor_data(ifc_file, element): + external_object = element.is_a() + if element.ObjectType and element.ObjectType.lower() in ("site", "ifcsite"): + external_object = "IfcSite" + + height_names = { + "Height", + "NetHeight", + "GrossHeight", + "Net Height", + "Gross Height", + "StoreyHeight", + "Storey Height", + "FloorHeight", + "Floor Height", + } + + height = None + for _, props in ifcopenshell.util.element.get_psets(element).items(): + if height is not None: + break + for name, value in props.items(): + if name in height_names and val(value): + height = str(value) + break + + elevation = getattr(element, "Elevation", "") + elevation = "" if elevation is None else str(elevation) + + return { + "key": val(element.Name), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "ExternalSystem": get_external_system(element), + "ExternalObject": external_object, + "ExternalIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "Elevation": val(elevation), + "Height": height, + } + + +def get_space_data(ifc_file, element): + floor_name = None + floor = ifcopenshell.util.element.get_aggregate(element) + if floor and floor.is_a("IfcBuildingStorey"): + floor_name = val(floor.Name) + + room_tag = None + room_tag_names = {"RoomTag", "Tag", "Room Tag"} + usable_height = None + usable_height_names = {"FinishCeilingHeight", "Height", "UsableHeight"} + gross_area = None + gross_area_names = {"GrossFloorArea", "GSA"} + net_area = None + net_area_names = {"NetFloorArea", "GSA"} + for _, props in ifcopenshell.util.element.get_psets(element).items(): + for name, value in props.items(): + if not room_tag and name in room_tag_names and val(value): + room_tag = str(value) + if not usable_height and name in usable_height_names and val(value): + usable_height = str(value) + if not gross_area and name in gross_area_names and val(value): + gross_area = str(value) + if not net_area and name in net_area_names and val(value): + net_area = str(value) + + return { + "key": val(element.Name), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "FloorName": floor_name, + "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "RoomTag": room_tag, + "UsableHeight": usable_height, + "GrossArea": gross_area, + "NetArea": net_area, + } + + +def get_zone_data(ifc_file, element): + zone, space = element + + if isinstance(zone, tuple): + name, category = zone + history = get_history(ifc_file) + return { + "key": "-".join([str(name), str(category), str(space)]), + "Name": name, + "CreatedBy": get_email_from_history(history) if history else None, + "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, + "Category": category, + "SpaceNames": space, + "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None, + "ExternalObject": "IfcPropertySingleValue", + "ExternalIdentifier": None, + "Description": val(name) or val(category), + } + + name = zone.Name + parent = ifcopenshell.util.element.get_aggregate(zone) + if parent and val(parent.Name): + name = parent.Name + "-" + name + + category = get_category(zone) + space_name = val(space.Name) if space else None + + return { + "key": "-".join([str(name), str(category), str(space_name)]), + "Name": name, + "CreatedBy": get_created_by(zone), + "CreatedOn": get_created_on(zone), + "Category": category, + "SpaceNames": space_name, + "ExternalSystem": get_external_system(zone), + "ExternalObject": zone.is_a(), + "ExternalIdentifier": zone.GlobalId, + "Description": val(zone.Description) or zone.Name, + } + + +def get_type_data(ifc_file, element): + pset_metadata = {} + pset_mapping = { + "manufacturer": {"Manufacturer"}, + "model_number": {"ModelNumber", "ArticleNumber", "ModelReference"}, + "warranty_guarantor_parts": {"WarrantyGuarantorParts", "PointOfContact"}, + "warranty_guarantor_labor": {"WarrantyGuarantorLabor", "PointOfContact"}, + "warranty_description": {"WarrantyDescription", "WarrantyIdentifier"}, + "replacement_cost": {"ReplacementCost", "Replacement Cost", "Replacement", "Cost"}, + "nominal_length": {"NominalLength", "OverallLength", "Length"}, + "nominal_width": {"NominalWidth", "OverallWidth", "Width"}, + # https://github.com/opensourceBIM/COBie-plugins/blob/master/COBiePlugins/lib/IfcToCobieConfig.xml#L104 + "nominal_height": {"NominalHeight", "Height"}, # Original has a typo "Heght" + "model_reference": {"ModelLabel"}, # I believe this is what the intention was, not "ModelReference". + "shape": {"Shape"}, + "size": {"Size"}, + "color": {"Color", "Colour"}, + "finish": {"Finish"}, + "grade": {"Grade"}, + "material": {"Material"}, + "constituents": {"Constituents", "Parts"}, + "features": {"Features"}, + "accessibility_performance": {"AccessibilityPerformance", "Access"}, + "code_performance": {"CodePerformance", "Regulation"}, + "sustainability_performance": {"SustainabilityPerformance", "Environmental"}, + } + asset_type = None + asset_type_names = {"AssetType", "AssetAccountingType"} + warranty_duration_parts = None + warranty_duration_parts_names = {"WarrantyDurationParts", "WarrantyPeriod"} + warranty_duration_labor = None + warranty_duration_labor_names = {"WarrantyDurationLabor", "WarrantyPeriod"} + warranty_duration_unit = None + expected_life = None + expected_life_names = {"ExpectedLife", "Expected Life", "ServiceLifeDuration", "Expected"} + duration_unit = None + + for pset_name, props in ifcopenshell.util.element.get_psets(element).items(): + pset_warranty_type = None + if pset_name == "Pset_Warranty": + if "parts" in (props.get("WarrantyIdentifier", "") or "").lower(): + pset_warranty_type = "parts" + elif "labor" in (props.get("WarrantyIdentifier", "") or "").lower(): + pset_warranty_type = "labor" + + pset = ifc_file.by_id(props["id"]) + + for name, value in props.items(): + for key, prop_names in pset_mapping.items(): + if not pset_metadata.get(key, None) and name in prop_names and val(value): + pset_metadata[key] = str(value) + + if not asset_type and name in asset_type_names and val(value): + value = value.strip().lower() + if value in ("moveable", "nonfixed"): + asset_type = "Moveable" + elif value == "fixed": + asset_type = "Fixed" + if not warranty_duration_parts and name in warranty_duration_parts_names and val(value): + warranty_duration_parts = str(value) + if not warranty_duration_unit: + warranty_duration_unit = get_property_unit(pset, name) + if not warranty_duration_labor and name in warranty_duration_labor_names and val(value): + warranty_duration_labor = str(value) + if not warranty_duration_unit: + warranty_duration_unit = get_property_unit(pset, name) + if not expected_life and name in expected_life_names and val(value): + expected_life = str(value) + if not duration_unit: + duration_unit = get_property_unit(pset, name) + + if pset_warranty_type == "parts" and val(value): + if name == "PointOfContact": + # https://github.com/buildingSMART/IFC4.3.x-development/issues/698 + warranty_guarantor_parts = str(value) + elif name == "WarrantyPeriod": + warranty_duration_parts = str(value) + unit = get_property_unit(pset, name) + warranty_duration_unit = unit or warranty_duration_unit + elif pset_warranty_type == "labor" and val(value): + if name == "PointOfContact": + # https://github.com/buildingSMART/IFC4.3.x-development/issues/698 + warranty_guarantor_labor = str(value) + elif name == "WarrantyPeriod": + warranty_duration_labor = str(value) + unit = get_property_unit(pset, name) + warranty_duration_unit = unit or warranty_duration_unit + + if warranty_duration_parts or warranty_duration_labor: + if not warranty_duration_unit: + warranty_duration_unit = get_unit_type_name(ifc_file, "TIMEUNIT") + + if not asset_type and element.is_a("IfcFurnitureType"): + asset_type = "Moveable" + + return { + "key": val(element.Name), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "Description": val(element.Description) or val(element.Name), + "AssetType": asset_type, + "Manufacturer": pset_metadata.get("manufacturer", None), + "ModelNumber": pset_metadata.get("model_number", None), + "WarrantyGuarantorParts": pset_metadata.get("warranty_guarantor_parts", None), + "WarrantyDurationParts": warranty_duration_parts, + "WarrantyGuarantorLabor": pset_metadata.get("warranty_guarantor_labor", None), + "WarrantyDurationLabor": warranty_duration_labor, + "WarrantyDurationUnit": warranty_duration_unit, + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "ReplacementCost": pset_metadata.get("replacement_cost", None), + "ExpectedLife": expected_life, + "DurationUnit": duration_unit, + "WarrantyDescription": pset_metadata.get("warranty_description", None), + "NominalLength": pset_metadata.get("nominal_length", None), + "NominalWidth": pset_metadata.get("nominal_width", None), + "NominalHeight": pset_metadata.get("nominal_height", None), + "ModelReference": pset_metadata.get("model_reference", None), + "Shape": pset_metadata.get("shape", None), + "Size": pset_metadata.get("size", None), + "Color": pset_metadata.get("color", None), + "Finish": pset_metadata.get("finish", None), + "Grade": pset_metadata.get("grade", None), + "Material": pset_metadata.get("material", None), + "Constituents": pset_metadata.get("constituents", None), + "Features": pset_metadata.get("features", None), + "AccessibilityPerformance": pset_metadata.get("accessibility_performance", None), + "CodePerformance": pset_metadata.get("code_performance", None), + "SustainabilityPerformance": pset_metadata.get("sustainability_performance", None), + } + + +def get_component_data(ifc_file, element): + space = ifcopenshell.util.element.get_container(element) + space_name = space.Name if space.is_a("IfcSpace") else None + systems = ifcopenshell.util.system.get_element_systems(element) + system = systems[0].Name if systems else None + + type_name = None + relating_type = ifcopenshell.util.element.get_type(element) + if relating_type and val(relating_type.Name): + type_name = relating_type.Name + else: + material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) + type_name = getattr(material, "Name", None) or getattr(material, "LayerSetName", None) + + serial_number = None + installation_date = None + warranty_start_date = None + tag_number = None + bar_code = None + asset_identifier = None + + for _, props in ifcopenshell.util.element.get_psets(element).items(): + for name, value in props.items(): + if not serial_number and name == "SerialNumber" and val(value): + serial_number = str(value) + if not installation_date and name == "InstallationDate" and val(value): + installation_date = str(value) + if not warranty_start_date and name == "WarrantyStartDate" and val(value): + warranty_start_date = str(value) + if not tag_number and name == "TagNumber" and val(value): + tag_number = str(value) + if not bar_code and name == "BarCode" and val(value): + bar_code = str(value) + if not asset_identifier and name == "AssetIdentifier" and val(value): + asset_identifier = str(value) + + return { + "key": element.Name, + "Name": element.Name, + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "TypeName": type_name, + "Space": space_name, + "Description": val(element.Description) or val(element.Name), + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "SerialNumber": serial_number, + "InstallationDate": installation_date, + "WarrantyStartDate": warranty_start_date, + "TagNumber": tag_number, + "BarCode": bar_code, + "AssetIdentifier": asset_identifier, + } + + +def get_system_data(ifc_file, element): + system, component = element + category = get_category(system) + component_name = val(component.Name) + return { + "key": str(val(system.Name)) + str(category) + str(component_name), + "Name": val(system.Name), + "CreatedBy": get_created_by(system), + "CreatedOn": get_created_on(system), + "Category": get_category(system), + "ComponentNames": component_name, + "ExternalSystem": get_external_system(system), + "ExternalObject": system.is_a(), + "ExternalIdentifier": system.GlobalId, + "Description": val(system.Description) or val(system.Name), + } + + +def get_assembly_data(ifc_file, element): + rel, relating_object, related_object = element + + if relating_object.is_a("IfcMaterialLayerSet"): + name = val(relating_object.LayerSetName) + parent_name = name + if name: + name += " assembly" + assembly_type = "Layer" + sheet_name = "Type" + description = val(relating_object.LayerSetName) + else: + name = val(relating_object.Name) + parent_name = name + assembly_type = "Fixed" + sheet_name = get_sheet_name(relating_object) + description = val(rel.Description) or val(rel.Name) + + child_name = val(related_object.Name) + history = get_history(ifc_file) + + return { + "key": str(name) + str(sheet_name) + str(parent_name), + "Name": name, + "CreatedBy": get_email_from_history(history) if history else None, + "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, + "SheetName": sheet_name, + "ParentName": parent_name, + "ChildNames": child_name, + "AssemblyType": assembly_type, + "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None, + "ExternalObject": rel.is_a() if rel else relating_object.is_a(), + "ExternalIdentifier": rel.GlobalId if rel else None, + "Description": description, + } + + +def get_connection_data(ifc_file, element): + connection_type = ( + val(element.RelatingPort.ObjectType) + or val(element.RelatedPort.ObjectType) + or val(element.Description) + or val(element.Name) + ) + name = val(element.Name) + row_name1 = val(ifcopenshell.util.system.get_port_element(element.RelatingPort).Name) + row_name2 = val(ifcopenshell.util.system.get_port_element(element.RelatedPort).Name) + return { + "key": str(name) + str(connection_type) + str(row_name1) + str(row_name2), + "Name": name, + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "ConnectionType": connection_type, + "SheetName": "Component", + "RowName1": row_name1, + "RowName2": row_name2, + "RealizingElement": val(element.RealizingElement.Name) if element.RealizingElement else None, + "PortName1": val(element.RelatingPort.Name), + "PortName2": val(element.RelatedPort.Name), + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.Name), + } + + +def get_spare_data(ifc_file, element): + type_name = None + for rel in element.ResourceOf or []: + for related_object in rel.RelatedObjects or []: + if val(related_object.Name): + type_name = val(related_object.Name) + + suppliers = None + set_number = None + part_number = None + for _, props in ifcopenshell.util.element.get_psets(element).items(): + for name, value in props.items(): + if name == "Suppliers" and val(value): + suppliers = str(value) + if name == "SetNumber" and val(value): + set_number = str(value) + if name == "PartNumber" and val(value): + part_number = str(value) + + return { + "key": val(element), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "TypeName": type_name, + "Suppliers": suppliers, + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.Name), + "SetNumber": set_number, + "PartNumber": part_number, + } + + +def get_resource_data(ifc_file, element): + return { + "key": val(element.Name), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": val(element.ObjectType), + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.Name), + } + + +def get_job_data(ifc_file, element): + type_names = [] + resource_names = [] + for rel in element.OperatesOn or []: + for related_object in rel.RelatedObjects or []: + if not val(related_object.Name): + continue + if related_object.is_a("IfcTypeObject"): + type_names.append(related_object.Name) + elif related_object.is_a("IfcConstructionEquipmentResource"): + resource_names.append(related_object.Name) + type_name = ",".join(type_names) if type_names else None + resource_names = ",".join(resource_names) if resource_names else None + + duration = None + duration_unit = None + start = None + task_start_unit = None + frequency = None + frequency_unit = None + + for _, props in ifcopenshell.util.element.get_psets(element).items(): + pset = ifc_file.by_id(props["id"]) + for name, value in props.items(): + if not duration and name == "TaskDuration" and val(value): + duration = str(value) + if not duration_unit: + duration_unit = get_property_unit(pset, name) + if not start and name == "TaskStartDate" and val(value): + start = str(value) + if not task_start_unit: + task_start_unit = get_property_unit(pset, name) + if not frequency and name == "TaskInterval" and val(value): + frequency = str(value) + if not frequency_unit: + frequency_unit = get_property_unit(pset, name) + + task_number = val(getattr(element, "Id", None)) or val(getattr(element, "Identification", None)) + + priors = [] + for rel in element.IsSuccessorFrom or []: + if rel.RelatedProcess.is_a("IfcTask"): + prior_task = rel.RelatedProcess + prior_id = val(getattr(prior_task, "Id", None)) or val(getattr(prior_task, "Identification", None)) + if prior_id: + priors.append(prior_id) + priors = ",".join(priors) if priors else task_number + + return { + "key": str(val(element.Name)) + str(type_name) + str(task_number), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": val(element.ObjectType), + "Status": val(element.Status), + "TypeName": type_name, + "Description": val(element.Description) or val(element.Name), + "Duration": duration, + "DurationUnit": duration_unit, + "Start": start, + "TaskStartUnit": task_start_unit, + "Frequency": frequency, + "FrequencyUnit": frequency_unit, + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "TaskNumber": task_number, + "Priors": priors, + "ResourceNames": resource_names, + } + + +def get_document_data(ifc_file, element): + rel, doc, related_object = element + directory = getattr(doc, "Location", None) + file = None + if not directory: + references = getattr(doc, "DocumentReferences", []) or getattr(doc, "HasDocumentReferences", []) + for reference in references or []: + if val(reference.Location): + directory = reference.Location + identification = getattr(reference, "ItemReference", None) or getattr(reference, "Identification", None) + if val(reference.Name) and doc.Name != reference.Name: + file = reference.Name + elif val(identification): + file = identification + name = val(doc.Name) + stage = val(doc.Scope) or "Requirement" + sheet_name = get_sheet_name(related_object) + row_name = val(related_object.Name) + return { + "key": str(name) + str(stage) + str(sheet_name) + str(row_name), + "Name": name, + "CreatedBy": get_created_by(rel), + "CreatedOn": get_created_on(rel), + "Category": val(doc.Purpose), + "ApprovalBy": val(doc.IntendedUse) or "Information Only", + "Stage": stage, + "SheetName": sheet_name, + "RowName": row_name, + "Directory": directory, + "File": file, + "ExternalSystem": get_external_system(rel), + "ExternalObject": rel.is_a(), + "ExternalIdentifier": rel.GlobalId, + "Description": val(doc.Description), + "Reference": val(doc.Description) or val(doc.Name), + } + + +def get_attribute_data(ifc_file, element): + return element + + +def get_unit_type_name(ifc_file, unit_type): + for unit in ifc_file.by_type("IfcUnitAssignment")[0].Units: + if unit.is_a("IfcNamedUnit") and unit.UnitType == unit_type: + if unit.is_a("IfcSIUnit"): + prefix = (unit.Prefix or "").lower() + if unit_type == "LENGTHUNIT": + return f"{prefix}meters" + elif unit_type == "AREAUNIT": + return f"square {prefix}meters" + elif unit_type == "VOLUMEUNIT": + return f"cubic {prefix}meters" + else: + return val(unit.Name) + elif unit.is_a("IfcMonetaryUnit") and unit_type == "IfcMonetaryUnit": + return val(unit.Currency) + + +def get_unit_name(ifc_file, unit): + if unit.is_a("IfcNamedUnit"): + return val(unit.Name) + + +def get_created_by(element): + if getattr(element, "OwnerHistory", None): + return get_email_from_history(element.OwnerHistory) + + +def get_email_from_history(element): + pao = element.OwningUser + if pao.is_a("IfcPersonAndOrganization"): + return get_email_from_pao(pao) + + +def get_email_from_pao(pao): + for address in pao.ThePerson.Addresses or []: + if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: + return address.ElectronicMailAddresses[0] + + for address in pao.TheOrganization.Addresses or []: + if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: + return address.ElectronicMailAddresses[0] + + person_id = getattr(pao.ThePerson, "Identification", getattr(pao.ThePerson, "Id", None)) + if person_id: + return person_id + + organization_id = getattr(pao.TheOrganization, "Identification", getattr(pao.TheOrganization, "Id", None)) + if organization_id: + return organization_id + + if pao.ThePerson.GivenName and pao.ThePerson.FamilyName and pao.TheOrganization.Name: + return pao.ThePerson.GivenName + pao.ThePerson.FamilyName + "@" + pao.TheOrganization.Name + ".com" + + +def get_owner_name(element): + if getattr(element, "OwnerHistory", None): + return element.OwnerHistory.OwningUser.TheOrganization.Name + + +def get_created_on(element): + if getattr(element, "OwnerHistory", None): + return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() + + +def get_external_system(element): + if getattr(element, "OwnerHistory", None): + return val(element.OwnerHistory.OwningApplication.ApplicationFullName) + + +def get_facility_parent(element, ifc_class): + parent = ifcopenshell.util.element.get_aggregate(element) + while parent: + if parent.is_a(ifc_class): + return parent + if parent.is_a("IfcProject"): + return + parent = ifcopenshell.util.element.get_aggregate(parent) + + +def val(x): + return x if x not in ("", "n/a") else None + + +def get_area_measurement(element): + for relationship in getattr(element, "IsDefinedBy", []) or []: + if relationship.is_a("IfcRelDefinesByProperties"): + definition = relationship.RelatingPropertyDefinition + if definition.is_a("IfcElementQuantity") and val(definition.MethodOfMeasurement): + return definition.MethodOfMeasurement + for rel in getattr(element, "IsDecomposedBy", []): + for related_object in rel.RelatedObjects: + result = get_area_measurement(related_object) + if result: + return result + + +def get_category(element): + references = list(ifcopenshell.util.classification.get_references(element)) + results = [] + for reference in references: + if reference.is_a("IfcClassification"): + results.append(reference.Name) + elif reference.is_a("IfcClassificationReference"): + identification = val(getattr(reference, "Identification", getattr(reference, "ItemReference", None))) + if val(reference.Name) and identification and val(reference.Name) != identification: + results.append(identification + " : " + val(reference.Name)) + elif val(reference.Name): + results.append(reference.Name) + elif identification: + results.append(identification) + elif reference.ReferencedSource and val(reference.ReferencedSource.Name): + results.append(reference.ReferencedSource.Name) + elif val(reference.Location): + results.append(reference.Location) + if results: + return ",".join(results) + + category_props = [ + ("Assembly Code", "Assembly Description"), + ("Category Code", "Category Description"), + ("Classification Code", "Classification Description"), + ("OmniClass Number", "OmniClass Title"), + ("Uniclass Code", "Uniclass Description"), + ] + + psets = ifcopenshell.util.element.get_psets(element) + properties = {} + if psets: + for _, props in psets.items(): + properties.update(props) + + for code, description in category_props: + code = val(properties.get(code, None)) + if code: + description = val(properties.get(description, None)) + if code and description: + results.append(code + " : " + description) + else: + results.append(code) + if results: + return ",".join(results) + + return val(getattr(element, "ObjectType", None)) + + +def get_pao_address(element, name): + for actor in [element.ThePerson, element.TheOrganization]: + for address in actor.Addresses or []: + if hasattr(address, name) and getattr(address, name, None): + result = getattr(address, name) + if isinstance(result, tuple): + if name == "AddressLines": + return " ".join(result) + return result[0] + return result + + +def get_property(psets, pset_name, prop_name, decimals=None): + if pset_name in psets: + result = psets[pset_name].get(prop_name, None) + if decimals is None or result is None: + return result + return round(result, decimals) + + +def get_history(ifc_file): + histories = ifc_file.by_type("IfcOwnerHistory") + if histories: + return sorted(histories, key=lambda x: x.id())[-1] + + +def get_property_unit(pset, prop_name): + for prop in getattr(pset, "HasProperties", []) or []: + if prop.Name == prop_name: + unit = getattr(prop, "Unit", None) + if unit: + return get_unit_name(unit) + + +def get_allowed_values(pset_id, prop_name): + pset = ifc_file.by_id(pset_id) + for prop in getattr(pset, "HasProperties", []) or []: + if prop.Name == prop_name: + if prop.is_a("IfcPropertyEnumeratedValue") and prop.EnumerationValues: + return ",".join([v.wrappedValue for v in prop.EnumerationValues]) + + +def get_sheet_name(element): + if element.is_a("IfcBuilding"): + return "Facility" + elif element.is_a("IfcBuildingStorey"): + return "Floor" + elif element.is_a("IfcSpace"): + return "Space" + elif element.is_a("IfcZone"): + return "Zone" + elif element.is_a("IfcSystem"): + return "System" + elif element.is_a("IfcElementType"): + return "Type" + elif element.is_a("IfcElement"): + return "Component" + elif element.is_a("IfcTask"): + return "Job" + elif element.is_a("IfcConstructionProductResource"): + return "Spare" + elif element.is_a("IfcConstructionEquipmentResource"): + return "Resource" + + +config = { + "colours": { + "h": "c0c0c0", # Header data + "r": "ffff99", # Required + "i": "ffcc99", # Internal reference + "e": "cc99ff", # External reference + "o": "ccffcc", # Optionally specified + "s": "c0c0c0", # Secondary product data + "b": "99ccff", # Bespoke data + "x": "000000", # Not in scope + }, + "null": "n/a", + "empty": "n/a", + "bool_true": "Yes", + "bool_false": "No", + "categories": { + "Contact": { + "headers": [ + "Email", + "CreatedBy", + "CreatedOn", + "Category", + "Company", + "Phone", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Department", + "OrganizationCode", + "GivenName", + "FamilyName", + "Street", + "PostalBox", + "Town", + "StateRegion", + "PostalCode", + "Country", + ], + "colours": "rrrrrreeeoooooooooo", + "sort": [{"name": "Email", "order": "ASC"}], + "get_category_elements": get_contacts, + "get_element_data": get_contact_data, + }, + "Facility": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ProjectName", + "SiteName", + "LinearUnits", + "AreaUnits", + "VolumeUnits", + "CurrencyUnit", + "AreaMeasurement", + "ExternalSystem", + "ExternalProjectObject", + "ExternalProjectIdentifier", + "ExternalSiteObject", + "ExternalSiteIdentifier", + "ExternalFacilityObject", + "ExternalFacilityIdentifier", + "Description", + "ProjectDescription", + "SiteDescription", + "Phase", + ], + "colours": "ririrriiiireeeeeeeoooo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_facilities, + "get_element_data": get_facility_data, + }, + "Floor": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + "Elevation", + "Height", + ], + "colours": "ririeeeooo", + "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_floors, + "get_element_data": get_floor_data, + }, + "Space": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "FloorName", + "Description", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "RoomTag", + "UsableHeight", + "GrossArea", + "NetArea", + ], + "colours": "ririrreeeoooo", + "sort": [{"name": "FloorName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_spaces, + "get_element_data": get_space_data, + }, + "Zone": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "SpaceNames", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririreeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_zones, + "get_element_data": get_zone_data, + }, + "Type": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "Description", + "AssetType", + "Manufacturer", + "ModelNumber", + "WarrantyGuarantorParts", + "WarrantyDurationParts", + "WarrantyGuarantorLabor", + "WarrantyDurationLabor", + "WarrantyDurationUnit", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "ReplacementCost", + "ExpectedLife", + "DurationUnit", + "WarrantyDescription", + "NominalLength", + "NominalWidth", + "NominalHeight", + "ModelReference", + "Shape", + "Size", + "Color", + "Finish", + "Grade", + "Material", + "Constituents", + "Features", + "AccessibilityPerformance", + "CodePerformance", + "SustainabilityPerformance", + ], + "colours": "riririiriririeeeooiorrroooooooooooo", + "sort": [{"name": "ExternalObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_types, + "get_element_data": get_type_data, + }, + "Component": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "TypeName", + "Space", + "Description", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "SerialNumber", + "InstallationDate", + "WarrantyStartDate", + "TagNumber", + "BarCode", + "AssetIdentifier", + ], + "colours": "ririireeeoooooo", + "sort": [ + {"name": "ExternalObject", "order": "ASC"}, + {"name": "TypeName", "order": "ASC"}, + {"name": "Name", "order": "ASC"}, + ], + "get_category_elements": get_components, + "get_element_data": get_component_data, + }, + "System": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ComponentNames", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririieeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_systems, + "get_element_data": get_system_data, + }, + "Assembly": { # Note that this is technically "not required" + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "SheetName", + "ParentName", + "ChildNames", + "AssemblyType", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririiiieeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_assemblies, + "get_element_data": get_assembly_data, + }, + "Connection": { # Note that this is technically "not required" + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "ConnectionType", + "SheetName", + "RowName1", + "RowName2", + "RealizingElement", + "PortName1", + "PortName2", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririiiiiiieeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_connections, + "get_element_data": get_connection_data, + }, + "Spare": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "TypeName", + "Suppliers", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + "SetNumber", + "PartNumber", + ], + "colours": "ririiieeeooo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_spares, + "get_element_data": get_spare_data, + }, + "Resource": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririeeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_resources, + "get_element_data": get_resource_data, + }, + "Job": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "Status", + "TypeName", + "Description", + "Duration", + "DurationUnit", + "Start", + "TaskStartUnit", + "Frequency", + "FrequencyUnit", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "TaskNumber", + "Priors", + "ResourceNames", + ], + "colours": "ririiirriririeeeoii", + "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "TaskNumber", "order": "ASC"}], + "get_category_elements": get_jobs, + "get_element_data": get_job_data, + }, + "Document": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ApprovalBy", + "Stage", + "SheetName", + "RowName", + "Directory", + "File", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + "Reference", + ], + "colours": "ririiiiirreeeoo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_documents, + "get_element_data": get_document_data, + }, + "Attribute": { + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "SheetName", + "RowName", + "Value", + "Unit", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + "AllowedValues", + ], + "colours": "ririiirreeeoo", + "sort": [{"name": "Category", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_attributes, + "get_element_data": get_attribute_data, + }, + }, +} From d838e1808dc1399bf54ab6e72f6ba349c08fc54c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 29 Sep 2023 23:03:42 +1000 Subject: [PATCH 07/15] IfcFM can now be used to federate multiple spreadsheets together --- src/ifcfm/ifcfm/__init__.py | 25 +++++++++++++++++++++---- src/ifcfm/ifcfm/basic.py | 14 +++++++------- src/ifcfm/ifcfm/cobie24.py | 31 +++++++++++++++---------------- src/ifcfm/ifcfm/cobie24legacy.py | 15 +++++++++++++++ 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/ifcfm/ifcfm/__init__.py b/src/ifcfm/ifcfm/__init__.py index c5414a46ac..36f6679145 100644 --- a/src/ifcfm/ifcfm/__init__.py +++ b/src/ifcfm/ifcfm/__init__.py @@ -57,7 +57,7 @@ class Parser: else: self.config = preset - def parse(self, ifc_file): + def parse(self, ifc_file, name=None): for category_name, category_config in self.config["categories"].items(): self.categories.setdefault(category_name, {}) for element in category_config["get_category_elements"](ifc_file): @@ -81,12 +81,29 @@ class Parser: data.update(custom_data) if data: - key = data["key"] - del data["key"] + key = "-".join([str(data[k]) for k in category_config["keys"]]) if key in self.categories[category_name]: self.duplicate_keys.append((self.categories[category_name][key], data)) self.categories[category_name][key] = data + def federate(self, paths): + for path in paths: + spreadsheet = pd.ExcelFile(path) + sheet_names = spreadsheet.sheet_names + + for category_name, category_config in self.config["categories"].items(): + self.categories.setdefault(category_name, {}) + if category_name not in sheet_names: + continue + + self.categories.setdefault(category_name, {}) + df = pd.read_excel(spreadsheet, sheet_name=category_name, keep_default_na=False) + for _, row in df.iterrows(): + key = "-".join([str(row[k]) for k in category_config["keys"]]) + if key in self.categories[category_name]: + continue + self.categories[category_name][key] = row.to_dict() + def exclude_categories(self, names): for name in names: if name in self.config["categories"]: @@ -148,7 +165,7 @@ class Writer: if isinstance(value, str): convert = lambda text: int(text) if text.isdigit() else text.lower() return [convert(c) for c in re.split("([0-9]+)", value)] - return value + return [str(value)] # Sort least important keys first, then more important keys. # https://stackoverflow.com/questions/11476371/sort-by-multiple-keys-using-different-orderings diff --git a/src/ifcfm/ifcfm/basic.py b/src/ifcfm/ifcfm/basic.py index f4ccb81ffd..697f9c2bbe 100644 --- a/src/ifcfm/ifcfm/basic.py +++ b/src/ifcfm/ifcfm/basic.py @@ -61,7 +61,6 @@ def get_systems(ifc_file): def get_facility_data(ifc_file, element): return { - "key": element.Name, "Name": element.Name, "ProjectName": ifc_file.by_type("IfcProject")[0].Name, "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None), @@ -80,7 +79,6 @@ def get_facility_data(ifc_file, element): def get_storey_data(ifc_file, element): return { - "key": element.Name, "Name": element.Name, "Category": "Level", "AuthorOrganizationName": get_owner_name(element), @@ -95,7 +93,6 @@ def get_storey_data(ifc_file, element): def get_space_data(ifc_file, element): psets = ifcopenshell.util.element.get_psets(element) return { - "key": element.Name, "Name": element.Name, "Description": element.LongName, "Category": get_classification(element), @@ -112,7 +109,6 @@ def get_space_data(ifc_file, element): def get_zone_data(ifc_file, element): zone, space = element return { - "key": (zone.Name or "Unnamed") + (space.Name or "Unnamed"), "Name": zone.Name, "SpaceName": space.Name, "AuthorOrganizationName": get_owner_name(zone), @@ -125,7 +121,6 @@ def get_zone_data(ifc_file, element): def get_element_type_data(ifc_file, element): psets = ifcopenshell.util.element.get_psets(element) return { - "key": element.Name, "Name": element.Name, "Description": element.Description, "Category": get_classification(element), @@ -150,7 +145,6 @@ def get_element_data(ifc_file, element): system = systems[0].Name if systems else None psets = ifcopenshell.util.element.get_psets(element) return { - "key": element.Name, "Name": element.Name, "TypeName": ifcopenshell.util.element.get_type(element).Name, "SpaceName": space_name, @@ -173,7 +167,6 @@ def get_element_data(ifc_file, element): def get_system_data(ifc_file, element): return { - "key": element.Name, "Name": element.Name, "Description": element.Description, "Category": get_classification(element), @@ -241,6 +234,7 @@ config = { }, "categories": { "Facilities": { + "keys": ["Name"], "headers": [ "Name", "ProjectName", @@ -262,6 +256,7 @@ config = { "get_element_data": get_facility_data, }, "Storeys": { + "keys": ["Name"], "headers": [ "Name", "Category", @@ -278,6 +273,7 @@ config = { "get_element_data": get_storey_data, }, "Spaces": { + "keys": ["Name"], "headers": [ "Name", "Description", @@ -296,6 +292,7 @@ config = { "get_element_data": get_space_data, }, "Zones": { + "keys": ["Name", "SpaceName"], "headers": ["Name", "SpaceName", "AuthorOrganizationName", "AuthorDate", "ModelSoftware", "ModelID"], "colours": "prreee", "sort": [{"name": "Name", "order": "ASC"}], @@ -303,6 +300,7 @@ config = { "get_element_data": get_zone_data, }, "ElementTypes": { + "keys": ["Name"], "headers": [ "Name", "Description", @@ -325,6 +323,7 @@ config = { "get_element_data": get_element_type_data, }, "Elements": { + "keys": ["Name"], "headers": [ "Name", "TypeName", @@ -350,6 +349,7 @@ config = { "get_element_data": get_element_data, }, "Systems": { + "keys": ["Name"], "headers": [ "Name", "Description", diff --git a/src/ifcfm/ifcfm/cobie24.py b/src/ifcfm/ifcfm/cobie24.py index 5af1274cc0..29d13cc3c2 100644 --- a/src/ifcfm/ifcfm/cobie24.py +++ b/src/ifcfm/ifcfm/cobie24.py @@ -245,7 +245,6 @@ def get_attributes(ifc_file): allowed_values = get_property_unit(props["id"], name) data = { - "key": str(val(name)) + str(sheet_name) + str(val(element.Name)), "Name": val(name), "CreatedBy": pset_created_by, "CreatedOn": pset_created_on, @@ -294,7 +293,6 @@ def get_contact_data(ifc_file, element): roles.add(role.Role) return { - "key": email, "Email": email, "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -342,7 +340,6 @@ def get_facility_data(ifc_file, element): name = val(site.Name) or val(site.LongName) return { - "key": name, "Name": name, "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -398,7 +395,6 @@ def get_floor_data(ifc_file, element): elevation = "" if elevation is None else str(elevation) return { - "key": val(element.Name), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -438,7 +434,6 @@ def get_space_data(ifc_file, element): net_area = str(value) return { - "key": val(element.Name), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -462,7 +457,6 @@ def get_zone_data(ifc_file, element): name, category = zone history = get_history(ifc_file) return { - "key": "-".join([str(name), str(category), str(space)]), "Name": name, "CreatedBy": get_email_from_history(history) if history else None, "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, @@ -483,7 +477,6 @@ def get_zone_data(ifc_file, element): space_name = val(space.Name) if space else None return { - "key": "-".join([str(name), str(category), str(space_name)]), "Name": name, "CreatedBy": get_created_by(zone), "CreatedOn": get_created_on(zone), @@ -592,7 +585,6 @@ def get_type_data(ifc_file, element): asset_type = "Moveable" return { - "key": val(element.Name), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -668,7 +660,6 @@ def get_component_data(ifc_file, element): asset_identifier = str(value) return { - "key": element.Name, "Name": element.Name, "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -692,7 +683,6 @@ def get_system_data(ifc_file, element): category = get_category(system) component_name = val(component.Name) return { - "key": str(val(system.Name)) + str(category) + str(component_name), "Name": val(system.Name), "CreatedBy": get_created_by(system), "CreatedOn": get_created_on(system), @@ -727,7 +717,6 @@ def get_assembly_data(ifc_file, element): history = get_history(ifc_file) return { - "key": str(name) + str(sheet_name) + str(parent_name), "Name": name, "CreatedBy": get_email_from_history(history) if history else None, "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, @@ -753,7 +742,6 @@ def get_connection_data(ifc_file, element): row_name1 = val(ifcopenshell.util.system.get_port_element(element.RelatingPort).Name) row_name2 = val(ifcopenshell.util.system.get_port_element(element.RelatedPort).Name) return { - "key": str(name) + str(connection_type) + str(row_name1) + str(row_name2), "Name": name, "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -791,7 +779,6 @@ def get_spare_data(ifc_file, element): part_number = str(value) return { - "key": val(element), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -809,7 +796,6 @@ def get_spare_data(ifc_file, element): def get_resource_data(ifc_file, element): return { - "key": val(element.Name), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -870,7 +856,6 @@ def get_job_data(ifc_file, element): priors = ",".join(priors) if priors else task_number return { - "key": str(val(element.Name)) + str(type_name) + str(task_number), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -912,7 +897,6 @@ def get_document_data(ifc_file, element): sheet_name = get_sheet_name(related_object) row_name = val(related_object.Name) return { - "key": str(name) + str(stage) + str(sheet_name) + str(row_name), "Name": name, "CreatedBy": get_created_by(rel), "CreatedOn": get_created_on(rel), @@ -1159,6 +1143,7 @@ config = { "bool_false": "No", "categories": { "Contact": { + "keys": ["Email"], "headers": [ "Email", "CreatedBy", @@ -1186,6 +1171,7 @@ config = { "get_element_data": get_contact_data, }, "Facility": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1216,6 +1202,7 @@ config = { "get_element_data": get_facility_data, }, "Floor": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1234,6 +1221,7 @@ config = { "get_element_data": get_floor_data, }, "Space": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1255,6 +1243,7 @@ config = { "get_element_data": get_space_data, }, "Zone": { + "keys": ["Name", "Category", "SpaceNames"], "headers": [ "Name", "CreatedBy", @@ -1272,6 +1261,7 @@ config = { "get_element_data": get_zone_data, }, "Type": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1315,6 +1305,7 @@ config = { "get_element_data": get_type_data, }, "Component": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1342,6 +1333,7 @@ config = { "get_element_data": get_component_data, }, "System": { + "keys": ["Name", "Category", "ComponentNames"], "headers": [ "Name", "CreatedBy", @@ -1359,6 +1351,7 @@ config = { "get_element_data": get_system_data, }, "Assembly": { # Note that this is technically "not required" + "keys": ["Name", "SheetName", "ParentName"], "headers": [ "Name", "CreatedBy", @@ -1378,6 +1371,7 @@ config = { "get_element_data": get_assembly_data, }, "Connection": { # Note that this is technically "not required" + "keys": ["Name", "ConnectionType", "RowName1", "RowName2"], "headers": [ "Name", "CreatedBy", @@ -1400,6 +1394,7 @@ config = { "get_element_data": get_connection_data, }, "Spare": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1420,6 +1415,7 @@ config = { "get_element_data": get_spare_data, }, "Resource": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1436,6 +1432,7 @@ config = { "get_element_data": get_resource_data, }, "Job": { + "keys": ["Name", "TypeName", "TaskNumber"], "headers": [ "Name", "CreatedBy", @@ -1463,6 +1460,7 @@ config = { "get_element_data": get_job_data, }, "Document": { + "keys": ["Name", "Stage", "SheetName", "RowName"], "headers": [ "Name", "CreatedBy", @@ -1486,6 +1484,7 @@ config = { "get_element_data": get_document_data, }, "Attribute": { + "keys": ["Name", "SheetName", "RowName"], "headers": [ "Name", "CreatedBy", diff --git a/src/ifcfm/ifcfm/cobie24legacy.py b/src/ifcfm/ifcfm/cobie24legacy.py index 6f385f1278..0de1a783ad 100644 --- a/src/ifcfm/ifcfm/cobie24legacy.py +++ b/src/ifcfm/ifcfm/cobie24legacy.py @@ -1158,6 +1158,7 @@ config = { "bool_false": "No", "categories": { "Contact": { + "keys": ["Email"], "headers": [ "Email", "CreatedBy", @@ -1185,6 +1186,7 @@ config = { "get_element_data": get_contact_data, }, "Facility": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1215,6 +1217,7 @@ config = { "get_element_data": get_facility_data, }, "Floor": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1233,6 +1236,7 @@ config = { "get_element_data": get_floor_data, }, "Space": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1254,6 +1258,7 @@ config = { "get_element_data": get_space_data, }, "Zone": { + "keys": ["Name", "Category", "SpaceNames"], "headers": [ "Name", "CreatedBy", @@ -1271,6 +1276,7 @@ config = { "get_element_data": get_zone_data, }, "Type": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1314,6 +1320,7 @@ config = { "get_element_data": get_type_data, }, "Component": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1341,6 +1348,7 @@ config = { "get_element_data": get_component_data, }, "System": { + "keys": ["Name", "Category", "ComponentNames"], "headers": [ "Name", "CreatedBy", @@ -1358,6 +1366,7 @@ config = { "get_element_data": get_system_data, }, "Assembly": { # Note that this is technically "not required" + "keys": ["Name", "SheetName", "ParentName"], "headers": [ "Name", "CreatedBy", @@ -1377,6 +1386,7 @@ config = { "get_element_data": get_assembly_data, }, "Connection": { # Note that this is technically "not required" + "keys": ["Name", "ConnectionType", "RowName1", "RowName2"], "headers": [ "Name", "CreatedBy", @@ -1399,6 +1409,7 @@ config = { "get_element_data": get_connection_data, }, "Spare": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1419,6 +1430,7 @@ config = { "get_element_data": get_spare_data, }, "Resource": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1435,6 +1447,7 @@ config = { "get_element_data": get_resource_data, }, "Job": { + "keys": ["Name", "TypeName", "TaskNumber"], "headers": [ "Name", "CreatedBy", @@ -1462,6 +1475,7 @@ config = { "get_element_data": get_job_data, }, "Document": { + "keys": ["Name", "Stage", "SheetName", "RowName"], "headers": [ "Name", "CreatedBy", @@ -1485,6 +1499,7 @@ config = { "get_element_data": get_document_data, }, "Attribute": { + "keys": ["Name", "SheetName", "RowName"], "headers": [ "Name", "CreatedBy", From f2097c0c5f5cad3099961eda09bd8211f44d03cd Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:10:16 +0100 Subject: [PATCH 08/15] importing csv cost schedules now allows using selector queries for effortless product assignments to cost items, and add new ods and csv format templates (in french) --- src/ifc5d/ifc5d/csv2ifc.py | 34 ++++- src/ifc5d/sample_cost_schedule_house_FR.csv | 147 ++++++++++++++++++++ src/ifc5d/sample_cost_schedule_house_FR.ods | Bin 0 -> 20627 bytes 3 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 src/ifc5d/sample_cost_schedule_house_FR.csv create mode 100644 src/ifc5d/sample_cost_schedule_house_FR.ods diff --git a/src/ifc5d/ifc5d/csv2ifc.py b/src/ifc5d/ifc5d/csv2ifc.py index 7632b10f44..11e14365cd 100644 --- a/src/ifc5d/ifc5d/csv2ifc.py +++ b/src/ifc5d/ifc5d/csv2ifc.py @@ -20,6 +20,8 @@ import csv import ifcopenshell import ifcopenshell.api import ifcopenshell.util.unit +import ifcopenshell.util.selector +import ifcopenshell.util.element import locale @@ -67,11 +69,15 @@ class Csv2Ifc: identification = row[self.headers["Identification"]] if "Identification" in self.headers else None quantity = row[self.headers["Quantity"]] unit = row[self.headers["Unit"]] + assignments = { + "PropertyName": row[self.headers["Property"]], + "Query": row[self.headers["Query"]], + } if self.has_categories: cost_values = { k: locale.atof(row[v]) for k, v in self.headers.items() - if k not in ["Hierarchy", "Identification", "Name", "Quantity", "Unit", "Subtotal"] and row[v] + if k not in ["Hierarchy", "Identification", "Name", "Quantity", "Unit", "Subtotal", "Property", "Query"] and row[v] } else: cost_values = row[self.headers["Value"]] @@ -82,6 +88,7 @@ class Csv2Ifc: "Quantity": float(quantity) if quantity else None, "Unit": str(unit) if unit else None, "CostValues": cost_values, + "assignments": assignments, "children": [], } @@ -107,7 +114,7 @@ class Csv2Ifc: cost_item["ifc"].Name = cost_item["Name"] cost_item["ifc"].Identification = cost_item["Identification"] - if not cost_item["CostValues"]: + if not cost_item["CostValues"] and cost_item["children"]: if not self.is_schedule_of_rates: cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) cost_value.Category = "*" @@ -115,7 +122,11 @@ class Csv2Ifc: for category, value in cost_item["CostValues"].items(): cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(value) - cost_value.Category = category + if category != "Rate" or category != "Price": + if "Rate" in category or"Price" in category: + category = category.replace("Rate","") + category = category.strip() + cost_value.Category = category else: cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(cost_item["CostValues"]) @@ -144,6 +155,15 @@ class Csv2Ifc: "cost.add_cost_item_quantity", self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class ) quantity[3] = cost_item["Quantity"] + + if cost_item["assignments"]["PropertyName"] and cost_item["assignments"]["Query"]: + print("for query",cost_item["assignments"]["Query"]) + results = ifcopenshell.util.selector.filter_elements(self.file, cost_item["assignments"]["Query"]) + + results = [r for r in results if has_property(self.file, r, cost_item["assignments"]["PropertyName"])] + if results: + ifcopenshell.api.run("cost.assign_cost_item_quantity", self.file, cost_item=cost_item["ifc"], products=results, prop_name=cost_item["assignments"]["PropertyName"]) + self.create_cost_items(cost_item["children"], cost_item["ifc"]) def create_unit(self, symbol): @@ -158,3 +178,11 @@ class Csv2Ifc: def create_boilerplate_ifc(self): self.file = ifcopenshell.file(schema="IFC4") + +def has_property(self, product, property_name): + qtos = ifcopenshell.util.element.get_psets(product, qtos_only=True) + for qset, quantities in qtos.items(): + for quantity, value in quantities.items(): + if quantity == property_name: + return True + return False \ No newline at end of file diff --git a/src/ifc5d/sample_cost_schedule_house_FR.csv b/src/ifc5d/sample_cost_schedule_house_FR.csv new file mode 100644 index 0000000000..18480f0873 --- /dev/null +++ b/src/ifc5d/sample_cost_schedule_house_FR.csv @@ -0,0 +1,147 @@ +Hierarchy,Identification,Name,Quantity,Unit,Contract,Rate,Material Rate,Labor Rate,Subtotal,Property,Query +1,DB,Design and build,,,,,,,,, +2,DB.1,Design,,,,,,,,, +3,DB.1.1,Architecte,1,unit,40000,,,,40000,, +3,DB.1.2,Bet structure,1,unit,10000,,,,10000,, +3,DB.1.3,Bet lots techniques,1,unit,8000,,,,8000,, +2,DB.2,Construction,,,,,,,,, +3,DB.2.1,Go & etancheite,,,,,,,,, +4,DB.2.1.1,Gros oeuvre,,,,,,,,, +4,DB.2.1.2,Etancheite,,,,,,,,, +5,DB.2.1.2.1,Forme de pente et chape de lissage,,,,97,,,,NetArea,"IfcSlab, location=Roof" +5,DB.2.1.2.2,Ecran pare-vapeur et isolation thermique,,,,47,,,,NetArea,"IfcSlab, location=Roof" +5,DB.2.1.2.3,Systeme d’etancheite bicouche,,,,23,,,,NetArea,"IfcCovering, type=Etancheite_H" +5,DB.2.1.2.4,Reliefs d’etancheite bicouche,,,,7,,,,Length,"IfcWall, type=”BLK150”, location=Roof" +5,DB.2.1.2.5,Protection horizontales d’etancheite par carreaux de ciment,,,,,,,,NetArea,"IfcCovering, type=Etancheite_H" +5,DB.2.1.2.6,Protection des reliefs d’etancheite,,,,,,,,, +5,DB.2.1.2.7,Etancheite legere,,,,,,,,, +5,DB.2.1.2.8,Fourniture et pose de gargouilles y compris crapaudines,3,unit,,,,,,, +,,,,,,,,,,, +4,DB.2.1.3,Gros oeuvre piscine,,,,,,,,, +5,DB.2.1.3.1,Fouilles en pleine masse dans tous terrains y compris rocher,,,,,,,,, +5,DB.2.1.3.2,Remblaiement ou evacuation aux decharges publiques,,,,,,,,, +5,DB.2.1.3.3,Beton de proprete,,,,,,,,, +5,DB.2.1.3.4,Beton hydrofuge pour tous les ouvrages en fondation et elevation,,,,,,,,, +5,DB.2.1.3.5,Aciers a haute limite elastique fe500 pour les ouvrages en fondation et elevation,,,,,,,,, +,,,,,,,,,,, +4,DB.2.1.4,Maconnerie et briquetage en elevation,,,,,,,,, +5,DB.2.1.5.1,Cloisons en briques creuses 7t,,,,,,,,, +5,DB.2.1.5.2,Doubles cloisons en briques creuses 7t+vide10+7t,,,,,,,,, +,,,,,,,,,,, +,,,,,,,,,,, +4,DB.2.1.5,Enduits interieurs et exterieurs,,,,,,,,, +5,DB.2.1.5.1,"Enduit interieur au mortier de ciment sur murs et plafonds y +Compris baguettes d'angles",,,,,,,,, +5,DB.2.1.5.1.1,Enduit interieur au platre,,,,,,,,, +5,DB.2.1.5.1.2,"Enduit projete exterieur au mortier de ciment y compris baguettes +D'angles",,,,,,,,, +5,DB.2.1.5.1.3,Enduit de couronnement d'acroteres y compris facon de larmier,,,,,,,,, +,,,,,,,,,,, +4,DB.2.1.5,Reseaux sous dallage,,,,,,,,, +5,DB.2.1.5.1,Canalisation en buse pvc type assainissement serie i,,,,,,,,, +6,DB.2.1.5.1.1,Tranchee ,,,,,,,,, +6,DB.2.1.5.1.2,P.v.c diametre de 200 mm,,,,,,,,, +,,,,,,,,,,, +5,DB.2.1.5.2,Regards en beton arme ,,,,,,,,, +6,DB.2.1.5.2.1,Regards visitable de 50x50 cm,,,,,,,,, +6,DB.2.1.5.2.2,Regards non visitable de 40x40 cm,,,,,,,,, +4,DB.2.1.5,Divers,,,,,,,,, +5,DB.2.1.5.1,Gaine technique d’aeration et habillage des descentes des eaux ,,,,,,,,, +,,,,,,,,,,, +3,DB.2.2,Fluides,,,,,,,,, +4,DB.2.2.1,Plomberie,,,,,,,,, +4,DB.2.2.2,Climatisation,,,,,,,,, +4,DB.2.2.3,Sanitaire,,,,,,,,, +3,DB.2.3,Electricite,,,,,,,,, +4,DB.2.3.1,Eclairage et appareillage,,,,,,,,, +4,DB.2.3.2,Panneaux solaires,,,,,,,,, +3,DB.2.4,Architecture interieure,,,,,,,,, +4,DB.2.4.1,Menuiserie metallique,,,,,,,,, +5,DB.2.4.1.1,G.c 1 en vitrage bord àbord 10*10*2(1unite) rail en alu encastre,,,,,,,,, +5,DB.2.4.1.2,G.c 2 en vitrage bord àbord 10*10*2(1unite) rail en alu encastre,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.2,Menuiserie bois,,,,,,,,, +5,DB.2.4.2.1,Portes,,,,,,,,, +6,DB.2.4.2.1.1,P1,,,,,,,,Count,"IfcDoor, type=P1" +6,DB.2.4.2.1.2,P2,,,,,,,,Count,"IfcDoor, type=P2-LEFT + IfcDoor, type=P2-RIGHT" +6,DB.2.4.2.1.3,P3,,,,,,,,Count,"IfcDoor, type=P3-LEFT + IfcDoor, type=P3-RIGHT" +,,,,,,,,,,, +5,DB.2.4.2.2,Placards et habillage,,,,,,,,, +6,DB.2.4.2.2.1,Fourniture placard type 1,,,,,,,,, +6,DB.2.4.2.2.2,Fourniture placard type 2,,,,,,,,, +6,DB.2.4.2.2.3,Fourniture placard type 3 ,,,,,,,,, +6,DB.2.4.2.2.3,Fourniture placard type 4,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.3,Revetement sol parquet,,,,,,,,, +5,DB.2.4.3.1,Fourniture parquet contrecolle 18mm dont epaisseur noyer americain 3mm,,,,,300,,,NetArea,"IfcCovering, type=RS-2" +5,DB.2.4.3.2,Fournitur basic underlay,,,,,50,,,NetArea,"IfcCovering, type=RS-2" +5,DB.2.4.3.3,"Fourniture profilpas finition laiton 2,5mm",,,,,20,,,Width,IfcDoor +5,DB.2.4.3.4,"Pose du parquet y compris isolant acoustique, chappe de ciment, sous-couche et barre de seuil",,,,,,74,,NetArea,"IfcCovering, type=RS-2" +,,,,,,,,,,, +4,DB.2.4.4,Revetement sol marbre,,,,,,,,, +5,DB.2.4.4.1,Fourniture marbre sols,,,,,,,,NetArea,"IfcCovering, type=RS-5" +5,DB.2.4.4.2,Fourniture marbre- sols sdb,,,,,,,,NetArea,"IfcCovering, type=RS-2" +5,DB.2.4.4.3,Fourniture marbre murs sdb ,,,,,,,,, +5,DB.2.4.4.4,Fourniture marbre marche escalier y/c nez,,,,,,,,, +5,DB.2.4.4.5,Pose marbre sols y compris chappe de ciment,,,,,,,,, +5,DB.2.4.4.6,Pose marbre murs,,,,,,,,, +5,DB.2.4.4.7,Pose marbre marches,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.5,Revetement carreaux,,,,,,,,, +5,DB.2.4.5.1,Revetements,,,,,,,,, +6,DB.2.4.5.1.1,Revêtement sol: cuisine - carreaux - gres cerame (30*60cm) - choix sous validation d'echantillons par l'architecte et le maitre d'ouvrage,,,,,,,,, +6,DB.2.4.5.1.2,Revêtement sol: salle de jeux - carreaux - gres cerame (30*60cm) - choix sous validation d'echantillons par l'architecte et le maitre d'ouvrage,,,,,,,,, +6,DB.2.4.5.1.3,Revêtement sol: (reserve) - gres cerame (30*60cm) - choix sous validation d'echantillons par l'architecte et le maitre d'ouvrage - ,,,,,,,,, +6,DB.2.4.5.1.4,Revêtement mural cuisine - carreaux - effet vitrage noir,,,,,,,,, +5,DB.2.4.5.2,Plinthes,,,,,,,,, +6,DB.2.4.5.2.1,Plinthes - duropolymer orac decor ref : sx157,,,,,,,,, +6,DB.2.4.5.2.2,Plinthes marches - - duropolymer orac decor ref : sx157,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.6,Peinture,,,,,,,,, +5,DB.2.4.6.1,Fourniture et pose peinture murs - marmorex astral y/c preparation (5 couches),,,,,,,,, +5,DB.2.4.6.2,Fourniture et pose peinture contremarche escalier - marmorex astral y/c preparation (5 couches,,,,,,,,, +5,DB.2.4.6.3,Fourniture et pose peinture murs cuisine - alpha tacto astral y/c preparation (5 couches),,,,,,,,, +5,DB.2.4.6.4,Foruniture et pose peinture faux-plafond - jotun acrylique mate blanc y/c preparation (5 couches),,,,,,,,, +5,DB.2.4.6.5,Fourniture et pose peinture murs sdb - jotun acrylique semi-brillant blanc special piece humide y/c preparation (5 couches),,,,,,,,, +5,DB.2.4.6.6,Fourniture et pose peinture faux-plafond sdb + cuisine - jotun acrylique semi-brillant blanc special piece humide y/c preparation (5 couches),,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.7,Faux plafonds,,,,,,,,, +5,DB.2.4.7.1,Type 1,,,,,,,,, +5,DB.2.4.7.2,Type 2,,,,,,,,, +5,DB.2.4.7.3,Type 3 – hydrofuge – exterieur,,,,,,,,, +5,DB.2.4.7.4,Trappes d’acces,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.8,Cuisine,1,unit,,,,,,, +,,,,,,,,,,, +3,DB.2.5,Facade,,,,,,,,, +4,DB.2.5.1,Revetement exterieur ,,,,,,,,, +5,DB.2.5.1.1,Revetement mural: plus value enduit mural hydrofuge (monocouche),,,,,,,,, +5,DB.2.5.1.2,Revetement mural: pierre volcanique grise ,,,,,,,,, +5,DB.2.5.1.3,Revetement mural: bardage en bois iroko ou tole alu section carre ou rectangulaire avec habillage bois technowood (voir detail architecte),,,,,,,,, +5,DB.2.5.1.4,Revêtement sol: terrasses et cour - tech - choix sous validation d'echantillons par l'architecte et le maitre d'ouvrage ,,,,,,,,, +5,DB.2.5.1.5,Revetement sol: seuill entree en pierre de taza,,,,,,,,, +5,DB.2.5.1.6,Revetement sol: sous bassement perimetre villa en pierre de taza ,,,,,,,,, +,,,,,,,,,,, +4,DB.2.5.2,Menuiserie aluminium,,,,,,,,, +5,DB.2.5.2.1,Fenetres,,,,,,,,, +6,DB.2.5.2.1.1,F1,,,,,,,,, +6,DB.2.5.2.1.2,F2,,,,,,,,, +,,,,,,,,,,, +5,DB.2.5.2.2,Chassis ouvrants,,,,,,,,, +6,DB.2.5.2.2.1,Ch1,,,,,,,,, +6,DB.2.5.2.2.2,Ch2,,,,,,,,, +6,DB.2.5.2.2.3,Ch3,,,,,,,,, +,,,,,,,,,,, +5,DB.2.5.2.3,Portes coulissantes/ mur rideau,,,,,,,,, +6,DB.2.5.2.3.1,P4,,,,,,,,, +6,DB.2.5.2.3.2,P5,,,,,,,,, +6,DB.2.5.2.3.3,P6,,,,,,,,, +,,,,,,,,,,, +5,DB.2.5.2.4,Volets roulants,,,,,,,,, +6,DB.2.5.2.4.1,Type 1,,,,,,,,, +6,DB.2.5.2.4.2,Type 2,,,,,,,,, +6,DB.2.5.2.4.3,Type 3,,,,,,,,, +3,DB.2.6,Exterieur,,,,,,,,, +4,DB.2.6.1,Cloture,,,,,,,,, +4,DB.2.6.2,Piscine,,,,,,,,, +4,DB.2.6.3,Jardin,,,,,,,,, diff --git a/src/ifc5d/sample_cost_schedule_house_FR.ods b/src/ifc5d/sample_cost_schedule_house_FR.ods new file mode 100644 index 0000000000000000000000000000000000000000..3d8cb6bb1bd9a88dc312bf960788532ab261323a GIT binary patch literal 20627 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 Date: Fri, 29 Sep 2023 17:30:33 +0100 Subject: [PATCH 09/15] fix f2097c0 --- src/ifc5d/ifc5d/csv2ifc.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ifc5d/ifc5d/csv2ifc.py b/src/ifc5d/ifc5d/csv2ifc.py index 11e14365cd..7f2241c67b 100644 --- a/src/ifc5d/ifc5d/csv2ifc.py +++ b/src/ifc5d/ifc5d/csv2ifc.py @@ -69,10 +69,16 @@ class Csv2Ifc: identification = row[self.headers["Identification"]] if "Identification" in self.headers else None quantity = row[self.headers["Quantity"]] unit = row[self.headers["Unit"]] - assignments = { - "PropertyName": row[self.headers["Property"]], - "Query": row[self.headers["Query"]], - } + if not self.is_schedule_of_rates: + assignments = { + "PropertyName": row[self.headers["Property"]], + "Query": row[self.headers["Query"]], + } + else: + assignments = { + "PropertyName": None, + "Query": None, + } if self.has_categories: cost_values = { k: locale.atof(row[v]) From ad79c63a37e1e46f25ededb09c9212666e4fffd7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 2 Oct 2023 10:57:24 +0500 Subject: [PATCH 10/15] Fixed #3832 --- .../ifcopenshell/api/system/connect_port.py | 4 ++-- .../ifcopenshell/api/system/disconnect_port.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py index 20380d9dc4..a39794a5d7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py @@ -82,8 +82,8 @@ class Usecase: # Create 2 ports, one for either end of both the duct and fitting. duct_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) duct_port2 = ifcopenshell.api.run("system.add_port", model, element=duct) - fitting_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) - fitting_port2 = ifcopenshell.api.run("system.add_port", model, element=duct) + fitting_port1 = ifcopenshell.api.run("system.add_port", model, element=fitting) + fitting_port2 = ifcopenshell.api.run("system.add_port", model, element=fitting) # Connect the duct and fitting together. At this point, we have not # yet determined the direction of the flow, so we leave direction as diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py index eddfae5f59..3782e8e27b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py @@ -52,8 +52,8 @@ class Usecase: # Create 2 ports, one for either end of both the duct and fitting. duct_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) duct_port2 = ifcopenshell.api.run("system.add_port", model, element=duct) - fitting_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) - fitting_port2 = ifcopenshell.api.run("system.add_port", model, element=duct) + fitting_port1 = ifcopenshell.api.run("system.add_port", model, element=fitting) + fitting_port2 = ifcopenshell.api.run("system.add_port", model, element=fitting) # Connect the duct and fitting together. At this point, we have not # yet determined the direction of the flow, so we leave direction as From edaabf5bd28a1a65cf92a3cdeaa95b664e6d667a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 2 Oct 2023 11:44:31 +0500 Subject: [PATCH 11/15] Fixed #3829 occured after ae45e6b similar_openings wasn't reset on each iteration causing moving unrelated openings --- src/blenderbim/blenderbim/bim/module/model/opening.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index 4b4fe6c8e4..9b841cfe2b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -748,7 +748,6 @@ class EditOpenings(Operator, tool.Ifc.Operator): building_objs = set() model = tool.Ifc.get() all_openings = model.by_type("IfcOpeningElement") - similar_openings = [] for obj in context.selected_objects: element = tool.Ifc.get_entity(obj) @@ -756,9 +755,7 @@ class EditOpenings(Operator, tool.Ifc.Operator): continue openings = [r.RelatedOpeningElement for r in element.HasOpenings] for opening in openings: - for all_opening in all_openings: - if all_opening.ObjectPlacement == opening.ObjectPlacement: - similar_openings.append(all_opening) + similar_openings = [o for o in all_openings if o.ObjectPlacement == opening.ObjectPlacement] opening_obj = tool.Ifc.get_object(opening) if opening_obj: if tool.Ifc.is_edited(opening_obj): From 05d4dc5666dd84ffb2fe3936f0e20ecd59278154 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 2 Oct 2023 15:30:32 +0500 Subject: [PATCH 12/15] Rever IFC Project https://i.imgur.com/ajQiK8D.png --- .../blenderbim/bim/module/project/__init__.py | 1 + .../blenderbim/bim/module/project/operator.py | 17 +++++++++++++++++ .../blenderbim/bim/module/project/ui.py | 2 ++ 3 files changed, 20 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index 383b648053..5adcab8073 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -37,6 +37,7 @@ classes = ( operator.LoadProjectElements, operator.NewProject, operator.RefreshLibrary, + operator.RevertProject, operator.RewindLibrary, operator.SaveLibraryFile, operator.SelectLibraryFile, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index ec431b80bb..7e8a9b821c 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -676,6 +676,23 @@ class UnloadProject(bpy.types.Operator): return {"FINISHED"} +class RevertProject(bpy.types.Operator, IFCFileSelector): + bl_idname = "bim.revert_project" + bl_label = "Revert IFC Project" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Reload currently opened IFC project discarding all unsaved changes" + + @classmethod + def poll(cls, context): + if not context.scene.BIMProperties.ifc_file: + cls.poll_message_set("IFC project need to be loaded and saved on the disk.") + return False + return True + + def execute(self, context): + bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=context.scene.BIMProperties.ifc_file) + + class LoadProjectElements(bpy.types.Operator): bl_idname = "bim.load_project_elements" bl_label = "Load Project Elements" diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 0797238bd1..d2bc4feb8b 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -73,6 +73,8 @@ def file_menu(self, context): op = self.layout.operator("export_ifc.bim", text="Save IFC Project As...") op.should_save_as = True self.layout.separator() + self.layout.operator("bim.revert_project") + self.layout.separator() class BIM_PT_project(Panel): From 3c27c52c9fb2843306b0854c18666b5b5312ba04 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 2 Oct 2023 15:38:26 +0500 Subject: [PATCH 13/15] Small fix for 05d4dc566 --- src/blenderbim/blenderbim/bim/module/project/operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 7e8a9b821c..0cd756882e 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -691,6 +691,7 @@ class RevertProject(bpy.types.Operator, IFCFileSelector): def execute(self, context): bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=context.scene.BIMProperties.ifc_file) + return {"FINISHED"} class LoadProjectElements(bpy.types.Operator): From 90b40ff0dc0a8c6d61b90d0ee0ed2d6184983fdd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 2 Oct 2023 16:55:38 +0500 Subject: [PATCH 14/15] Getting drawing group more precisely Previously it was conflicting with other possible groups assigned to the object. It's now even more important since we're actively using groups to save search queries. Example error caused by this issue - during drawing activation if you had 2 IfcGroups on the drawing element: draiwng's IfcGroup RelatedObjects attribute was updated, then inverse attrtibute `drawing.HasAssignments` order was updated too (DRAWING IfcGroup now would be positioned second after IfcGroup for search query) and it was leading to errors during filtering current drawing annotations --- src/blenderbim/blenderbim/tool/drawing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 6d461cefc5..ae75535699 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -416,7 +416,7 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def get_drawing_group(cls, drawing): for rel in drawing.HasAssignments or []: - if rel.is_a("IfcRelAssignsToGroup"): + if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING": return rel.RelatingGroup @classmethod From 9dd728ce7e182576e789f028e61288809d5fcec0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 2 Oct 2023 17:32:56 +0500 Subject: [PATCH 15/15] Fixed error creating a door type / window types after 8f6be60c6 --- src/blenderbim/blenderbim/bim/module/model/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/window.py b/src/blenderbim/blenderbim/bim/module/model/window.py index e432be6797..2aa33b5309 100644 --- a/src/blenderbim/blenderbim/bim/module/model/window.py +++ b/src/blenderbim/blenderbim/bim/module/model/window.py @@ -81,8 +81,9 @@ def update_simple_openings(element, opening_width, opening_height): has_replaced_opening_representation = True tool.Model.reload_body_representation(voided_objs) - with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]): - bpy.ops.bim.recalculate_fill() + if fillings: + with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]): + bpy.ops.bim.recalculate_fill() def update_window_modifier_representation(context, obj):