From 0fb9f584cb68a00624b309c175f059b2b9db7a07 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 7 Sep 2023 17:38:33 +1000 Subject: [PATCH] Big clean up to IfcFM. It's almost usable! --- src/ifcfm/ifcfm.py | 455 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 src/ifcfm/ifcfm.py diff --git a/src/ifcfm/ifcfm.py b/src/ifcfm/ifcfm.py new file mode 100644 index 0000000000..0662f42b0d --- /dev/null +++ b/src/ifcfm/ifcfm.py @@ -0,0 +1,455 @@ +# 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 os +import re +import csv +import datetime +import ifcopenshell +import ifcopenshell.util.fm +import ifcopenshell.util.date +import ifcopenshell.util.schema +import ifcopenshell.util.system +import ifcopenshell.util.placement +import ifcopenshell.util.classification + +try: + from xlsxwriter import Workbook +except: + pass # No XLSX support + +try: + from odf.opendocument import OpenDocumentSpreadsheet + from odf.style import Style, TableCellProperties + from odf.table import Table, TableRow, TableCell + from odf.text import P +except: + pass # No ODF support + + +class Parser: + def __init__(self, preset="BASIC"): + self.file = None + self.categories = {} + self.get_category_elements = {} + self.get_element_data = {} + self.get_custom_element_data = {} + self.duplicate_keys = [] + + if preset == "BASIC": + self.get_category_elements = { + "Actors": get_actors, + "Facilities": get_facilities, + "Storeys": get_storeys, + "Spaces": get_spaces, + "Zones": get_zones, + "Types": get_types, + "Elements": get_elements, + "Systems": get_systems, + } + self.get_element_data = { + "Actors": get_actor_data, + "Facilities": get_facility_data, + "Storeys": get_storey_data, + "Spaces": get_space_data, + "Zones": get_zone_data, + "Types": get_type_data, + "Elements": get_element_data, + "Systems": get_system_data, + } + + def parse(self, ifc_file): + for category_name, get_category_elements in self.get_category_elements.items(): + self.categories.setdefault(category_name, {}) + for element in get_category_elements(ifc_file): + get_element_data = self.get_element_data[category_name] + + if isinstance(get_element_data, dict): + data = {} + for key, query in get_element_data.items(): + data[key] = ifcopenshell.util.selector.get_element_value(element, query) + else: + data = get_element_data(ifc_file, element) or {} + + get_custom_element_data = self.get_custom_element_data.get(category_name, lambda x, y: None) + if isinstance(get_element_data, dict): + custom_data = {} + for key, query in get_custom_element_data.items(): + custom_data[key] = ifcopenshell.util.selector.get_element_value(element, query) + else: + custom_data = get_custom_element_data(ifc_file, element) or {} + + data.update(custom_data) + + if data: + key = data["key"] + del data["key"] + if key in self.categories[category_name]: + self.duplicate_keys.append((self.categories[category_name][key], data)) + self.categories[category_name][key] = data + + +class Writer: + def __init__(self, parser, colours=None, preset="BASIC"): + self.parser = parser + if colours: + self.colours = colours + else: + self.colours = { + "h": "dddddd", # Header data + "p": "dc8774", # Primary identification data + "s": "b8dd73", # Secondary asset data + "r": "eda786", # Internal reference + "e": "96c7d0", # External / autogenerated data + "o": "ddb873", # Conditional / optional data + "n": "eeeeee", # Other data + "b": "000000", # Not in scope + } + if preset == "BASIC": + self.config = { + "Actors": {"colours": "ppssssssss", "sort": [{"name": "Name", "order": "ASC"}]}, + "Facilities": {"colours": "prppppsssseeee", "sort": [{"name": "Name", "order": "ASC"}]}, + "Storeys": { + "colours": "prppeees", + "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Spaces": { + "colours": "prpprpeess", + "sort": [{"name": "LevelName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Zones": {"colours": "prpree", "sort": [{"name": "Name", "order": "ASC"}]}, + "Types": { + "colours": "prpppeeee", + "sort": [{"name": "ModelObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Elements": { + "colours": "prprrreee", + "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Systems": {"colours": "pprppee", "sort": [{"name": "Name", "order": "ASC"}]}, + } + + def write(self, null="N/A", empty="-", bool_true="YES", bool_false="NO"): + self.categories = {} + for category, data in self.parser.categories.items(): + if not data: + self.categories[category] = {"headers": [], "rows": []} + continue + + headers = list(data[list(data.keys())[0]].keys()) + rows = [] + for row in data.values(): + processed_row = [] + for value in row.values(): + if value is None: + value = null + elif value == "": + value = empty + elif value is True: + value = bool_true + elif value is False: + value = bool_false + processed_row.append(value) + rows.append(processed_row) + + sort = self.config.get(category, {}).get("sort", None) + if sort: + + def natural_sort(value): + 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 + + # Sort least important keys first, then more important keys. + # https://stackoverflow.com/questions/11476371/sort-by-multiple-keys-using-different-orderings + for sort_data in reversed(sort): + i = headers.index(sort_data["name"]) + reverse = sort_data["order"] == "DESC" + rows = sorted(rows, key=lambda x: natural_sort(x[i]), reverse=reverse) + self.categories[category] = {"headers": headers, "rows": rows} + + def write_csv(self, output, delimiter=","): + filename = None + if len(self.categories.keys()) == 1 and "." in os.path.basename(output): + filename = output + for category, data in self.categories.items(): + category_filename = filename or os.path.join(output, f"{category}.csv") + with open(category_filename, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f, delimiter=delimiter) + writer.writerow(data["headers"]) + for row in data["rows"]: + writer.writerow(row) + + def write_ods(self, output): + doc = OpenDocumentSpreadsheet() + + for key, value in self.colours.items(): + style = Style(name=key, family="table-cell") + style.addElement(TableCellProperties(backgroundcolor="#" + value)) + doc.automaticstyles.addElement(style) + + for category, data in self.categories.items(): + colours = self.config.get(category, {}).get("colours", []) + + table = Table(name=category) + tr = TableRow() + for header in data["headers"]: + tc = TableCell(valuetype="string", stylename="h") + tc.addElement(P(text=header)) + tr.addElement(tc) + table.addElement(tr) + for row in data["rows"]: + tr = TableRow() + c = 0 + for col in row: + if c >= len(colours): + cell_format = "n" + else: + cell_format = colours[c] + tc = TableCell(valuetype="string", stylename=cell_format) + tc.addElement(P(text=str(col))) + tr.addElement(tc) + c += 1 + table.addElement(tr) + doc.spreadsheet.addElement(table) + + if len(output) > 4 and output[-4:].lower() == ".ods": + output = output[0:-4] + + doc.save(output, True) + + +def get_actors(ifc_file): + return ifc_file.by_type("IfcActor") + + +def get_facilities(ifc_file): + return ifc_file.by_type("IfcBuilding") + + +def get_storeys(ifc_file): + return ifc_file.by_type("IfcBuildingStorey") + + +def get_spaces(ifc_file): + return ifc_file.by_type("IfcSpace") + + +def get_zones(ifc_file): + zones = [] + for zone in ifc_file.by_type("IfcZone"): + for rel in zone.IsGroupedBy: + zones.extend([(zone, space) for space in rel.RelatedObjects]) + return zones + + +def get_types(ifc_file): + return ifcopenshell.util.fm.get_fmhem_types(ifc_file) + + +def get_elements(ifc_file): + elements = set() + for element_type in ifcopenshell.util.fm.get_fmhem_types(ifc_file): + elements.update(ifcopenshell.util.element.get_types(element_type)) + return elements + + +def get_systems(ifc_file): + return ifc_file.by_type("IfcSystem") + + +def get_actor_data(ifc_file, element): + return { + "key": element.TheActor.Name, + "Name": element.TheActor.Name, + "Category": get_classification(element), + "Email": get_actor_address(element, "ElectronicMailAddresses"), + "Phone": get_actor_address(element, "TelephoneNumbers"), + "CompanyURL": get_actor_address(element, "WWWHomePageURL"), + "Department": get_actor_address(element, "InternalLocation"), + "Address1": get_actor_address(element, "AddressLines"), + "Address2": get_actor_address(element, "Town"), + "StateRegion": get_actor_address(element, "Region"), + "PostalCode": get_actor_address(element, "PostalCode"), + "Country": get_actor_address(element, "Country"), + } + + +def get_facility_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(ifc_file.by_type("IfcProject")[0]), + "Category": get_classification(element), + "ProjectName": ifc_file.by_type("IfcProject")[0].Name, + "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None), + "LinearUnits": "millimeters", + "AreaUnits": "square meters", + "AreaMeasurement": "BIM Software", + "Phase": ifc_file.by_type("IfcProject")[0].Phase, + "ModelSoftware": get_owner_application(element), + "ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId, + "ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None), + "ModelBuildingID": element.GlobalId, + } + + +def get_storey_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": "Level", + "ModelSoftware": get_owner_application(element), + "ModelObject": element.is_a(), + "ModelID": element.GlobalId, + "Elevation": ifcopenshell.util.placement.get_storey_elevation(element), + } + + +def get_space_data(ifc_file, element): + psets = ifcopenshell.util.element.get_psets(element) + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None), + "Description": element.LongName, + "ModelSoftware": get_owner_application(element), + "ModelID": element.GlobalId, + "AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2), + "AreaNet": get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2), + } + + +def get_zone_data(ifc_file, element): + zone, space = element + return { + "key": (element.Name or "Unnamed") + (space.Name or "Unnamed"), + "Name": zone.Name, + "AuthorOrganizationName": get_owner_name(zone), + "AuthorDate": get_owner_creation_date(zone), + "SpaceName": space.Name, + "ModelSoftware": get_owner_application(zone), + "ModelID": zone.GlobalId, + } + + +def get_type_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "Description": element.Description, + "ModelSoftware": get_owner_application(element), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), + "ModelTag": element.Tag, + "ModelID": element.GlobalId, + } + + +def get_element_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 + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "TypeName": ifcopenshell.util.element.get_type(element).Name, + "SpaceName": space_name, + "SystemName": system, + "ModelSoftware": get_owner_application(element), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), + "ModelID": element.GlobalId, + } + + +def get_system_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "Description": element.Description, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "ModelSoftware": get_owner_application(element), + "ModelID": element.GlobalId, + } + + +def get_owner_name(element): + if not getattr(element, "OwnerHistory", None): + return + return element.OwnerHistory.OwningUser.TheOrganization.Name + + +def get_owner_creation_date(element): + if not getattr(element, "OwnerHistory", None): + return + return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() + + +def get_owner_application(element): + if not getattr(element, "OwnerHistory", None): + return + return 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 get_classification(element): + 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_actor_address(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): + 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)