This article describes how to create a Python script that allows to transform the CIPM report extracted from Progress Monitoring tool into:
- BCF files to be used with a Revit model or any other tool
- formatted Annotations inside a Cintoo project tied to problematic model elements based on Coverage parameter
An example of a script is provided in the attachment. All supplementary scripts are attached as well.
TABLE OF CONTENTS
Prerequisites
Install Python using instructions from AI chatbot of your choice. Ask for an instruction to run Python scripts from command prompt if no dedicated environment is installed.
In this Python script the following libraries are used to run the workflow.
Install them before using the script:
- png - run the following command to connect the library
pip install pypng
- requests - run the following command to connect the library
pip install requests
Input information
Execute the script createBCF.py. All other scripts are supplementary ones.
Once executed the script will prompt the user to provide the following information:
- Insert the path to the CIPM .csv report - full path to the CIPM report in CSV format should be provided
Example: C:\Projects\ProgressMonitoring\Walls_0.0500(Meters).csv - Insert the path where BCF files should be created - full path to create and store BCF files locally
Example: C:\Projects\ProgressMonitoring\BCF - Insert the minimum coverage - user-defined limit to take all the Coverage values strictly below it in decimals.
Example: 0.1 - Insert the path to the workzone where to create annotations - full URL address to the work zone in Cintoo project.
Example: https://aec.cintoo.com/accounts/0a000a00-000a-000a-a0000aa0a00000a0/
projects/a0a0000a-aaa0000a-000a-aaaa00aaa000/workzones/aaaaaaaaaa0aaaaaaaa/data
Copy this URL directly from the browser.
Authorization
Once all the parameters are inserted the user would be prompted to authorize the usage of a Cintoo account in a popped-up browser page. Click Allow to proceed.
Once authorized the following message will appear signalizing the successful authentication. This browser tab could safely be closed.
Note: cintoo-tokens.json - authentication json created by the script to establish the connection. Recommended to be deleted every time a user finishes the job.
Results
- BCF files saved locally at the user-defined path, that could be used in Revit (for example) to detect problematic model elements and correct them
- Cintoo Annotations in the project tied visually to model elements with all the necessary data in the description (editable).
CreateBCF.py Script Explained
Below please find the script with comments on each part.
Warning: if any adjustments to the code are needed to be made it's possible but Cintoo doesn't take any responsibility on the functionality of this code once modified.
import csv import os import zipfile import png from createAnnotation import createAnnotation import login from cintooUtils import parse_cintoo_workzone_context_url import datetime import uuid def create_png(path): width = 255 height = 255 img = [] for y in range(height): row = () for x in range(width): row = row + (x, max(0, 255 - x - y), y) img.append(row) with open(path, 'wb') as f: w = png.Writer(width, height, greyscale=False) w.write(f, img)
Importing all the necessary libraries and supplementary scripts to support the main script. Creating a template png that is necessary to create a BCF file.
def create_bcfzip(input_csv, output_dir, min_coverage, tenant, account_id, project_id, workzone_id, camera_state, headers): if not os.path.exists(output_dir): os.makedirs(output_dir) report_name = input_csv.split("\\")[-1] with open(input_csv, 'r', encoding='utf-8') as file: reader = csv.DictReader(file) for row in reader: if (row.get('IfcGUID') or row.get('GlobalId')) and row.get('Coverage by Scan Data (%)'): print(row) try: coverage = float(row['Coverage by Scan Data (%)']) print(coverage) except ValueError: continue if coverage < min_coverage: guid = row.get('IfcGUID') or row.get('GlobalId') element_name = row.get('Model Element Name', 'Unknown') element_id = list(row.values())[0] print(element_id) create_bcf_file(guid, element_name, output_dir, coverage, report_name) createAnnotation(tenant, account_id, project_id, workzone_id, f'Low Coverage Issue: {element_name}', f"The element {element_name} has a coverage score of {coverage}, according to the report {report_name}", {"x": 0, "y": 0, "z": 0}, camera_state, headers, modelElementId=element_id, labels=[f'{report_name}-{datetime.date}'])
Function that creates BCF files related to each element that is taken based on the Coverage parameter defined by the user.
It parses all the necessary data from the user-provided parameters like output directory, minimum coverage, tenant etc. and existing GUIDs in the CIPM report lines to extract them based on coverage comparison.
Warning: It’s not recommended to change the create_bcfzip function, only if there is a need to change the way the condition to choose the data works (e.g. coverage<min_coverage). For the changes of the annotation text, description, labeling - please refer to create_issue_xml function.
def create_bcf_file(guid, element_name, output_dir, coverage, report_name):
bcf_dir = os.path.join(output_dir, guid)
os.makedirs(bcf_dir, exist_ok=True)
viewpoint_guid = str(uuid.uuid4())
comment_guid = str(uuid.uuid4())
topic_guid = str(uuid.uuid4())
project_guid = str(uuid.uuid4())
viewpoint_filename = os.path.join(bcf_dir, 'viewpoint.bcfv')
viewpoint_xml = create_viewpoint_xml(guid, viewpoint_guid)
with open(viewpoint_filename, 'w', encoding='utf-8') as f:
f.write(viewpoint_xml)
issue_filename = os.path.join(bcf_dir, 'markup.bcf')
issue_xml = create_issue_xml(element_name, coverage, report_name, viewpoint_guid, comment_guid, topic_guid)
with open(issue_filename, 'w', encoding='utf-8') as f:
f.write(issue_xml)
project_filename = os.path.join(output_dir, 'project.bcfp')
project_xml = create_project_xml(project_guid)
with open(project_filename, 'w', encoding='utf-8') as f:
f.write(project_xml)
version_filename = os.path.join(output_dir, 'bcf.version')
with open(version_filename, 'w', encoding='utf-8') as f:
f.write(create_bcf_version())
snapshot_filename = os.path.join(output_dir, 'snapshot.png')
create_png(snapshot_filename)
zip_filename = os.path.join(output_dir, f'{guid}.bcf')
with zipfile.ZipFile(zip_filename, 'w') as zf:
zf.write(viewpoint_filename, os.path.join(topic_guid, 'viewpoint.bcfv'))
zf.write(issue_filename, os.path.join(topic_guid, 'markup.bcf'))
zf.write(snapshot_filename, os.path.join(topic_guid, 'snapshot.png'))
zf.write(project_filename, os.path.join('project.bcfp'))
zf.write(version_filename, os.path.join('bcf.version'))
# Cleanup
os.remove(viewpoint_filename)
os.remove(issue_filename)
os.remove(project_filename)
os.remove(version_filename)
os.remove(snapshot_filename)
os.rmdir(bcf_dir)
Creating individual BCF files.
def create_viewpoint_xml(guid, viewpoint_guid): return f"""<?xml version="1.0" encoding="UTF-8"?> <VisualizationInfo Guid="{viewpoint_guid}"> <Components> <ViewSetupHints SpacesVisible="false" SpaceBoundariesVisible="false" OpeningsVisible="false" /> <Selection> <Component IfcGuid="{guid}" /> </Selection> <Visibility DefaultVisibility="true" /> </Components> <OrthogonalCamera> <CameraViewPoint> <X>129.93820633961636</X> <Y>-124.61204504554462</Y> <Z>104.47418360973809</Z> </CameraViewPoint> <CameraDirection> <X>-0.58963662529065941</X> <Y>0.5647967039409042</Y> <Z>-0.57735026918962584</Z> </CameraDirection> <CameraUpVector> <X>-0.41693605617897656</X> <Y>0.39937157934842415</Y> <Z>0.81649658092772603</Z> </CameraUpVector> <ViewToWorldScale>44.256083397559856</ViewToWorldScale> </OrthogonalCamera> </VisualizationInfo>"""
Setting up the default camera position.
def create_issue_xml(element_name, coverage, report_name, viewpoint_guid, comment_guid, topic_guid): return f""" <?xml version="1.0" encoding="UTF-8"?> <Markup> <Topic Guid="{topic_guid}" TopicType="Issue" TopicStatus="Active"> <Title>Low Coverage Issue: {element_name}</Title> <Priority>Normal</Priority> <Index>1</Index> <CreationDate>2025-03-05T09:22:02+00:00</CreationDate> <CreationAuthor>youremail@domain.com</CreationAuthor> <ModifiedDate>2025-03-05T09:22:03+00:00</ModifiedDate> <ModifiedAuthor>youremail@domain.com</ModifiedAuthor> <DueDate>2025-03-05T09:00:00+00:00</DueDate> <AssignedTo>youremail@domain.com</AssignedTo> <Description>The element {element_name} has a coverage score of {coverage}, according to the report {report_name}</Description> </Topic> <Comment Guid="{comment_guid}"> <Date>2025-03-04T09:53:03+00:00</Date> <Author>youremail@domain.com</Author> <Comment>The element {element_name} has a coverage score of {coverage}, according to the report {report_name}</Comment> <Viewpoint Guid="{viewpoint_guid}" /> <ModifiedDate>2025-03-04T09:53:03+00:00</ModifiedDate> <ModifiedAuthor>youremail@domain.com</ModifiedAuthor> </Comment> <Viewpoints Guid="{viewpoint_guid}"> <Viewpoint>viewpoint.bcfv</Viewpoint> <Snapshot>snapshot.png</Snapshot> </Viewpoints> </Markup> """
Creating XMLs for metadata for the problems.
Input the needed information about the author in the fields:
- CreationAuthor - line 154
- ModifiedAuthor - line 156
- AssignedTo - line 158
- Author - line 163
- ModifiedAuthor - line 167
def create_project_xml(projectGuid): return f"""<?xml version="1.0" encoding="UTF-8"?> <ProjectExtension> <Project ProjectId="{projectGuid}" /> <ExtensionSchema></ExtensionSchema> </ProjectExtension>"""
Creating a file referencing a project.
def create_extensions_xsd(): return """<?xml version="1.0" encoding="UTF-8" standalone="no" ?> <schema> <redefine schemaLocation="markup.xsd"> <simpleType name="Priority"> <restriction base="Priority"> <enumeration value="Aucune"/> <enumeration value="Faible"/> <enumeration value="Moyenne"/> <enumeration value="Haute"/> <enumeration value="Critique"/> </restriction> </simpleType> <simpleType name="TopicStatus"> <restriction base="TopicStatus"> <enumeration value="Assignée"/> <enumeration value="En cours"/> <enumeration value="Terminée"/> <enumeration value="Bloquée"/> <enumeration value="Validée"/> <enumeration value="Archivée"/> </restriction> </simpleType> <simpleType name="TopicLabel"> <restriction base="TopicLabel"/> </simpleType> </redefine> </schema> """
Describing the BCF structure (categories).
def create_bcf_version(): return """<?xml version="1.0" encoding="UTF-8"?> <Version VersionId="2.1" xsi:noNamespaceSchemaLocation="version.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <DetailedVersion>2.1 KUBUS BV</DetailedVersion> </Version>"""
Defining the BCF version (2.1).
def main():
input_csv = input("Insert the path to the CIPM .csv report :")
output_dir = input("Insert the path where BCF files should be created :")
min_coverage = float(input("Insert the minimum coverage"))
context_url = input("Insert the path to the workzone where to create annotations (e.g: "
"https://aec.cintoo.com/accounts/0a000a00-000a-000a-a000-0aa0a00000a0/projects/a0a0000a-aaa0"
"-000a-000a-aaaa00aaa000/workzones/aaaaaaaaaa0aaaaaaaa/data ")
context_details = parse_cintoo_workzone_context_url(context_url)
tenant, account_id, project_id, workzone_id = context_details['tenant'], context_details['accountId'], \
context_details['projectId'], context_details['workzoneId']
# Authenticate and get access token
login.load_tokens()
token = login.get_token(tenant)
headers = {"Authorization": f"Bearer {token}"}
camera_state = {"type": "legacy",
"camerastate": {
"position": [0, 0, 1],
"rotationQuaternion": [0, 0, 0, 1],
"fov": 1.5708,
"ortho": False,
"overviewScale": 10,
"camType": "scan"},
}
create_bcfzip(input_csv,
output_dir,
min_coverage,
tenant,
account_id,
project_id,
workzone_id,
camera_state,
headers)
main()
Definition of the main part of the script prompting the input questions to the user, getting the access token and authenticating with Cintoo account according to Cintoo API Authentication, default camera position that is changed once imported into Cintoo, creating output BCFzip files and executing the main function.
CreateAnnotation.py Script Explained
Below please find the script with comments on each part.
Warning: if any adjustments to the code are needed to be made it's possible but Cintoo doesn't take any responsibility on the functionality of this code once modified.
import login
import requests
from cintooUtils import parse_cintoo_workzone_context_url
TIMEOUT = 10
Importing all the necessary libraries and data from supplementary scripts to support the main script. Setting a timeout parameter.
def getWorkzoneGuid(tenant, account_id, project_id, workzone_idv1, headers):
url = f"{tenant}/api/2/accounts/{account_id}/projects/{project_id}/workzones"
response = requests.get(url, headers=headers, timeout=TIMEOUT)
response.raise_for_status()
return [wz for wz in response.json() if wz["api1Id"] == workzone_idv1]
Extracting work zone ID from user input.
def createAnnotation(tenant,
account_id,
project_id,
workzone_id,
title,
description,
position,
saved_view,
headers,
modelElementId=None,):
"""Creates a project in the specified account."""
workzone_guid = getWorkzoneGuid(tenant, account_id, project_id, workzone_id, headers)[0]['id'].split(':')[-1]
url = f"{tenant}/api/2/accounts/{account_id}/projects/{project_id}/annotations"
print(f"modelElementId : {modelElementId}")
if modelElementId:
body = {
'workzoneId': workzone_guid,
'annotationType': "Note",
'title': title,
'description': description,
'position': position,
'normal': {"x": 0, "y": 0, "z": 1},
'savedView': {**saved_view, 'modelElementId': modelElementId},
}
else:
body = {
'workzoneId': workzone_guid,
'annotationType': "Note",
'title': title,
'description': description,
'position': position,
'normal': {"x": 0, "y": 0, "z": 1},
'savedView': saved_view,
'elementId': "Toto"
}
print(url)
print(body)
response = requests.post(url, headers=headers, json=body, timeout=TIMEOUT)
print(response.text)
response.raise_for_status()
return response.json()
Parsing Cintoo location information from user input, attaching the annotation to a corresponding model element.
def main(context_url):
context_details = parse_cintoo_workzone_context_url(context_url)
tenant, account_id, project_id, workzone_id = context_details['tenant'], context_details['accountId'], \
context_details['projectId'], context_details['workzoneId']
# Authenticate and get access token
login.load_tokens()
token = login.get_token(tenant)
headers = {"Authorization": f"Bearer {token}"}
camera_state = {"type": "legacy",
"camerastate": {
"position": [0, 0, 1],
"rotationQuaternion": [0, 0, 0, 1],
"fov": 1.5708,
"ortho": False,
"overviewScale": 10,
"camType": "scan"}}
print(workzone_id)
createAnnotation(tenant, account_id, project_id, workzone_id, "Test API", "Created with API",
{"x": 0, "y": 0, "z": 0}, camera_state, headers)
Main function with authentication process, setting up the camera position and creating annotations in the Cintoo project.
Was this article helpful?
That’s Great!
Thank you for your feedback
Sorry! We couldn't be helpful
Thank you for your feedback
Feedback sent
We appreciate your effort and will try to fix the article