From d1baaa21e7da025c888f89f51a428ffc9f4fa0ec Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 5 Sep 2022 12:41:06 +1000 Subject: [PATCH] Update IfcFM to use actors, not contacts, support federated datasets, and be more true to spec --- src/ifcfm/ifcfm/parser.py | 340 +++++++++++++++++--------------------- src/ifcfm/ifcfm/writer.py | 15 +- 2 files changed, 158 insertions(+), 197 deletions(-) diff --git a/src/ifcfm/ifcfm/parser.py b/src/ifcfm/ifcfm/parser.py index fc369e5003..d23890e767 100644 --- a/src/ifcfm/ifcfm/parser.py +++ b/src/ifcfm/ifcfm/parser.py @@ -22,6 +22,7 @@ import ifcopenshell.util.fm import ifcopenshell.util.selector import ifcopenshell.util.date import ifcopenshell.util.schema +import ifcopenshell.util.classification class Parser: @@ -29,7 +30,7 @@ class Parser: self.logger = logger self.file = None self.sheets = [ - "contacts", + "actors", "facilities", "floors", "spaces", @@ -69,11 +70,8 @@ class Parser: def parse(self, files): self.files = files - # self.file = ifcopenshell.open(file) - # self.type_assets = self.selector.parse(self.file, type_query) - # self.component_assets = self.selector.parse(self.file, component_query) - self.get_contacts() + self.get_actors() self.get_facilities() self.get_floors() self.get_spaces() @@ -92,36 +90,30 @@ class Parser: # self.get_coordinates() # self.get_issues() - def get_contacts(self): - for element in self.files["arch"].by_type("IfcOrganization"): - if "IfcApplication" in [e.is_a() for e in self.files["arch"].get_inverse(element)]: - continue - name = element.Name - self.contacts[name] = self.get_organisation(element) + def get_actors(self): + for ifc in self.files.values(): + for element in ifc.by_type("IfcActor"): + name = element.TheActor.Name + self.actors[name] = self.get_actor(element) - def get_organisation(self, element): + def get_actor(self, element): + psets = ifcopenshell.util.element.get_psets(element) return { - "Name": element.Name, - "Category": self.get_organisation_category(element), - "Email": self.get_organisation_address(element, "ElectronicMailAddresses"), - "Phone": self.get_organisation_address(element, "TelephoneNumbers"), - "Department": self.get_organisation_address(element, "InternalLocation"), - "Street": self.get_organisation_address(element, "AddressLines"), - "PostalBox": self.get_organisation_address(element, "PostalBox"), - "Town": self.get_organisation_address(element, "Town"), - "StateRegion": self.get_organisation_address(element, "Region"), - "PostalCode": self.get_organisation_address(element, "PostalCode"), - "Country": self.get_organisation_address(element, "Country"), - "CompanyURL": self.get_organisation_address(element, "WWWHomePageURL"), + "Name": element.TheActor.Name, + "Category": self.get_classification(element), + "Email": self.get_actor_address(element, "ElectronicMailAddresses"), + "Phone": self.get_actor_address(element, "TelephoneNumbers"), + "CompanyURL": self.get_actor_address(element, "WWWHomePageURL"), + "Department": self.get_actor_address(element, "InternalLocation"), + "Address1": self.get_actor_address(element, "AddressLines"), + "Address2": self.get_actor_address(element, "Town"), + "StateRegion": self.get_actor_address(element, "Region"), + "PostalCode": self.get_actor_address(element, "PostalCode"), + "Country": self.get_actor_address(element, "Country"), } - def get_organisation_category(self, element): - for role in element.Roles or []: - if role.UserDefinedRole: - return role.UserDefinedRole - - def get_organisation_address(self, element, name): - for address in element.Addresses or []: + def get_actor_address(self, element, name): + for address in element.TheActor.Addresses or []: if hasattr(address, name) and getattr(address, name, None): result = getattr(address, name) if isinstance(result, tuple): @@ -129,49 +121,43 @@ class Parser: return result def get_facilities(self): - element = self.files["arch"].by_type("IfcBuilding")[0] - self.facilities[element.Name] = { - "Name": element.Name, - "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, - "AuthorDate": ifcopenshell.util.date.ifc2datetime( - self.files["arch"].by_type("IfcProject")[0].OwnerHistory.CreationDate - ).isoformat(), - "Category": self.get_classification(element), - "ProjectName": element.Decomposes[0].RelatingObject.Decomposes[0].RelatingObject.Name, - "SiteName": element.Decomposes[0].RelatingObject.Name, - "LinearUnits": "millimeters", - "AreaUnits": "square meters", - "AreaMeasurement": "TODO", - "Phase": element.Decomposes[0].RelatingObject.Decomposes[0].RelatingObject.Phase, - "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelProjectID": self.files["arch"].by_type("IfcProject")[0].GlobalId, - "ModelSiteID": element.Decomposes[0].RelatingObject.GlobalId, - "ModelBuildingID": element.GlobalId, - } + for key, ifc in self.files.items(): + if "arch" not in key: + continue + element = ifc.by_type("IfcBuilding")[0] + self.facilities[element.Name] = { + "Name": element.Name, + "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, + "AuthorDate": ifcopenshell.util.date.ifc2datetime( + ifc.by_type("IfcProject")[0].OwnerHistory.CreationDate + ).isoformat(), + "Category": self.get_classification(element), + "ProjectName": element.Decomposes[0].RelatingObject.Decomposes[0].RelatingObject.Name, + "SiteName": element.Decomposes[0].RelatingObject.Name, + "LinearUnits": "millimeters", + "AreaUnits": "square meters", + "AreaMeasurement": "Revit", + "Phase": element.Decomposes[0].RelatingObject.Decomposes[0].RelatingObject.Phase, + "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, + "ModelProjectID": ifc.by_type("IfcProject")[0].GlobalId, + "ModelSiteID": element.Decomposes[0].RelatingObject.GlobalId, + "ModelBuildingID": element.GlobalId, + } def get_classification(self, element): - rel = [r for r in element.HasAssociations or [] if r.is_a("IfcRelAssociatesClassification")] - if rel: - classification = rel[0].RelatingClassification - if getattr(classification, "Identification", None) and getattr(classification, "Name", None): - return "{}:{}".format(classification.Identification, classification.Name) - elif getattr(classification, "ItemReference", None) and getattr(classification, "Name", None): - return "{}:{}".format(classification.ItemReference, classification.Name) + references = list(ifcopenshell.util.classification.get_references(element)) + if references: + if hasattr(references[0], "Identification"): + return "{}:{}".format(references[0].Identification, references[0].Name) + return "{}:{}".format(references[0].ItemReference, references[0].Name) def get_floors(self): - storeys = self.files["arch"].by_type("IfcBuildingStorey") - self.floors["Site"] = { - "Name": "Site", - "AuthorOrganizationName": storeys[0].OwnerHistory.OwningUser.TheOrganization.Name, - "AuthorDate": datetime.datetime.now().replace(microsecond=0).isoformat(), - "Category": "Site", - "ModelSoftware": "IfcFM", - "ModelObject": "IfcExternalSpatialElement", - "ModelID": ifcopenshell.guid.new(), # TODO - "Elevation": None, - } - for element in storeys: - self.get_floor(element) + for key, ifc in self.files.items(): + if "arch" not in key: + continue + storeys = ifc.by_type("IfcBuildingStorey") + for element in storeys: + self.get_floor(element) def get_floor(self, element): name = element.Name @@ -187,68 +173,61 @@ class Parser: "Elevation": elevation, } + def get_property(self, 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_spaces(self): - primary_keys = [] - for element in self.files["arch"].by_type("IfcSpace"): - name = element.Name - primary_keys.append(name) - # TODO: not correct mapping - psets = ifcopenshell.util.element.get_psets(element) + for key, ifc in self.files.items(): + if "arch" not in key: + continue + primary_keys = [] + for element in ifc.by_type("IfcSpace"): + name = element.Name + primary_keys.append(name) + # TODO: not correct mapping + psets = ifcopenshell.util.element.get_psets(element) - category = None - if "Data" in psets and "COBie.Space.Category" in psets["Data"]: - category = psets["Data"]["COBie.Space.Category"].replace(" : ", ":") - - usable_height = None - if "Data" in psets and "COBie.Space.Category" in psets["Data"]: - usable_height = round(psets["Data"]["COBie.Space.UsableHeight"], 2) or None - - area_gross = None - if "Data" in psets and "COBie.Space.GrossArea" in psets["Data"]: - area_gross = round(psets["Data"]["COBie.Space.GrossArea"], 2) or None - - area_net = None - if "Data" in psets and "COBie.Space.NetArea" in psets["Data"]: - area_net = round(psets["Data"]["COBie.Space.NetArea"], 2) or None - - self.spaces[name] = { - "Name": name, - "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, - "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(), - "Category": category, - "LevelName": element.Decomposes[0].RelatingObject.Name, - "Description": element.LongName, - "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelID": element.GlobalId, - "BuildingRoomNumber": None, - "UsableHeight": usable_height, - "AreaGross": area_gross, - "AreaNet": area_net, - } + self.spaces[name] = { + "Name": name, + "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, + "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(), + "Category": self.get_classification(element), + "LevelName": element.Decomposes[0].RelatingObject.Name, + "Description": element.LongName, + "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, + "ModelID": element.GlobalId, + "BuildingRoomNumber": None, + "UsableHeight": self.get_property(psets, "Data", "COBie.Space.UsableHeight", decimals=0), + "AreaGross": self.get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2), + "AreaNet": self.get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2), + } def get_zones(self): - for element in self.files["arch"].by_type("IfcZone"): - for rel in element.IsGroupedBy: - for space in rel.RelatedObjects: - if not space.is_a("IfcSpace"): - continue - self.zones[element.Name + space.Name] = { - "Name": element.Name, - "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, - "AuthorDate": ifcopenshell.util.date.ifc2datetime( - element.OwnerHistory.CreationDate - ).isoformat(), - "Category": "Occupancy", - "SpaceName": space.Name, - "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelID": element.GlobalId, - "ParentZoneName": None, - } + for ifc in self.files.values(): + for element in ifc.by_type("IfcZone"): + for rel in element.IsGroupedBy: + for space in rel.RelatedObjects: + if not space.is_a("IfcSpace"): + continue + self.zones[element.Name + space.Name] = { + "Name": element.Name, + "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, + "AuthorDate": ifcopenshell.util.date.ifc2datetime( + element.OwnerHistory.CreationDate + ).isoformat(), + "Category": "Occupancy", + "SpaceName": space.Name, + "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, + "ModelID": element.GlobalId, + "ParentZoneName": None, + } def get_systems(self): for discipline, ifc in self.files.items(): - # if discipline == "arch": - # continue for element in ifc.by_type("IfcSystem"): name = element.Name self.systems[name] = { @@ -270,20 +249,7 @@ class Parser: for element in ifcopenshell.util.fm.get_fmhem_types(self.files[ifc_file]): name = element.Name primary_keys.append(name) - - psets = ifcopenshell.util.element.get_psets(element) - - # Hack - category = None - if ifc_file == "arch": - # if "Data" in psets and "COBie.Type.Category" in psets["Data"]: - # if ":" in psets["Data"]["COBie.Type.Category"]: - # category = psets["Data"]["COBie.Type.Category"].replace(" : ", ":") - if "Data" in psets and "Classification.Uniclass.Pr.Number" in psets["Data"]: - category = f"{psets['Data']['Classification.Uniclass.Pr.Number']}:{psets['Data']['Classification.Uniclass.Pr.Description']}" - elif ifc_file == "hyd": - if "Data" in psets and "Classification.Uniclass.Pr.Number" in psets["Data"]: - category = f"{psets['Data']['Classification.Uniclass.Pr.Number']}:{psets['Data']['Classification.Uniclass.Pr.Description']}" + category = self.get_classification(element) self.types[name] = { "Name": name, @@ -298,7 +264,7 @@ class Parser: "WarrantyOrganizationName": None, "WarrantyDuration": None, "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelObject": element.is_a(), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), "ModelID": element.GlobalId, "SpecificationSection": None, "SubmittalID": None, @@ -311,7 +277,11 @@ class Parser: def get_components_from_file(self, ifc_file): for element_type in ifcopenshell.util.fm.get_fmhem_types(self.files[ifc_file]): - for element in element_type.ObjectTypeOf[0].RelatedObjects: + elements = ifcopenshell.util.element.get_types(element_type) + if not elements: + self.logger.warning("The type has no occurrences %s", element_type) + continue + for element in elements: name = element.Name system = None @@ -319,19 +289,8 @@ class Parser: if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a("IfcSystem"): system = rel.RelatingGroup.Name - space_name = None - - # Nasty hack - if ifc_file == "arch": - psets = ifcopenshell.util.element.get_psets(element) - if "Data" in psets and "COBie.Component.Space" in psets["Data"]: - space_name = psets["Data"]["COBie.Component.Space"] - if space_name not in self.spaces: - space_name = None - else: - space = element.ContainedInStructure[0].RelatingStructure - if space.is_a("IfcSpace"): - space_name = space.Name + space = ifcopenshell.util.element.get_container(element) + space_name = space.Name if space.is_a("IfcSpace") else None self.components[name] = { "Name": name, @@ -342,7 +301,7 @@ class Parser: "InstallationDate": None, "WarrantyStartDate": None, "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelObject": element.is_a(), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), "ModelID": element.GlobalId, "InstalledModelNumber": None, "SerialNumber": None, @@ -360,39 +319,42 @@ class Parser: } def get_documents(self): - for rel in self.files["arch"].by_type("IfcRelAssociatesDocument"): - element = rel.RelatingDocument - for related_object in rel.RelatedObjects: - name = element.Name - worksheet_row = related_object.Name - if self.files["arch"].schema == "IFC2X3": - submittal_id = element.ItemReference - referenced_document = element.ReferenceToDocument[0] - author_date = None - if referenced_document.CreationTime: - author_date = ifcopenshell.util.date.ifc2datetime(referenced_document.CreationTime).isoformat() - else: - submittal_id = element.Identification - referenced_document = element.ReferencedDocument - author_date = referenced_document.CreationTime + for ifc in self.files.values(): + for rel in ifc.by_type("IfcRelAssociatesDocument"): + element = rel.RelatingDocument + if element.is_a("IfcDocumentInformation"): + continue + for related_object in rel.RelatedObjects: + name = element.Name + worksheet_row = related_object.Name + if ifc.schema == "IFC2X3": + submittal_id = element.ItemReference + referenced_document = element.ReferenceToDocument[0] + author_date = None + if referenced_document.CreationTime: + author_date = ifcopenshell.util.date.ifc2datetime(referenced_document.CreationTime).isoformat() + else: + submittal_id = element.Identification + referenced_document = element.ReferencedDocument + author_date = referenced_document.CreationTime - worksheet_name = None - if related_object.is_a("IfcSpace"): - worksheet_name = "Space" - elif related_object.is_a("IfcTypeObject"): - worksheet_name = "Type" + worksheet_name = None + if related_object.is_a("IfcSpace"): + worksheet_name = "Space" + elif related_object.is_a("IfcTypeObject"): + worksheet_name = "Type" - self.documents[element.Name + worksheet_name + worksheet_row] = { - "Name": name, - "AuthorOrganizationName": referenced_document.DocumentOwner.Name, - "AuthorDate": author_date, - "Category": referenced_document.Purpose, - "WorksheetName": worksheet_name, - "WorksheetRow": worksheet_row, - "Revision": referenced_document.Revision, - "Location": referenced_document.Name, - "Description": referenced_document.Description, - "SpecificationSection": None, - "SubmittalID": submittal_id, - "SourceURL": None, - } + self.documents[element.Name + worksheet_name + worksheet_row] = { + "Name": name, + "AuthorOrganizationName": referenced_document.DocumentOwner.Name, + "AuthorDate": author_date, + "Category": referenced_document.Purpose, + "WorksheetName": worksheet_name, + "WorksheetRow": worksheet_row, + "Revision": referenced_document.Revision, + "Location": referenced_document.Name, + "Description": referenced_document.Description, + "SpecificationSection": None, + "SubmittalID": submittal_id, + "SourceURL": None, + } diff --git a/src/ifcfm/ifcfm/writer.py b/src/ifcfm/ifcfm/writer.py index 803a6cc9b1..f1e433272e 100644 --- a/src/ifcfm/ifcfm/writer.py +++ b/src/ifcfm/ifcfm/writer.py @@ -93,7 +93,7 @@ class Writer: def write(self): self.sheets = [ - "Contact", + "Actor", "Facility", "Floor", "Space", @@ -113,23 +113,22 @@ class Writer: # "Issue", ] self.write_data( - "Contact", - self.parser.contacts, + "Actor", + self.parser.actors, [ "Name", "Category", "Email", "Phone", + "CompanyURL", "Department", - "Street", - "PostalBox", - "Town", + "Address1", + "Address2", "StateRegion", "PostalCode", "Country", - "CompanyURL", ], - "rirrrrrrrrrr", + "rirrrrrrrrr", ["Name"], ) self.write_data(