CIPM report to BCF/annotations script example

Modified on Tue, 3 Jun at 10:58 AM

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

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article