Files
IfcOpenShell/src/ifccobie/cobie.py
T
Dion Moult ee82714423 Whoops
2023-07-04 19:47:49 +10:00

1668 lines
71 KiB
Python
Executable File

#!/usr/bin/env python3
# IfcCOBie - Extract COBie data from IFC to spreadsheets
# Copyright (C) 2019, 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcCOBie.
#
# IfcCOBie 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.
#
# IfcCOBie 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 IfcCOBie. If not, see <http://www.gnu.org/licenses/>.
# This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico bimtester.py`
import os
import time
import argparse
import datetime
import logging
import ifcopenshell
import ifcopenshell.util.selector
import ifcopenshell.util.placement
class IfcCobieParser:
def __init__(self, logger, selector):
self.selector = selector
self.logger = logger
self.file = None
self.sheets = [
"contacts",
"facilities",
"floors",
"spaces",
"zones",
"types",
"components",
"systems",
"assemblies",
"connections",
"spares",
"resources",
"jobs",
"impacts",
"documents",
"attributes",
"coordinates",
"issues",
]
for sheet in self.sheets:
setattr(self, sheet, {})
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, file, type_query=".COBieType", component_query=".COBie", custom_data={}):
self.custom_data = custom_data
for sheet in self.sheets:
if sheet not in self.custom_data:
self.custom_data[sheet] = {}
if isinstance(file, str):
self.file = ifcopenshell.open(file)
else:
self.file = file
self.type_assets = self.selector.parse(self.file, type_query)
self.component_assets = self.selector.parse(self.file, component_query)
self.get_contacts()
self.get_facilities()
self.get_floors()
self.get_spaces()
self.get_zones()
self.get_types()
self.get_components()
self.get_systems()
self.get_assemblies()
self.get_connections()
self.get_spares()
self.get_resources()
self.get_jobs()
self.get_impacts()
self.get_documents()
self.get_attributes()
self.get_coordinates()
self.get_issues()
def get_contacts(self):
histories = self.file.by_type("IfcOwnerHistory")
for history in histories:
email = self.get_email_from_history(history)
if not email:
continue
postal_address = self.get_postal_address_from_history(history)
self.contacts[email] = {
"CreatedBy": email,
"CreatedOn": datetime.datetime.fromtimestamp(history.CreationDate).isoformat()
if history.CreationDate
else datetime.datetime.now().isoformat(),
"Category": self.get_category_from_history(history),
"Company": history.OwningUser.TheOrganization.Name or "n/a",
"Phone": self.get_phone_from_history(history),
"ExtSystem": self.get_ext_system_from_history(history),
"ExtObject": self.get_ext_object_from_history(history),
"ExtIdentifier": history.OwningUser.ThePerson.Id
if self.file.schema == "IFC2X3"
else history.OwningUser.ThePerson.Identification,
"Department": self.get_department_from_history(history),
"OrganizationCode": (history.OwningUser.TheOrganization.Id or "n/a")
if self.file.schema == "IFC2X3"
else (history.OwningUser.TheOrganization.Identification or "n/a"),
"GivenName": self.get_name_from_person(history.OwningUser.ThePerson, "GivenName"),
"FamilyName": self.get_name_from_person(history.OwningUser.ThePerson, "FamilyName"),
"Street": self.get_lines_from_address(postal_address),
"PostalBox": self.get_attribute_from_address(postal_address, "PostalBox"),
"Town": self.get_attribute_from_address(postal_address, "Town"),
"StateRegion": self.get_attribute_from_address(postal_address, "Region"),
"PostalCode": self.get_attribute_from_address(postal_address, "PostalCode"),
"Country": self.get_attribute_from_address(postal_address, "Country"),
}
for field, key in self.custom_data["contacts"].items():
self.contacts[email][field] = self.get_element_value(history, key)
def get_facilities(self):
buildings = self.file.by_type("IfcBuilding")
for building in buildings:
building_name = self.get_object_name(building)
units = self.get_units_from_building(building)
self.facilities[building_name] = {
"CreatedBy": self.get_email_from_history(building.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(building.OwnerHistory),
"Category": self.get_category_from_object(building, "Category-Facility"),
"ProjectName": self.get_project_name_from_building(building),
"SiteName": self.get_site_name_from_building(building),
"LinearUnits": self.get_unit_type_from_units(units, "LENGTHUNIT"),
"AreaUnits": self.get_unit_type_from_units(units, "AREAUNIT"),
"VolumeUnits": self.get_unit_type_from_units(units, "VOLUMEUNIT"),
"CostUnit": self.get_monetary_unit_from_units(units),
"AreaMeasurement": self.get_area_measurement_from_building(building),
"ExternalSystem": self.get_ext_system_from_history(building.OwnerHistory),
"ExternalProjectObject": self.get_ext_project_object(),
"ExternalProjectIdentifier": self.get_project_globalid_from_building(building),
"ExternalSiteObject": self.get_ext_site_object(),
"ExternalSiteIdentifier": self.get_site_globalid_from_building(building),
"ExternalFacilityObject": self.get_ext_object(building),
"ExternalFacilityIdentifier": building.GlobalId,
"Description": self.get_object_attribute(building, "Description", default="n/a"),
"ProjectDescription": self.get_object_attribute(
self.get_parent_spatial_element(building, "IfcProject"), "Description", default="n/a"
),
"SiteDescription": self.get_object_attribute(
self.get_parent_spatial_element(building, "IfcSite"), "Description", default="n/a"
),
"Phase": self.get_object_attribute(
self.get_parent_spatial_element(building, "IfcProject"), "Phase", default="n/a"
),
}
for field, key in self.custom_data["facilities"].items():
self.facilities[building_name][field] = self.get_element_value(building, key)
def get_floors(self):
storeys = self.file.by_type("IfcBuildingStorey")
for storey in storeys:
storey_name = self.get_object_name(storey)
self.floors[storey_name] = {
"CreatedBy": self.get_email_from_history(storey.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(storey.OwnerHistory),
"Category": self.get_category_from_object(storey, "FloorType"),
"ExtSystem": self.get_ext_system_from_history(storey.OwnerHistory),
"ExtObject": self.get_ext_object(storey),
"ExtIdentifier": storey.GlobalId,
"Description": self.get_object_attribute(storey, "Description", default="n/a"),
"Elevation": self.get_object_attribute(storey, "Elevation", default="n/a"),
"Height": self.get_height_from_storey(storey),
}
for field, key in self.custom_data["floors"].items():
self.floors[storey_name][field] = self.get_element_value(storey, key)
def get_spaces(self):
spaces = self.file.by_type("IfcSpace")
for space in spaces:
space_name = self.get_object_name(space)
self.spaces[space_name] = {
"CreatedBy": self.get_email_from_history(space.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(space.OwnerHistory),
"Category": self.get_category_from_object(space, "Category-Space"),
"FloorName": self.get_object_attribute(
self.get_parent_spatial_element(space, "IfcBuildingStorey"),
"Name",
is_primary_key=True,
default="n/a",
),
"Description": self.get_object_attribute(space, "Description", default="n/a"),
"ExtSystem": self.get_ext_system_from_history(space.OwnerHistory),
"ExtObject": self.get_ext_object(space),
"ExtIdentifier": space.GlobalId,
"RoomTag": self.get_pset_value_from_object(space, "COBie_Space", "RoomTag", "n/a"),
"UsableHeight": self.get_usable_height_from_space(space),
"GrossArea": self.get_gross_area_from_space(space),
"NetArea": self.get_net_area_from_space(space),
}
for field, key in self.custom_data["spaces"].items():
self.spaces[space_name][field] = self.get_element_value(space, key)
def get_zones(self):
zones = self.file.by_type("IfcZone")
for zone in zones:
zone_name = self.get_object_name(zone)
self.zones[zone_name] = {
"CreatedBy": self.get_email_from_history(zone.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(zone.OwnerHistory),
"Category": self.get_category_from_object(zone, "ZoneType"),
"SpaceNames": self.get_grouped_product_names_from_object(zone, "IfcSpace"),
"ExtSystem": self.get_ext_system_from_history(zone.OwnerHistory),
"ExtObject": self.get_ext_object(zone),
"ExtIdentifier": zone.GlobalId,
"Description": self.get_object_attribute(zone, "Description", default="n/a"),
}
for field, key in self.custom_data["zones"].items():
self.zones[zone_name][field] = self.get_element_value(zone, key)
def get_types(self):
types = self.file.by_type("IfcTypeObject")
for type in self.type_assets:
# The responsibility matrix states to parse IfcMaterial and
# IfcMaterialLayerSet too, but it doesn't make much sense, so I
# don't parse it.
type_name = self.get_object_name(type)
self.types[type_name] = {
"CreatedBy": self.get_email_from_history(type.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(type.OwnerHistory),
"Category": self.get_category_from_object(type, "Category-Product"),
# The responsibility matrix states two possible fallbacks. I
# choose the 'n/a' option as opposed to repeating the name.
"Description": self.get_object_attribute(type, "Description", default="n/a"),
"AssetType": self.get_pset_value_from_object(type, "COBie_Asset", "AssetType", "n/a", "AssetType"),
"Manufacturer": self.get_contact_pset_value_from_object(
type, "Pset_ManufacturerTypeInformation", "Manufacturer"
),
"ModelNumber": self.get_pset_value_from_object(
type, "Pset_ManufacturerTypeInformation", "ModelLabel", "n/a"
),
# The responsibility matrix talks about using the Pset_Warranty
# values, but Pset_Warranty only applies to objects, not types,
# and so they are ignored.
"WarrantyGuarantorParts": self.get_contact_pset_value_from_object(
type, "COBie_Warranty", "WarrantyGuarantorParts"
),
"WarrantyDurationParts": self.get_pset_value_from_object(
type, "COBie_Warranty", "WarrantyDurationParts", 0
),
"WarrantyGuarantorLabor": self.get_contact_pset_value_from_object(
type, "COBie_Warranty", "WarrantyGuarantorLabor"
),
"WarrantyDurationLabor": self.get_pset_value_from_object(
type, "COBie_Warranty", "WarrantyDurationLabor", 0
),
# TODO: this may be derived from the duration values above, but
# until it is clarified, it will be hardcoded as 'day'
"WarrantyDurationUnit": "day",
"ExtSystem": self.get_ext_system_from_history(type.OwnerHistory),
"ExtObject": self.get_ext_object(type),
"ExtIdentifier": type.GlobalId,
"ReplacementCost": self.get_pset_value_from_object(
type, "COBie_EconomicImpactValues", "ReplacementCost", "n/a"
),
"ExpectedLife": self.get_expected_life_from_type(type),
# See note about WarrantyDurationUnit above
"DurationUnit": "day",
"NominalLength": self.get_pset_value_from_object(type, "COBie_Specification", "NominalLength", 0),
"NominalWidth": self.get_pset_value_from_object(type, "COBie_Specification", "NominalWidth", 0),
"NominalHeight": self.get_pset_value_from_object(type, "COBie_Specification", "NominalHeight", 0),
"ModelReference": self.get_pset_value_from_object(
type, "Pset_ManufacturerTypeInformation", "ModelReference", "n/a"
),
"Shape": self.get_pset_value_from_object(type, "COBie_Specification", "Shape", "n/a"),
"Size": self.get_pset_value_from_object(type, "COBie_Specification", "Size", "n/a"),
# The responsbility matrix allows the British spelling of
# "colour". I, however, do not.
"Color": self.get_pset_value_from_object(type, "COBie_Specification", "Color", "n/a"),
"Finish": self.get_pset_value_from_object(type, "COBie_Specification", "Finish", "n/a"),
"Grade": self.get_pset_value_from_object(type, "COBie_Specification", "Grade", "n/a"),
"Material": self.get_pset_value_from_object(type, "COBie_Specification", "Material", "n/a"),
"Constituents": self.get_pset_value_from_object(type, "COBie_Specification", "Constituents", "n/a"),
"Features": self.get_pset_value_from_object(type, "COBie_Specification", "Features", "n/a"),
"AccessibilityPerformance": self.get_pset_value_from_object(
type, "COBie_Specification", "AccessibilityPerformance", "n/a"
),
"CodePerformance": self.get_pset_value_from_object(
type, "COBie_Specification", "CodePerformance", "n/a"
),
"SustainabilityPerformance": self.get_pset_value_from_object(
type, "COBie_Specification", "SustainabilityPerformance", "n/a"
),
}
for field, key in self.custom_data["types"].items():
self.types[type_name][field] = self.get_element_value(type, key)
def get_components(self):
components = self.file.by_type("IfcElement")
for component in components:
if not self.is_object_a_component_asset(component):
self.logger.warning("A component which is not an asset was found for %s", component)
continue
component_name = self.get_object_name(component)
self.components[component_name] = {
"CreatedBy": self.get_email_from_history(component.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(component.OwnerHistory),
"TypeName": self.get_type_name_from_object(component),
"Space": self.get_space_name_from_component(component),
"Description": self.get_object_attribute(component, "Description", default="n/a"),
"ExtSystem": self.get_ext_system_from_history(component.OwnerHistory),
"ExtObject": self.get_ext_object(component),
"ExtIdentifier": component.GlobalId,
"SerialNumber": self.get_pset_value_from_object(
component, "Pset_ManufacturerOccurence", "SerialNumber", "n/a"
),
"InstallationDate": self.get_pset_value_from_object(
component, "COBie_Component", "InstallationDate", self.default_date
),
"WarrantyStartDate": self.get_pset_value_from_object(
component, "COBie_Component", "WarrantyStartDate", self.default_date
),
"TagNumber": self.get_pset_value_from_object(component, "COBie_Component", "TagNumber", "n/a"),
"BarCode": self.get_pset_value_from_object(component, "Pset_ManufacturerOccurence", "BarCode", "n/a"),
"AssetIdentifier": self.get_pset_value_from_object(
component, "COBie_Component", "AssetIdentifier", "n/a"
),
}
for field, key in self.custom_data["components"].items():
self.components[component_name][field] = self.get_element_value(component, key)
def get_systems(self):
systems = self.file.by_type("IfcSystem")
for system in systems:
system_name = self.get_object_name(system)
self.systems[system_name] = {
"CreatedBy": self.get_email_from_history(system.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(system.OwnerHistory),
"Category": self.get_category_from_object(system, "Category-Element"),
"ComponentNames": self.get_grouped_product_names_from_object(system, "IfcProduct"),
"ExtSystem": self.get_ext_system_from_history(system.OwnerHistory),
"ExtObject": self.get_ext_object(system),
"ExtIdentifier": system.GlobalId,
"Description": self.get_object_attribute(system, "Description", default="n/a"),
}
for field, key in self.custom_data["systems"].items():
self.systems[system_name][field] = self.get_element_value(system, key)
def get_assemblies(self):
assemblies = self.file.by_type("IfcRelAggregates")
for assembly in assemblies:
assembly_name = self.get_object_name(assembly)
if not self.is_object_a_component_asset(assembly.RelatingObject):
continue
self.assemblies[assembly_name] = {
"CreatedBy": self.get_email_from_history(assembly.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(assembly.OwnerHistory),
"SheetName": "Assembly",
"ParentName": self.get_object_name(assembly.RelatingObject),
"ChildNames": ",".join([o.Name if o.Name else "" for o in assembly.RelatedObjects]),
"AssemblyType": "n/a", # I don't understand this field
"ExtSystem": self.get_ext_system_from_history(assembly.OwnerHistory),
"ExtObject": self.get_ext_object(assembly),
"ExtIdentifier": assembly.GlobalId,
"Description": self.get_object_attribute(assembly, "Description", default="n/a"),
}
for field, key in self.custom_data["assemblies"].items():
self.assemblies[assembly_name][field] = self.get_element_value(assembly, key)
def get_connections(self):
connections = self.file.by_type("IfcRelConnects")
for connection in connections:
connection_name = self.get_object_name(connection)
self.connections[connection_name] = {
"CreatedBy": self.get_email_from_history(connection.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(connection.OwnerHistory),
# There is ambiguity for what the ConnectionType mapping should be
"ConnectionType": self.get_object_attribute(connection, "Description", default="n/a"),
"SheetName": "Connections",
"RowName1": self.get_row_name_from_connection(connection, "RelatingElement"),
"RowName2": self.get_row_name_from_connection(connection, "RelatedElement"),
"RealizingElement": self.get_port_name_from_connection(connection, "RealizingElement"),
"PortName1": self.get_port_name_from_connection(connection, "RelatingPort"),
"PortName2": self.get_port_name_from_connection(connection, "RelatedPort"),
"ExtSystem": self.get_ext_system_from_history(connection.OwnerHistory),
"ExtObject": self.get_ext_object(connection),
"ExtIdentifier": connection.GlobalId,
"Description": self.get_object_attribute(connection, "Description", default="n/a"),
}
for field, key in self.custom_data["connections"].items():
self.connections[connection_name][field] = self.get_element_value(connection, key)
def get_spares(self):
spares = self.file.by_type("IfcConstructionProductResource")
for spare in spares:
spare_name = self.get_object_name(spare)
self.spares[spare_name] = {
"CreatedBy": self.get_email_from_history(spare.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(spare.OwnerHistory),
"Category": self.get_category_from_object(spare, "SpareType"),
"TypeName": self.get_type_name_from_object(spare),
"Suppliers": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "Suppliers"),
"ExtSystem": self.get_ext_system_from_history(spare.OwnerHistory),
"ExtObject": self.get_ext_object(spare),
"ExtIdentifier": spare.GlobalId,
"Description": self.get_object_attribute(spare, "Description", default="n/a"),
"SetNumber": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "SetNumber"),
"PartNumber": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "PartNumber"),
}
for field, key in self.custom_data["spares"].items():
self.spares[spare_name][field] = self.get_element_value(spare, key)
def get_resources(self):
resources = self.file.by_type("IfcConstructionProductResource")
for resource in resources:
resource_name = self.get_object_name(resource)
self.resources[resource_name] = {
"CreatedBy": self.get_email_from_history(resource.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(resource.OwnerHistory),
"Category": self.get_object_attribute(resource, "ObjectType", picklist="ResourceType", default="n/a"),
"ExtSystem": self.get_ext_system_from_history(resource.OwnerHistory),
"ExtObject": self.get_ext_object(resource),
"ExtIdentifier": resource.GlobalId,
"Description": self.get_object_attribute(resource, "Description", default="n/a"),
}
for field, key in self.custom_data["resources"].items():
self.resources[resource_name][field] = self.get_element_value(resource, key)
def get_jobs(self):
jobs = self.file.by_type("IfcTask")
for job in jobs:
job_name = self.get_object_name(job)
task_time = job.TaskTime
self.jobs[job_name] = {
"CreatedBy": self.get_email_from_history(job.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(job.OwnerHistory),
"Category": self.get_object_attribute(job, "ObjectType", picklist="JobType", default="n/a"),
"Status": self.get_object_attribute(job, "Status", picklist="JobStatusType", default="n/a"),
"TypeName": self.get_type_name_from_object(job),
"Description": self.get_object_attribute(job, "Description", default="n/a"),
"Duration": self.get_object_attribute(task_time, "ScheduleDuration", default=0),
"DurationUnit": "day",
"Start": self.get_object_attribute(task_time, "ScheduleStart", default=0),
"TaskStartUnit": "day",
"Frequency": self.get_object_attribute(task_time.Recurrence, "Occurrences", default=0)
if hasattr(task_time, "Recurrence")
else 0,
"FrequencyUnit": "day",
"ExtSystem": self.get_ext_system_from_history(job.OwnerHistory),
"ExtObject": self.get_ext_object(job),
"ExtIdentifier": job.GlobalId,
"TaskNumber": self.get_object_attribute(job, "Identification"),
"Priors": self.get_priors_from_job(job),
"ResourceNames": self.get_resource_names_from_job(job),
}
for field, key in self.custom_data["jobs"].items():
self.jobs[job_name][field] = self.get_element_value(job, key)
# Impacts is not explicitly defined as a mapping in the responsibliity
# matrix. This is my best guess. This data should not be relied upon until
# this is clarified.
def get_impacts(self):
impacts = self.file.by_type("IfcPropertySet")
for impact in impacts:
if impact.Name != "Pset_EnvironmentalImpactValues" or not impact.HasProperties:
continue
for property in impact.HasProperties:
property_name = "{}-{}".format(property.id(), self.get_object_name(property))
self.impacts[property_name] = {
"CreatedBy": self.get_email_from_history(impact.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(impact.OwnerHistory),
"ImpactType": None,
"ImpactStage": None,
"SheetName": "Impacts",
"RowName": "n/a",
"Value": self.get_property_value(property),
"Unit": "{}{}".format(property.Unit.Prefix, property.Unit.Name)
if hasattr(property, "Unit") and property.Unit
else "n/a",
"LeadInTime": self.get_property_value(property, name="LeadInTime"),
"Duration": self.get_property_value(property, name="Duration"),
"LeadOutTime": self.get_property_value(property, name="LeadOutTime"),
"ExtSystem": self.get_ext_system_from_history(impact.OwnerHistory),
"ExtObject": self.get_ext_object(impact),
"ExtIdentifier": impact.GlobalId,
"Description": self.get_object_attribute(impact, "Description", default="n/a"),
}
for field, key in self.custom_data["impacts"].items():
self.impacts[impact_name][field] = self.get_element_value(impact, key)
def get_documents(self):
documents = self.file.by_type("IfcDocumentInformation")
for document in documents:
document_name = self.get_object_name(document)
self.documents[document_name] = {
"CreatedBy": self.get_email_from_history(document.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(document.OwnerHistory),
"Category": "n/a", # I am not sure what this mapping is meant to be
"ApprovalBy": self.get_object_attribute(
document, "IntendedUse", picklist="ApprovalBy", default="Information Only"
),
"Stage": self.get_object_attribute(document, "Scope", picklist="StageType", default="Required"),
"SheetName": "Documents",
"RowName": "n/a",
"Directory": self.get_directory_from_document(document),
"File": self.get_file_from_document(document),
"ExtSystem": self.get_ext_system_from_history(document.OwnerHistory),
"ExtObject": self.get_ext_object(document),
"ExtIdentifier": document.GlobalId,
"Description": self.get_object_attribute(document, "Description", default=document_name),
"Reference": document_name,
}
for field, key in self.custom_data["documents"].items():
self.documents[document_name][field] = self.get_element_value(document, key)
# Attributes is not explicitly defined as a mapping in the responsibliity
# matrix. This is my best guess. This data should not be relied upon until
# this is clarified.
def get_attributes(self):
attributes = self.file.by_type("IfcPropertySet")
for attribute in attributes:
if not attribute.HasProperties:
continue
for property in attribute.HasProperties:
property_name = "{}-{}".format(property.id(), self.get_object_name(property))
self.attributes[property_name] = {
"CreatedBy": self.get_email_from_history(attribute.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(attribute.OwnerHistory),
"Category": "n/a", # I am not sure what this mapping is meant to be
"SheetName": "Attributes",
"RowName": "n/a", # I am not sure what this mapping is meant to be
"Value": self.get_property_value(property),
"Unit": "{}{}".format(
property.Unit.Prefix if hasattr(property.Unit, "Prefix") else "",
property.Unit.Name if hasattr(property.Unit, "Name") else "n/a",
)
if hasattr(property, "Unit") and property.Unit
else "n/a",
"ExtSystem": self.get_ext_system_from_history(attribute.OwnerHistory),
"ExtObject": self.get_ext_object(attribute),
"ExtIdentifier": attribute.GlobalId,
"Description": self.get_object_attribute(attribute, "Description", default="n/a"),
# I'm holding off implementing this until I understand a bit
# more about attributes
"AllowedValues": "n/a",
}
def get_coordinates(self):
coordinates = (
self.file.by_type("IfcBuildingStorey") + self.file.by_type("IfcSpace") + self.file.by_type("IfcProduct")
)
for coordinate in coordinates:
coordinate_name = "{}/{}".format(coordinate.is_a(), self.get_object_name(coordinate))
mat = ifcopenshell.util.placement.get_local_placement(coordinate.ObjectPlacement)
x, y, z = mat[:,3][:3]
self.coordinates[coordinate_name] = {
"CreatedBy": self.get_email_from_history(coordinate.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(coordinate.OwnerHistory),
"Category": "Location", # I am not sure what this mapping is meant to be
"SheetName": "Coordinates",
"RowName": "n/a",
"CoordinateXAxis": x,
"CoordinateYAxis": y,
"CoordinateZAxis": z,
"ExtSystem": self.get_ext_system_from_history(coordinate.OwnerHistory),
"ExtObject": self.get_ext_object(coordinate),
"ExtIdentifier": coordinate.GlobalId,
# Holding off implementing this, see Bug #688:
# https://github.com/IfcOpenShell/IfcOpenShell/issues/688
"ClockwiseRotation": "n/a", # X axis
"ElevationalRotation": "n/a", # Y axis
"YawRotation": "n/a", # Z axis
}
# I don't fully understand this worksheet. Don't trust this data.
def get_issues(self):
issues = self.file.by_type("IfcApproval")
for issue in issues:
issue_name = self.get_object_name(issue)
self.issues[issue_name] = {
"CreatedBy": self.get_email_from_history(issue.OwnerHistory),
"CreatedOn": self.get_created_on_from_history(issue.OwnerHistory),
"Type": "n/a", # How do we get to the Pset_Risk from the IfcApproval?
"Risk": "n/a", # How do we get to the Pset_Risk from the IfcApproval?
"Chance": "n/a", # How do we get to the Pset_Risk from the IfcApproval?
"Impact": "n/a", # How do we get to the Pset_Risk from the IfcApproval?
"SheetName1": "n/a",
"RowName1": "n/a",
"SheetName2": "n/a",
"RowName2": "n/a",
"Description": self.get_object_attribute(issue, "Description", default="n/a"),
"Owner": self.get_email_from_history(issue.RequestingApproval), # Is this correct?
"Mitigation": "n/a",
"ExtSystem": self.get_ext_system_from_history(issue.OwnerHistory),
"ExtObject": self.get_ext_object(issue),
"ExtIdentifier": issue.GlobalId,
}
def get_directory_from_document(self, document):
if self.file.schema == "IFC2X3":
if document.HasDocumentReferences:
for reference in document.HasDocumentReferences:
return self.get_object_attribute(reference, "Location", default="n/a")
else:
return self.get_object_attribute(document, "Location", default="n/a")
def get_file_from_document(self, document):
if self.file.schema == "IFC2X3":
if document.HasDocumentReferences:
for reference in document.HasDocumentReferences:
return self.get_object_attribute(reference, "Name", default="n/a")
else:
self.get_object_attribute(document, "Identification", default="n/a")
def get_resource_names_from_job(self, job):
names = []
if job.OperatesOn and job.OperatesOn.RelatedObjects:
for object in job.OperatesOn.RelatedObjects:
names.append(object.Name)
return ",".join(names)
def get_priors_from_job(self, job):
# The responsibility matrix is vague as to whether it expects a task
# name or a task identification. I chose task name.
if (
job.IsSuccessorFrom
and job.IsSuccessorFrom.RelatingProcess
and job.IsSuccessorFrom.RelatingProcess.is_a("IfcTask")
):
return self.get_object_name(job.IsSuccessorFrom.RelatingProcess)
def get_row_name_from_connection(self, connection, key):
if not connection.is_a("IfcRelConnectsElements"):
return None
object = getattr(connection, key)
if self.is_object_a_component_asset(object):
return object.Name
self.logger.error("The connected object relationship %s is not a component asset for %s", key, connection)
def get_port_name_from_connection(self, connection, key):
if not connection.is_a("IfcRelConnectsPorts"):
return None
object = getattr(connection, key)
if object and self.is_object_a_component_asset(object):
return object.Name
self.logger.error("The connected object relationship %s is not a component asset for %s", key, connection)
def is_object_a_component_asset(self, obj):
return obj in self.component_assets
def get_space_name_from_component(self, component):
for relationship in component.ContainedInStructure:
if relationship.RelatingStructure.is_a("IfcSpace") and relationship.RelatingStructure.Name:
return relationship.RelatingStructure.Name
self.logger.error("A related space name could not be determined for %s", component)
def get_type_name_from_object(self, object):
if self.file.schema == "IFC2X3":
for relationship in object.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByType") and relationship.RelatingType.Name:
return relationship.RelatingType.Name
else:
for relationship in object.IsTypedBy:
if relationship.RelatingType.Name:
return relationship.RelatingType.Name
self.logger.error("A related type name could not be determined for %s", object)
def get_expected_life_from_type(self, type):
if self.file.schema == "IFC2X3":
return self.get_pset_value_from_object(type, "COBie_ServiceLife", "ServiceLifeDuration", "n/a")
return self.get_pset_value_from_object(type, "Pset_ServiceLife", "ServiceLifeDuration", "n/a")
def get_contact_pset_value_from_object(self, object, pset_name, property_name):
result = self.get_pset_value_from_object(object, pset_name, property_name)
if not result:
self.logger.error("No property %s in %s was found for %s", property_name, pset_name, object)
if result not in self.contacts:
self.logger.error("A coresponding %s contact in %s was not found for %s", property_name, pset_name, object)
return result
def get_pset_value_from_object(self, object, pset_name, property_name, default=None, picklist=None):
pset = self.get_pset_from_object(object, pset_name)
if not pset:
if picklist:
self.picklists[picklist].append(default)
return default
prop = self.get_property_from_pset(pset, property_name, default)
if picklist:
self.picklists[picklist].append(prop)
return prop
def get_grouped_product_names_from_object(self, object, type):
names = []
for relationship in object.IsGroupedBy:
for related_object in relationship.RelatedObjects:
if related_object.is_a(type):
names.append(related_object.Name)
if names:
return ",".join(names)
self.logger.error("No related %s were found for %s", type, object)
def get_net_area_from_space(self, space):
qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities")
if not qto:
return "n/a"
return self.get_property_from_qto(qto, "NetFloorArea", "AreaValue")
def get_gross_area_from_space(self, space):
qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities")
if not qto:
return "n/a"
return self.get_property_from_qto(qto, "GrossFloorArea", "AreaValue")
def get_usable_height_from_space(self, space):
qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities")
if not qto:
return "n/a"
return self.get_property_from_qto(qto, "FinishCeilingHeight", "LengthValue")
def get_qto_from_object(self, object, name):
for relationship in object.IsDefinedBy:
if (
relationship.is_a("IfcRelDefinesByProperties")
and relationship.RelatingPropertyDefinition.is_a("IfcQuantitySet")
and relationship.RelatingPropertyDefinition.Name == name
):
return relationship.RelatingPropertyDefinition
self.logger.warning("The qto %s was not found for %s", name, object)
def get_property_from_qto(self, qto, name, attribute):
for property in qto.Quantities:
if property.Name == name:
return getattr(property, attribute)
self.logger.warning("The quantity value %s was not found for %s", name, qto)
return "n/a"
def get_property_from_pset(self, pset, name, default=None):
for prop in pset.HasProperties:
if prop.Name == name:
return prop.NominalValue.wrappedValue
self.logger.warning("The property %s was not found for %s", name, pset)
return default
def get_property_value(self, prop, name=None):
if not prop.is_a("IfcPropertySingleValue"):
return "n/a"
if name is not None and prop.Name != name:
return "n/a"
value = self.get_object_attribute(prop, "NominalValue", default=None)
if value:
return value.wrappedValue
return "n/a"
def get_pset_from_object(self, object, name):
if object.is_a("IfcTypeObject"):
if object.HasPropertySets:
for pset in object.HasPropertySets:
if pset.is_a("IfcPropertySet") and pset.Name == name:
return pset
else:
for relationship in object.IsDefinedBy:
if (
relationship.is_a("IfcRelDefinesByProperties")
and relationship.RelatingPropertyDefinition.is_a("IfcPropertySet")
and relationship.RelatingPropertyDefinition.Name == name
):
return relationship.RelatingPropertyDefinition
self.logger.warning("The pset %s was not found for %s", name, object)
def get_height_from_storey(self, storey):
for relationship in storey.IsDefinedBy:
if not relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity"):
continue
for quantity in relationship.RelatingPropertyDefinition.Quantities:
if quantity.is_a("IfcQuantityLength") and quantity.LengthValue:
return quantity.LengthValue
self.logger.warning("A height length value was not found for %s", storey)
return "n/a"
def get_created_on_from_history(self, history):
if history.CreationDate:
return datetime.datetime.fromtimestamp(history.CreationDate).isoformat()
self.logger.warning("A created on date was not found for %s", history)
return self.default_date
def get_object_attribute(self, object, attribute, is_primary_key=False, picklist=None, default=None):
result = getattr(object, attribute)
if result:
if picklist:
self.picklists[picklist].append(result)
return result
if is_primary_key:
self.logger.error("The primary key attribute %s was not found for %s", attribute, object)
else:
self.logger.warning("The attribute %s was not found for %s", attribute, object)
return default
def get_ext_project_object(self):
self.picklists["objType"].append("IfcProject")
return "IfcProject"
def get_ext_site_object(self):
self.picklists["objType"].append("IfcSite")
return "IfcSite"
def get_ext_object(self, object):
self.picklists["objType"].append(object.is_a())
return object.is_a()
def get_ext_system_from_history(self, history):
return history.OwningApplication.ApplicationFullName
def get_object_name(self, object):
if not object.Name:
self.logger.error("A primary key name was not found for %s", object)
return "Object{}".format(object.id())
return object.Name
def get_area_measurement_from_building(self, building):
for relationship in building.IsDefinedBy:
if (
relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity")
and relationship.RelatingPropertyDefinition.MethodOfMeasurement
):
return relationship.RelatingPropertyDefinition.MethodOfMeasurement
self.logger.warning("A method of measurement was not defined for %s", building)
def get_unit_type_from_units(self, units, type):
for unit in units:
if unit.UnitType == type:
if unit.is_a("IfcSIUnit") and unit.Prefix:
return "{}{}".format(unit.Prefix, unit.Name)
return unit.Name
self.logger.error("A unit %s was not defined in this project for %s", type, units)
def get_monetary_unit_from_units(self, units):
for unit in units:
if unit.is_a("IfcMonetaryUnit"):
return unit.Currency
self.logger.error("A monetary unit could not be found for %s", units)
def get_project_globalid_from_building(self, building):
return self.get_parent_spatial_element(building, "IfcProject").GlobalId
def get_site_globalid_from_building(self, building):
return self.get_parent_spatial_element(building, "IfcSite").GlobalId
def get_project_name_from_building(self, building):
project = self.get_parent_spatial_element(building, "IfcProject")
if project.Name:
return project.Name
self.logger.error("The project name is empty for %s", project)
return "n/a"
def get_site_name_from_building(self, building):
site = self.get_parent_spatial_element(building, "IfcSite")
if site.Name:
return site.Name
self.logger.error("The site name is empty for %s", site)
return "n/a"
def get_units_from_building(self, building):
return self.get_parent_spatial_element(building, "IfcProject").UnitsInContext.Units
def get_parent_spatial_element(self, child, name):
for relationship in child.Decomposes:
if relationship.RelatingObject.is_a(name):
return relationship.RelatingObject
return self.get_parent_spatial_element(relationship.RelatingObject, name)
return None
def get_category_from_object(self, object, picklist):
class_identification = None
class_name = None
for association in object.HasAssociations:
if not association.is_a("IfcRelAssociatesClassification"):
continue
if not association.RelatingClassification.is_a("IfcClassificationReference"):
continue
if self.file.schema == "IFC2X3":
class_identification = association.RelatingClassification.ItemReference
else:
class_identification = association.RelatingClassification.Identification
class_name = association.RelatingClassification.Name
break
if not class_identification or class_name:
self.logger.error("The classification has invalid identification and name for %s", object)
result = "{}:{}".format(class_identification, class_name)
self.picklists[picklist].append(result)
return result
# The responsibility matrix lists a fallback, but it is a very
# cumbersome check, and so it is not implemented here.
def get_name_from_person(self, person, attribute):
name = getattr(person, attribute)
if not name or not name.isalpha():
self.logger.warning('The person\'s %s seems to be badly formatted ("%s") for %s', attribute, name, person)
return name if name else "n/a"
def get_lines_from_address(self, address):
result = self.get_attribute_from_address(address, "AddressLines")
if isinstance(result, tuple):
return ", ".join(result)
return result
def get_attribute_from_address(self, address, attribute):
result = getattr(address, attribute)
if not result:
self.logger.warning("The address %s seems to not exist for %s", attribute, address)
return "n/a"
return result
def get_email_from_history(self, history):
person = history.OwningUser.ThePerson
organisation = history.OwningUser.TheOrganization
email = self.get_email_from_person_or_organisation(person)
if email:
return email
email = self.get_email_from_person_or_organisation(organisation)
if email:
return email
given_name = person.GivenName if person.GivenName else "unknown"
family_name = person.FamilyName if person.FamilyName else "unknown"
organisation_name = organisation.Name if organisation.Name else "unknown"
if given_name == "unknown" and family_name == "unknown" and organisation_name == "unknown":
self.logger.error("No primary key could be determined from %s", history)
return "{}{}@{}".format(given_name, family_name, organisation_name)
def get_postal_address_from_history(self, history):
for address in history.OwningUser.ThePerson.Addresses or []:
if address.is_a("IfcPostalAddress"):
return address
for address in history.OwningUser.TheOrganization.Addresses or []:
if address.is_a("IfcPostalAddress"):
return address
return self.file.createIfcPostalAddress()
def get_category_from_history(self, history):
roles = []
both = history.OwningUser
person = both.ThePerson
organisation = both.TheOrganization
both_roles = list(both.Roles) if both.Roles else []
person_roles = list(person.Roles) if person.Roles else []
organisation_roles = list(organisation.Roles) if organisation.Roles else []
for role in both_roles + person_roles + organisation_roles:
roles.append(self.get_role(role))
result = ",".join(set(roles))
if not result:
self.logger.error("No roles could be found for %s", history)
return
self.picklists["Category-Role"].append(result)
return result
def get_phone_from_history(self, history):
person = history.OwningUser.ThePerson
organisation = history.OwningUser.TheOrganization
phone = self.get_phone_from_person_or_organisation(person)
if phone:
return phone
phone = self.get_phone_from_person_or_organisation(organisation)
if phone:
return phone
return "n/a"
def get_ext_object_from_history(self, history):
result = history.OwningUser.is_a()
self.picklists["objType"].append(result)
return result
def get_department_from_history(self, history):
organisation = history.OwningUser.TheOrganization
department = self.get_internal_location_from_organisation(organisation)
if department:
return department
last_department = None
for relationship in organisation.Relates:
for related_organisation in relationship.RelatedOrganizations:
department = self.get_internal_location_from_organisation(related_organisation)
if department:
last_department = department
if last_department:
return last_department
return history.OwningUser.TheOrganization.Name or "n/a"
def get_internal_location_from_organisation(self, organisation):
for address in organisation.Addresses or []:
if address.is_a("IfcPostalAddress"):
return address.InternalLocation
self.logger.warning("An internal location was not found for %s", organisation)
def get_role(self, role):
if role.Role == "USERDEFINED":
return role.UserDefinedRole
return role.Role
def get_phone_from_person_or_organisation(self, person_or_org):
for address in person_or_org.Addresses or []:
if address.is_a("IfcTelecomAddress"):
return address.TelephoneNumbers[0]
self.logger.warning("A phone was not found for {}", person_or_org)
def get_email_from_person_or_organisation(self, person_or_org):
for address in person_or_org.Addresses or []:
if address.is_a("IfcTelecomAddress"):
return address.ElectronicMailAddresses[0]
self.logger.warning("An email address was not found for {}", person_or_org)
def get_element_value(self, element, key):
value = self.selector.get_element_value(element, key)
if hasattr(value, "wrappedValue"):
return value.wrappedValue
return value
class CobieWriter:
def __init__(self, parser, filename=None):
self.filename = filename
self.parser = parser
self.sheets = []
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
}
def write(self):
self.sheets = [
"Contact",
"Facility",
"Floor",
"Space",
"Zone",
"Type",
"Component",
"System",
"Assembly",
"Connection",
"Spare",
"Resource",
"Job",
"Impact",
"Document",
"Attribute",
"Coordinate",
"Issue",
]
self.write_data(
"Contact",
self.parser.contacts,
"Email",
[
"Email",
"CreatedBy",
"CreatedOn",
"Category",
"Company",
"Phone",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Department",
"OrganizationCode",
"GivenName",
"FamilyName",
"Street",
"PostalBox",
"Town",
"StateRegion",
"PostalCode",
"Country",
],
"ririrreeeoooooooooo",
self.parser.custom_data["contacts"],
)
self.write_data(
"Facility",
self.parser.facilities,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"ProjectName",
"SiteName",
"LinearUnits",
"AreaUnits",
"VolumeUnits",
"CostUnit",
"AreaMeasurement",
"ExternalSystem",
"ExternalProjectObject",
"ExternalProjectIdentifier",
"ExternalSiteObject",
"ExternalSiteIdentifier",
"ExternalFacilityObject",
"ExternalFacilityIdentifier",
"Description",
"ProjectDescription",
"SiteDescription",
"Phase",
],
"ririrriiiireeeeeeeoooo",
self.parser.custom_data["facilities"],
)
self.write_data(
"Floor",
self.parser.floors,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
"Elevation",
"Height",
],
"ririeeeooo",
self.parser.custom_data["floors"],
)
self.write_data(
"Space",
self.parser.spaces,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"FloorName",
"Description",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"RoomTag",
"UsableHeight",
"GrossArea",
"NetArea",
],
"ririireeeoooo",
self.parser.custom_data["spaces"],
)
self.write_data(
"Zone",
self.parser.zones,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"SpaceNames",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
],
"ririieeeo",
self.parser.custom_data["zones"],
)
self.write_data(
"Type",
self.parser.types,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"Description",
"AssetType",
"Manufacturer",
"ModelNumber",
"WarrantyGuarantorParts",
"WarrantyDurationParts",
"WarrantyGuarantorLabor",
"WarrantyDurationLabor",
"WarrantyDurationUnit",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"ReplacementCost",
"ExpectedLife",
"DurationUnit",
"NominalLength",
"NominalWidth",
"NominalHeight",
"ModelReference",
"Shape",
"Size",
"Color",
"Finish",
"Grade",
"Material",
"Constituents",
"Features",
"AccessibilityPerformance",
"CodePerformance",
"SustainabilityPerformance",
],
"riririoooooooeeeooooooooooooooooooo",
self.parser.custom_data["types"],
)
self.write_data(
"Component",
self.parser.components,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"TypeName",
"Space",
"Description",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"SerialNumber",
"InstallationDate",
"WarrantyStartDate",
"TagNumber",
"BarCode",
"AssetIdentifier",
],
"ririireeeoooooo",
self.parser.custom_data["components"],
)
self.write_data(
"System",
self.parser.systems,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"ComponentNames",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
],
"ririieeeo",
self.parser.custom_data["systems"],
)
self.write_data(
"Assembly",
self.parser.assemblies,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"SheetName",
"ParentName",
"ChildNames",
"AssemblyType",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
],
"rirrrrreeeo",
self.parser.custom_data["assemblies"],
)
self.write_data(
"Connection",
self.parser.connections,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"ConnectionType",
"SheetName",
"RowName1",
"RowName2",
"RealizingElement",
"PortName1",
"PortName2",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
],
"ririiiiiiieeeo",
self.parser.custom_data["connections"],
)
self.write_data(
"Spare",
self.parser.spares,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"TypeName",
"Suppliers",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
"SetNumber",
"PartNumber",
],
"ririiieeeooo",
self.parser.custom_data["spares"],
)
self.write_data(
"Resource",
self.parser.resources,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
],
"ririeeeo",
self.parser.custom_data["resources"],
)
self.write_data(
"Job",
self.parser.jobs,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"Status",
"TypeName",
"Description",
"Duration",
"DurationUnit",
"Start",
"TaskStartUnit",
"Frequency",
"FrequencyUnit",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"TaskNumber",
"Priors",
"ResourceNames",
],
"ririiirriririeeeoii",
self.parser.custom_data["jobs"],
)
self.write_data(
"Impact",
self.parser.impacts,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"ImpactType",
"ImpactStage",
"SheetName",
"RowName",
"Value",
"Unit",
"LeadInTime",
"Duration",
"LeadOutTime",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
],
"ririiiirioooeeeo",
self.parser.custom_data["impacts"],
)
self.write_data(
"Document",
self.parser.documents,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"ApprovalBy",
"Stage",
"SheetName",
"RowName",
"Directory",
"File",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
"Reference",
],
"ririiiiirreeeoo",
self.parser.custom_data["documents"],
)
self.write_data(
"Attribute",
self.parser.attributes,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"SheetName",
"RowName",
"Value",
"Unit",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"Description",
"AllowedValues",
],
"ririiirreeeoo",
)
self.write_data(
"Coordinate",
self.parser.coordinates,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Category",
"SheetName",
"RowName",
"CoordinateXAxis",
"CoordinateYAxis",
"CoordinateZAxis",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
"ClockwiseRotation",
"ElevationalRotation",
"YawRotation",
],
"ririiooooeeeooo",
)
self.write_data(
"Issue",
self.parser.issues,
"Name",
[
"Name",
"CreatedBy",
"CreatedOn",
"Type",
"Risk",
"Chance",
"Impact",
"SheetName1",
"RowName1",
"SheetName2",
"RowName2",
"Description",
"Owner",
"Mitigation",
"ExtSystem",
"ExtObject",
"ExtIdentifier",
],
"ririooooooooooeee",
)
def write_data(self, sheet, data, primary_key, fieldnames, colours, custom_data={}):
self.sheet_data[sheet] = {"headers": fieldnames + list(custom_data.keys()), "colours": colours, "rows": []}
for name, row in data.items():
row[primary_key] = name
values = []
for fieldname in fieldnames:
values.append(row[fieldname])
for fieldname in custom_data.keys():
values.append(row[fieldname])
self.sheet_data[sheet]["rows"].append(values)
class CobieCsvWriter(CobieWriter):
def write(self):
import csv
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 CobieXlsWriter(CobieWriter):
def write(self):
from xlsxwriter import Workbook
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:
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 CobieOdsWriter(CobieWriter):
def write(self):
from odf.opendocument import OpenDocumentSpreadsheet
from odf.style import Style, TableCellProperties
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:
self.write_table(sheet)
self.doc.save(self.filename, True)
def write_table(self, name):
from odf.table import Table, TableRow, TableCell
from odf.text import P
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)
tc.addElement(P(text=col))
tr.addElement(tc)
c += 1
table.addElement(tr)
self.doc.spreadsheet.addElement(table)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Converts COBie IFC MVD into its spreadsheet equivalent")
parser.add_argument("input", type=str, help="Specify an IFC file to process")
parser.add_argument("output", type=str, help="The output directory for CSV or filename for other formats")
parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log")
parser.add_argument(
"-f", "--format", type=str, help="Choose which format to export in (csv/ods/xlsx)", default="csv"
)
parser.add_argument(
"-c", "--components", type=str, help="A custom selector for components. Defaults to COBie", default=".COBie"
)
parser.add_argument(
"-t", "--types", type=str, help="A custom selector for types. Defaults to COBieType", default=".COBieType"
)
parser.add_argument(
"-d",
"--data",
type=str,
help="JSON file containing custom data to be appended to the COBie spreadsheet template",
default="",
)
args = vars(parser.parse_args())
print("Processing IFC file ...")
start = time.time()
logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG)
logger = logging.getLogger("IFCtoCOBie")
logger.info("Starting conversion")
selector = ifcopenshell.util.selector.Selector()
parser = IfcCobieParser(logger, selector)
if args["data"]:
with open(bpy.context.scene.BIMProperties.cobie_json_file, "r") as f:
custom_data = json.load(f)
else:
custom_data = {}
parser.parse(args["input"], args["types"], args["components"], custom_data)
print("Generating reports ...")
if args["format"] == "xlsx":
writer = CobieXlsWriter(parser, args["output"])
elif args["format"] == "ods":
writer = CobieOdsWriter(parser, args["output"])
else:
writer = CobieCsvWriter(parser, args["output"])
writer.write()
logger.info("Finished conversion in %ss", time.time() - start)
print("# All reports are complete :-)")