From 0285d97e3f235a9e24573b26a5303b7945b97ab4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 9 Sep 2023 21:26:57 +1000 Subject: [PATCH] Reimplement IfcCOBie contacts, facilities, floors, and spaces in IfcFM. --- src/ifcfm/ifcfm.py | 494 --------------------------- src/ifcfm/ifcfm/__init__.py | 243 +++++++++++++ src/ifcfm/ifcfm/basic.py | 391 +++++++++++++++++++++ src/ifcfm/ifcfm/cobie.py | 625 ++++++++++++++++++++++++++++++++++ src/ifcfm/ifcfm/parser.py | 658 ------------------------------------ src/ifcfm/ifcfm/util.py | 94 ------ src/ifcfm/ifcfm/writer.py | 344 ------------------- 7 files changed, 1259 insertions(+), 1590 deletions(-) delete mode 100644 src/ifcfm/ifcfm.py create mode 100644 src/ifcfm/ifcfm/__init__.py create mode 100644 src/ifcfm/ifcfm/basic.py create mode 100644 src/ifcfm/ifcfm/cobie.py delete mode 100644 src/ifcfm/ifcfm/parser.py delete mode 100644 src/ifcfm/ifcfm/util.py delete mode 100644 src/ifcfm/ifcfm/writer.py diff --git a/src/ifcfm/ifcfm.py b/src/ifcfm/ifcfm.py deleted file mode 100644 index 9a4da9649f..0000000000 --- a/src/ifcfm/ifcfm.py +++ /dev/null @@ -1,494 +0,0 @@ -# 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) diff --git a/src/ifcfm/ifcfm/__init__.py b/src/ifcfm/ifcfm/__init__.py new file mode 100644 index 0000000000..ce8f0960b8 --- /dev/null +++ b/src/ifcfm/ifcfm/__init__.py @@ -0,0 +1,243 @@ +# 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 importlib + +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.preset = preset + self.categories = {} + self.get_category_elements = {} + self.get_element_data = {} + self.get_custom_element_data = {} + self.duplicate_keys = [] + + if isinstance(preset, str): + module = importlib.import_module(f"ifcfm.{preset}") + self.get_category_elements = getattr(module, "get_category_elements") + self.get_element_data = getattr(module, "get_element_data") + else: + self.get_category_elements = getattr(preset, "get_category_elements") + self.get_element_data = getattr(preset, "get_element_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): + 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 isinstance(self.parser.preset, str): + module = importlib.import_module(f"ifcfm.{self.parser.preset}") + self.config = getattr(module, "config") + else: + self.config = getattr(self.parser.preset, "config") + + + def write(self, null="N/A", empty="-", bool_true="YES", bool_false="NO"): + self.categories = {} + for category, data in self.parser.categories.items(): + headers = self.config.get(category, {}).get("headers", []) + + if not data: + self.categories[category] = {"headers": headers, "rows": []} + continue + + if not headers: + headers = list(data[list(data.keys())[0]].keys()) + + rows = [] + for row in data.values(): + processed_row = [] + for header in headers: + value = row[header] + 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) diff --git a/src/ifcfm/ifcfm/basic.py b/src/ifcfm/ifcfm/basic.py new file mode 100644 index 0000000000..74c3074d75 --- /dev/null +++ b/src/ifcfm/ifcfm/basic.py @@ -0,0 +1,391 @@ +# 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 + + +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"), + "Address": get_actor_address(element, "AddressLines"), + "Town": get_actor_address(element, "Town"), + "Region": 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(element), + "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): + actors = [] + if element.TheActor.is_a("IfcOrganization") or element.TheActor.is_a("IfcPerson"): + actors = [element.TheActor] + elif element.TheActor.is_a("IfcPersonAndOrganization"): + actors = [element.TheActor.TheOrganization, element.TheActor.ThePerson] + for actor in actors: + for address in actor.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) + + +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, +} + +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, +} + +config = { + "Actors": { + "headers": [ + "Name", + "Category", + "Email", + "Phone", + "CompanyURL", + "Department", + "Address1", + "Address2", + "StateRegion", + "PostalCode", + "Country", + ], + "colours": "ppssssssss", + "sort": [{"name": "Name", "order": "ASC"}], + }, + "Facilities": { + "headers": [ + "Name", + "ProjectName", + "SiteName", + "Category", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelProjectID", + "ModelSiteID", + "ModelBuildingID", + "LinearUnits", + "AreaUnits", + "AreaMeasurement", + "Phase", + ], + "colours": "ppppreeeeessss", + "sort": [{"name": "Name", "order": "ASC"}], + }, + "Storeys": { + "headers": [ + "Name", + "Category", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelObject", + "ModelID", + "Elevation", + ], + "colours": "ppreeees", + "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Spaces": { + "headers": [ + "Name", + "Description", + "Category", + "LevelName", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelID", + "AreaGross", + "AreaNet", + ], + "colours": "ppprreeess", + "sort": [{"name": "LevelName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Zones": { + "headers": ["Name", "SpaceName", "AuthorOrganizationName", "AuthorDate", "ModelSoftware", "ModelID"], + "colours": "prreee", + "sort": [{"name": "Name", "order": "ASC"}], + }, + "Types": { + "headers": [ + "Name", + "Description", + "Category", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelObject", + "ModelTag", + "ModelID", + ], + "colours": "pppreeeee", + "sort": [{"name": "ModelObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Elements": { + "headers": [ + "Name", + "TypeName", + "SpaceName", + "SystemName", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelObject", + "ModelID", + ], + "colours": "prrrreeee", + "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Systems": { + "headers": [ + "Name", + "Description", + "Category", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelID", + ], + "colours": "pppreee", + "sort": [{"name": "Name", "order": "ASC"}], + }, +} diff --git a/src/ifcfm/ifcfm/cobie.py b/src/ifcfm/ifcfm/cobie.py new file mode 100644 index 0000000000..d87fbfeeca --- /dev/null +++ b/src/ifcfm/ifcfm/cobie.py @@ -0,0 +1,625 @@ +# 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 + + +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): + 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_cobie_types(ifc_file) + + +def get_components(ifc_file): + elements = set() + for element_type in ifcopenshell.util.fm.get_cobie_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_contact_data(ifc_file, element): + email = get_email_from_pao(element) + + history = None + created_by = None + created_on = None + histories = ifc_file.by_type("IfcOwnerHistory") + if histories: + history = sorted(histories, key=lambda x: x.id())[-1] + + 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_name(ifc_file, "LENGTHUNIT"), + "AreaUnits": get_unit_name(ifc_file, "AREAUNIT"), + "VolumeUnits": get_unit_name(ifc_file, "VOLUMEUNIT"), + "CurrencyUnit": get_unit_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 external_object.ObjectType and external_object.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): + if height is not None: + break + for name, value in props.items(): + if name in height_names and val(value): + height = str(value) + break + + return { + "key": var(element.Name), + "Name": var(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(str(getattr(element, "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): + 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 + 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_external_system(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_external_system(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_external_system(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_external_system(element), + "ModelID": element.GlobalId, + } + + +def get_unit_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_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.TheActor.ThePerson, element.TheActor.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) + + +get_category_elements = { + "Contact": get_contacts, + "Facility": get_facilities, + "Floor": get_floors, + "Space": get_spaces, + "Zone": get_zones, + "Type": get_types, + "Component": get_components, + "System": get_systems, +} + +get_element_data = { + "Contact": get_contact_data, + "Facility": get_facility_data, + "Floor": get_floor_data, + "Space": get_space_data, + "Zone": get_zone_data, + "Type": get_type_data, + "Component": get_component_data, + "System": get_system_data, +} + +config = { + "Actors": { + "headers": [ + "Name", + "Category", + "Email", + "Phone", + "CompanyURL", + "Department", + "Address1", + "Address2", + "StateRegion", + "PostalCode", + "Country", + ], + "colours": "ppssssssss", + "sort": [{"name": "Name", "order": "ASC"}], + }, + "Facilities": { + "headers": [ + "Name", + "ProjectName", + "SiteName", + "Category", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelProjectID", + "ModelSiteID", + "ModelBuildingID", + "LinearUnits", + "AreaUnits", + "AreaMeasurement", + "Phase", + ], + "colours": "ppppreeeeessss", + "sort": [{"name": "Name", "order": "ASC"}], + }, + "Storeys": { + "headers": [ + "Name", + "Category", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelObject", + "ModelID", + "Elevation", + ], + "colours": "ppreeees", + "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Spaces": { + "headers": [ + "Name", + "Description", + "Category", + "LevelName", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelID", + "AreaGross", + "AreaNet", + ], + "colours": "ppprreeess", + "sort": [{"name": "LevelName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Zones": { + "headers": ["Name", "SpaceName", "AuthorOrganizationName", "AuthorDate", "ModelSoftware", "ModelID"], + "colours": "prreee", + "sort": [{"name": "Name", "order": "ASC"}], + }, + "Types": { + "headers": [ + "Name", + "Description", + "Category", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelObject", + "ModelTag", + "ModelID", + ], + "colours": "pppreeeee", + "sort": [{"name": "ModelObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Elements": { + "headers": [ + "Name", + "TypeName", + "SpaceName", + "SystemName", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelObject", + "ModelID", + ], + "colours": "prrrreeee", + "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + }, + "Systems": { + "headers": [ + "Name", + "Description", + "Category", + "AuthorOrganizationName", + "AuthorDate", + "ModelSoftware", + "ModelID", + ], + "colours": "pppreee", + "sort": [{"name": "Name", "order": "ASC"}], + }, +} diff --git a/src/ifcfm/ifcfm/parser.py b/src/ifcfm/ifcfm/parser.py deleted file mode 100644 index f74ce4ad78..0000000000 --- a/src/ifcfm/ifcfm/parser.py +++ /dev/null @@ -1,658 +0,0 @@ -# IfcFM - IFC for facility management -# Copyright (C) 2021 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 datetime -import ifcopenshell -import ifcopenshell.util.fm -import ifcopenshell.util.selector -import ifcopenshell.util.date -import ifcopenshell.util.schema -import ifcopenshell.util.system -import ifcopenshell.util.placement -import ifcopenshell.util.classification - - -class Parser2: - 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): - data = self.get_element_data[category_name](ifc_file, element) or {} - custom_data = ( - self.get_custom_element_data.get(category_name, lambda x, y: None)(ifc_file, element) or {} - ) - data.update(custom_data) - - if data: - if data["key"] in self.categories[category_name]: - self.duplicate_keys.append((self.categories[category_name][data["key"]], data)) - self.categories[category_name][data["key"]] = data - - -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) - - -class Parser: - def __init__(self, logger): - self.logger = logger - self.file = None - self.categories = { - "actors": self.get_actors, - "facilities": self.get_facilities, - "floors": self.get_floors, - "spaces": self.get_spaces, - "zones": self.get_zones, - "types": self.get_types, - "components": self.get_components, - "systems": self.get_systems, - # "assemblies", - # "connections", - # "spares", - # "resources", - # "jobs", - # "impacts", - "documents": self.get_documents, - # "attributes", - # "coordinates", - # "issues", - } - # COBie - self.custom_parameters = { - "types": { - "AssetType": lambda e, p: None, - "ManufacturerOrganizationName": lambda e, p: None, - "ModelNumber": lambda e, p: None, - "WarrantyGuarantorParts": lambda e, p: None, - "WarrantyDurationParts": lambda e, p: None, - "WarrantyGuarantorLabour": lambda e, p: None, - "WarrantyDurationLabour": lambda e, p: None, - "DurationUnit": lambda e, p: "months", - "WarrantyDescription": lambda e, p: None, - "ReplacementCost": lambda e, p: None, - "ExpectedLife": lambda e, p: None, - "NominalLength": lambda e, p: None, - "NominalWidth": lambda e, p: None, - "NominalHeight": lambda e, p: None, - }, - "components": { - "SerialNumber": lambda e, p: None, - "InstallationDate": lambda e, p: None, - "WarrantyStartDate": lambda e, p: None, - "TagNumber": lambda e, p: None, - "BarCode": lambda e, p: None, - "AssetIdentifier": lambda e, p: None, - } - } - # AOH-BEM - self.custom_parameters = { - "types": { - "ProcurementMethod": lambda e, p: None, - "ManufacturerOrganizationName": lambda e, p: None, - "SupplierOrganizationName": lambda e, p: None, - "ModelNumber": lambda e, p: None, - "WarrantyOrganizationName": lambda e, p: None, - "WarrantyDuration": lambda e, p: None, - "SpecificationSection": lambda e, p: None, - "SubmittalID": lambda e, p: None, - "ProductURL": lambda e, p: None, - }, - "components": { - "InstallationDate": lambda e, p: None, - "WarrantyStartDate": lambda e, p: None, - "InstalledModelNumber": lambda e, p: None, - "SerialNumber": lambda e, p: None, - "BarCode": lambda e, p: None, - "TagNumber": lambda e, p: None, - "OwnerAssetID": lambda e, p: None, - "FluidHotFeedName": lambda e, p: None, - "FluidColdFeedName": lambda e, p: None, - "ElectricPanelName": lambda e, p: None, - "ElectricCircuitName": lambda e, p: None, - "ControlledByName": lambda e, p: None, - "InterlockedWithName": lambda e, p: None, - "PartOfAssemblyName": lambda e, p: None, - } - } - self.picklists = { - "Category-Role": [], - "Category-Facility": [], - "FloorType": [], - "Category-Space": [], - "ZoneType": [], - "Category-Product": [], - "AssetType": [], - "DurationUnit": ["day"], # See note about hardcoded day below - "Category-Element": [], - "SpareType": [], - "ApprovalBy": [], - "StageType": [], - "objType": [], - } - self.default_date = (datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=-2177452801)).isoformat() - - def parse(self, files): - self.files = files - for category, get_category in self.categories.items(): - setattr(self, category, {}) - get_category() - - 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_actor(self, element): - psets = ifcopenshell.util.element.get_psets(element) - return { - "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_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): - return result[0] - return result - - def get_facilities(self): - 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): - 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): - 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 - elevation = element.ObjectPlacement.RelativePlacement.Location.Coordinates[2] - self.floors[name] = { - "Name": name, - "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, - "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(), - "Category": "Level", - "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelObject": element.is_a(), - "ModelID": element.GlobalId, - "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_custom_parameters(self, category, data, element, psets): - for parameter, get_parameter in self.custom_parameters.get(category, {}).items(): - data[parameter] = get_parameter(element, psets) - - def get_spaces(self): - 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) - data = { - "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), - } - self.get_custom_parameters("spaces", data, element, psets) - self.spaces[name] = data - - def get_zones(self): - 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 or "Unnamed") + (space.Name or "Unnamed")] = { - "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(): - for element in ifc.by_type("IfcSystem"): - name = element.Name - self.systems[name] = { - "Name": name, - "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, - "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(), - "Category": None, - "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelID": element.GlobalId, - "ParentSystemName": None, - } - - def get_types(self): - for discipline, ifc in self.files.items(): - self.get_types_from_file(discipline) - - def get_types_from_file(self, ifc_file): - primary_keys = [] - 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) - data = { - "Name": name, - "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, - "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(), - "Category": self.get_classification(element), - "Description": element.Description, - "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), - "ModelID": element.GlobalId, - } - self.get_custom_parameters("types", data, element, psets) - self.types[name] = data - - def get_components(self): - for discipline, ifc in self.files.items(): - self.get_components_from_file(discipline) - - def get_components_from_file(self, ifc_file): - for element_type in ifcopenshell.util.fm.get_fmhem_types(self.files[ifc_file]): - 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 - for rel in element.HasAssignments: - if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a("IfcSystem"): - system = rel.RelatingGroup.Name - - space = ifcopenshell.util.element.get_container(element) - space_name = space.Name if space.is_a("IfcSpace") else None - - psets = ifcopenshell.util.element.get_psets(element) - - data = { - "Name": name, - "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name, - "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(), - "TypeName": element_type.Name, - "SpaceName": space_name, - "SystemName": system, - "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName, - "ModelObject": "{}[{}]".format( - element.is_a(), ifcopenshell.util.element.get_predefined_type(element) - ), - "ModelID": element.GlobalId, - } - self.get_custom_parameters("components", data, element, psets) - self.components[name] = data - - def get_documents(self): - 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: - worksheet_row = related_object.Name - if ifc.schema == "IFC2X3": - referenced_document = element.ReferenceToDocument[0] - identification = referenced_document.ItemReference - author_date = None - if referenced_document.CreationTime: - author_date = ifcopenshell.util.date.ifc2datetime( - referenced_document.CreationTime - ).isoformat() - else: - referenced_document = element.ReferencedDocument - identification = referenced_document.Identification - 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" - - self.documents[identification + worksheet_name + worksheet_row] = { - "Name": identification, - "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.Location, - "Description": referenced_document.Name, - "SpecificationSection": None, - "SubmittalID": None, - "SourceURL": None, - } diff --git a/src/ifcfm/ifcfm/util.py b/src/ifcfm/ifcfm/util.py deleted file mode 100644 index 199b280b6e..0000000000 --- a/src/ifcfm/ifcfm/util.py +++ /dev/null @@ -1,94 +0,0 @@ -# IfcFM - IFC for facility management -# Copyright (C) 2021 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 textwrap -import ifcopenshell -import ifcopenshell.util.fm -import ifcopenshell.util.schema -import ifcopenshell.util.attribute - - -def print_element(declaration, should_print_subtypes=True): - if declaration.name() in ifcopenshell.util.fm.fmhem_excluded_classes: - pass - elif declaration.is_abstract(): - pass - else: - types = [] - for attribute in declaration.all_attributes(): - if attribute.name() == "PredefinedType": - types = list(ifcopenshell.util.attribute.get_enum_items(attribute)) - if "NOTDEFINED" in types: - types.remove("NOTDEFINED") - print("{}".format(declaration.name())) - if types: - types = sorted(types) - for line in textwrap.wrap(", ".join(types), width=70): - print("\t\t{}".format(line)) - if should_print_subtypes: - for subtype in declaration.subtypes(): - print_element(subtype) - - -def print_fmhem_documentation(schema="IFC4"): - if schema == "IFC4": - classes = ifcopenshell.util.fm.fmhem_classes_ifc4 - elif schema == "IFC2X3": - classes = ifcopenshell.util.fm.fmhem_classes_ifc2x3 - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema) - for ifc_class in classes: - try: - declaration = schema.declaration_by_name(ifc_class) - print_element(declaration) - except: - pass - - -def print_all_documentation(schema="IFC4"): - if schema == "IFC4": - classes = ifcopenshell.util.fm.fmhem_classes_ifc4 - elif schema == "IFC2X3": - classes = ifcopenshell.util.fm.fmhem_classes_ifc2x3 - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema) - class_queue = [schema.declaration_by_name("IfcElementType")] - maintainable_declarations = [] - other_declarations = [] - while class_queue: - declaration = class_queue.pop(0) - class_queue.extend(declaration.subtypes()) - if declaration.is_abstract(): - continue - is_maintainable = False - for ifc_class in classes: - if ifcopenshell.util.schema.is_a(declaration, ifc_class): - is_maintainable = True - break - if is_maintainable: - maintainable_declarations.append(declaration) - else: - other_declarations.append(declaration) - print("# Maintainable classes\n") - for declaration in maintainable_declarations: - print_element(declaration, should_print_subtypes=False) - print("\n\n# Other classes\n") - for declaration in other_declarations: - print_element(declaration, should_print_subtypes=False) - - -print_all_documentation("IFC2X3") diff --git a/src/ifcfm/ifcfm/writer.py b/src/ifcfm/ifcfm/writer.py deleted file mode 100644 index bb102ab63c..0000000000 --- a/src/ifcfm/ifcfm/writer.py +++ /dev/null @@ -1,344 +0,0 @@ -# IfcFM - IFC for facility management -# Copyright (C) 2021 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 csv - -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 - - -# https://stackoverflow.com/questions/1143671/how-to-sort-objects-by-multiple-keys-in-python - -from operator import itemgetter as i -from functools import cmp_to_key - - -def cmp(x, y): - """ - Replacement for built-in function cmp that was removed in Python 3 - - Compare the two objects x and y and return an integer according to - the outcome. The return value is negative if x < y, zero if x == y - and strictly positive if x > y. - - https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function - """ - - try: - return (x > y) - (x < y) - except: - return 0 - - -def multikeysort(items, columns): - comparers = [((i(col[1:].strip()), -1) if col.startswith("-") else (i(col.strip()), 1)) for col in columns] - - def comparer(left, right): - comparer_iter = (cmp(fn(left), fn(right)) * mult for fn, mult in comparers) - return next((result for result in comparer_iter if result), 0) - - return sorted(items, key=cmp_to_key(comparer)) - - -class Writer: - def __init__(self, parser, filename=None): - self.filename = filename - self.parser = parser - self.sheet_data = {} - self.colours = { - "r": "fdff8e", # Required - "i": "fdcd94", # Internal reference - "e": "cd95ff", # External reference - "o": "cdffc8", # Optional - "s": "c0c0c0", # Secondary information - "p": "9ccaff", # Project specific - "n": "000000", # Not used - } - self.colours = { - "r": "dc8774", # Required - "i": "eda786", # Internal reference - "e": "96c7d0", # External reference - "o": "ddb873", # Optional or edd889 - "s": "dddddd", # Secondary information - "p": "b8dd73", # Project specific - "n": "000000", # Not used - } - self.sheets = { - "Actor": { - "data": self.parser.actors, - "fields": [ - "Name", - "Category", - "Email", - "Phone", - "CompanyURL", - "Department", - "Address1", - "Address2", - "StateRegion", - "PostalCode", - "Country", - ], - "colours": "rirrrrrrrrr", - "order": ["Name"], - }, - "Facility": { - "data": self.parser.facilities, - "fields": [ - "Name", - "AuthorOrganizationName", - "AuthorDate", - "Category", - "ProjectName", - "SiteName", - "LinearUnits", - "AreaUnits", - "AreaMeasurement", - "Phase", - "ModelSoftware", - "ModelProjectID", - "ModelSiteID", - "ModelBuildingID", - ], - "colours": "ririrrrrrreeee", - "order": ["Name"], - }, - "Floor": { - "data": self.parser.floors, - "fields": [ - "Name", - "AuthorOrganizationName", - "AuthorDate", - "Category", - "ModelSoftware", - "ModelObject", - "ModelID", - "Elevation", - ], - "colours": "ririeeer", - "order": ["Elevation"], - }, - "Space": { - "data": self.parser.spaces, - "fields": [ - "Name", - "AuthorOrganizationName", - "AuthorDate", - "Category", - "LevelName", - "Description", - "ModelSoftware", - "ModelID", - "BuildingRoomNumber", - "UsableHeight", - "AreaGross", - "AreaNet", - ], - "colours": "ririireerrrr", - "order": ["LevelName", "Name"], - }, - "Zone": { - "data": self.parser.zones, - "fields": [ - "Name", - "AuthorOrganizationName", - "AuthorDate", - "Category", - "SpaceName", - "ModelSoftware", - "ModelID", - "ParentZoneName", - ], - "colours": "ririieei", - "order": ["Name", "SpaceName"], - }, - "Type": { - "data": self.parser.types, - "fields": [ - "Name", - "AuthorOrganizationName", - "AuthorDate", - "Category", - "Description", - "ModelSoftware", - "ModelObject", - "ModelID", - ], - "colours": "ririreee", - "order": ["ModelObject", "Name"], - }, - "Component": { - "data": self.parser.components, - "fields": [ - "Name", - "AuthorOrganizationName", - "AuthorDate", - "TypeName", - "SpaceName", - "SystemName", - "ModelSoftware", - "ModelObject", - "ModelID", - ], - "colours": "ririiieee", - "order": ["ModelObject", "Name"], - }, - "System": { - "data": self.parser.systems, - "fields": [ - "Name", - "AuthorOrganizationName", - "AuthorDate", - "Category", - "ModelSoftware", - "ModelID", - "ParentSystemName", - ], - "colours": "ririeei", - "order": ["Name"], - }, - "Document": { - "data": self.parser.documents, - "fields": [ - "Name", - "AuthorOrganizationName", - "AuthorDate", - "Category", - "WorksheetName", - "WorksheetRow", - "Revision", - "Location", - "Description", - "SpecificationSection", - "SubmittalID", - "SourceURL", - ], - "colours": "ririiirrrrer", - "order": ["Name"], - }, - } - - def write(self): - for category, spec in self.sheets.items(): - self.write_data(category, spec["data"], spec["fields"], spec["colours"], spec["order"]) - - def write_data(self, sheet, data, fieldnames, colours, sort_fields): - self.sheet_data[sheet] = {"headers": fieldnames, "colours": colours, "rows": []} - for row in multikeysort(list(data.values()), sort_fields): - values = [] - for fieldname in fieldnames: - values.append(row[fieldname]) - self.sheet_data[sheet]["rows"].append(values) - - -class CsvWriter(Writer): - def write(self): - super().write() - for sheet, data in self.sheet_data.items(): - with open(os.path.join(self.filename, "{}.csv".format(sheet)), "w", newline="", encoding="utf-8") as file: - writer = csv.writer(file) - writer.writerow(data["headers"]) - for row in data["rows"]: - writer.writerow(row) - - -class XlsWriter(Writer): - def write(self): - super().write() - self.workbook = Workbook(self.filename + ".xlsx") - - self.cell_formats = {} - for key, value in self.colours.items(): - self.cell_formats[key] = self.workbook.add_format() - self.cell_formats[key].set_bg_color(value) - - for sheet in self.sheets.keys(): - self.write_worksheet(sheet) - self.workbook.close() - - def write_worksheet(self, name): - worksheet = self.workbook.add_worksheet(name) - r = 0 - c = 0 - for header in self.sheet_data[name]["headers"]: - cell = worksheet.write(r, c, header, self.cell_formats["s"]) - c += 1 - c = 0 - r += 1 - for row in self.sheet_data[name]["rows"]: - c = 0 - for col in row: - if c >= len(self.sheet_data[name]["colours"]): - cell_format = "p" - else: - cell_format = self.sheet_data[name]["colours"][c] - cell = worksheet.write(r, c, col, self.cell_formats[cell_format]) - c += 1 - r += 1 - - -class OdsWriter(Writer): - def write(self): - super().write() - self.doc = OpenDocumentSpreadsheet() - - self.cell_formats = {} - for key, value in self.colours.items(): - style = Style(name=key, family="table-cell") - style.addElement(TableCellProperties(backgroundcolor="#" + value)) - self.doc.automaticstyles.addElement(style) - self.cell_formats[key] = style - - for sheet in self.sheets.keys(): - self.write_table(sheet) - self.doc.save(self.filename, True) - - def write_table(self, name): - table = Table(name=name) - tr = TableRow() - for header in self.sheet_data[name]["headers"]: - tc = TableCell(valuetype="string", stylename="s") - tc.addElement(P(text=header)) - tr.addElement(tc) - table.addElement(tr) - for row in self.sheet_data[name]["rows"]: - tr = TableRow() - c = 0 - for col in row: - if c >= len(self.sheet_data[name]["colours"]): - cell_format = "p" - else: - cell_format = self.sheet_data[name]["colours"][c] - tc = TableCell(valuetype="string", stylename=cell_format) - if col is None: - col = "NULL" - tc.addElement(P(text=col)) - tr.addElement(tc) - c += 1 - table.addElement(tr) - self.doc.spreadsheet.addElement(table)