mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
495 lines
18 KiB
Python
495 lines
18 KiB
Python
# IfcFM - IFC for facility management
|
|
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
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)
|