# 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 openpyxl import Workbook from openpyxl.styles import PatternFill 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": "ppppreeeeessss", "sort": [{"name": "Name", "order": "ASC"}]}, "Storeys": { "colours": "ppreeees", "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}], }, "Spaces": { "colours": "ppprreeess", "sort": [{"name": "LevelName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], }, "Zones": {"colours": "prreee", "sort": [{"name": "Name", "order": "ASC"}]}, "Types": { "colours": "pppreeeee", "sort": [{"name": "ModelObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}], }, "Elements": { "colours": "prrrreeee", "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], }, "Systems": {"colours": "pppreee", "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 write_xlsx(self, output): workbook = Workbook() cell_formats = {} for key, value in self.colours.items(): fill = PatternFill(start_color=value, end_color=value, fill_type="solid") cell_formats[key] = fill for category, data in self.categories.items(): colours = self.config.get(category, {}).get("colours", []) if category in workbook.sheetnames: worksheet = workbook[category] else: worksheet = workbook.create_sheet(category) r = 1 # Openpyxl uses 1-based indexing c = 1 for header in data["headers"]: cell = worksheet.cell(row=r, column=c, value=header) cell.fill = cell_formats["h"] c += 1 r += 1 for row in data["rows"]: c = 1 for col in row: if c > len(colours): # Adjusted the comparison cell_format = "n" else: cell_format = colours[c - 1] # Adjusted the indexing cell = worksheet.cell(row=r, column=c, value=col) cell.fill = cell_formats[cell_format] c += 1 r += 1 workbook.save(output) 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, "ProjectName": ifc_file.by_type("IfcProject")[0].Name, "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None), "Category": get_classification(element), "AuthorOrganizationName": get_owner_name(element), "AuthorDate": get_owner_creation_date(ifc_file.by_type("IfcProject")[0]), "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, "LinearUnits": "millimeters", "AreaUnits": "square meters", "AreaMeasurement": "BIM Software", "Phase": ifc_file.by_type("IfcProject")[0].Phase, } def get_storey_data(ifc_file, element): return { "key": element.Name, "Name": element.Name, "Category": "Level", "AuthorOrganizationName": get_owner_name(element), "AuthorDate": get_owner_creation_date(element), "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, "Description": element.LongName, "Category": get_classification(element), "LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None), "AuthorOrganizationName": get_owner_name(element), "AuthorDate": get_owner_creation_date(element), "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, "SpaceName": space.Name, "AuthorOrganizationName": get_owner_name(zone), "AuthorDate": get_owner_creation_date(zone), "ModelSoftware": get_owner_application(zone), "ModelID": zone.GlobalId, } def get_type_data(ifc_file, element): return { "key": element.Name, "Name": element.Name, "Description": element.Description, "Category": get_classification(element), "AuthorOrganizationName": get_owner_name(element), "AuthorDate": get_owner_creation_date(element), "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, "TypeName": ifcopenshell.util.element.get_type(element).Name, "SpaceName": space_name, "SystemName": system, "AuthorOrganizationName": get_owner_name(element), "AuthorDate": get_owner_creation_date(element), "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, "Category": get_classification(element), "AuthorOrganizationName": get_owner_name(element), "AuthorDate": get_owner_creation_date(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)