Ejemplo de script para convertir un informe CIPM en BCF/anotaciones

Este artículo describe cómo crear un script de Python que permite transformar un informe CIPM extraído de la herramienta Progress Monitoring en:

  • archivos BCF para utilizarlos con un modelo de Revit o con cualquier otra herramienta
  • Anotaciones con formato dentro de un proyecto de Cintoo, vinculadas a elementos problemáticos del modelo según el parámetro Coverage


Permisos: los usuarios con roles que incluyan el permiso Administrar anotaciones pueden crear anotaciones con este script.


En el archivo adjunto se proporciona un ejemplo de script. También se adjuntan todos los scripts complementarios.


TABLA DE CONTENIDO


Requisitos previos

Instale Python siguiendo las instrucciones de un chatbot de IA de su elección. Pídale instrucciones para ejecutar scripts de Python desde la línea de comandos si no hay un entorno específico instalado.


En este script de Python se utilizan las siguientes bibliotecas para ejecutar el flujo de trabajo.
Instálelas antes de usar el script:

  • png - ejecute el siguiente comando para conectar la biblioteca
pip install pypng
  • requests - ejecute el siguiente comando para conectar la biblioteca
pip install requests


Información de entrada

Ejecute el script createBCF.py en la línea de comandos. Todos los demás scripts son complementarios.

python createBCF.py

Una vez ejecutado, el script solicitará al usuario la siguiente información:

  • Insert the path to the CIPM .csv report - ruta completa del informe CIPM en formato CSV, con la extensión
    Ejemplo: C:\Projects\ProgressMonitoring\Walls_0.0500(Meters).csv


  • Insert the path where BCF files should be created - ruta completa donde se crearán y almacenarán localmente los archivos BCF
    Ejemplo: C:\Projects\ProgressMonitoring\BCF


  • Insert the minimum coverage - límite definido por el usuario para tomar todos los valores de Coverage estrictamente inferiores a él, en porcentaje.
    Ejemplo: para crear BCF/anotaciones para los elementos del modelo con menos de un 25 % de cobertura de escaneo, introduzca el valor 25

  • Insert the path to the workzone where to create annotations - dirección URL completa de la zona de trabajo en el proyecto de Cintoo.

    Nota: se recomienda encarecidamente crear una zona de trabajo específica para las anotaciones creadas con este script, ya que así se tendrá más control sobre el resultado y será más fácil limpiarlo en caso necesario.

    Ejemplo: https://aec.cintoo.com/accounts/0a000a00-000a-000a-a0000aa0a00000a0/
    projects/a0a0000a-aaa0000a-000a-aaaa00aaa000/workzones/aaaaaaaaaa0aaaaaaaa/data

    Copie esta URL directamente desde el navegador.

Autorización

Una vez introducidos todos los parámetros, se solicita al usuario que autorice el uso de una cuenta de Cintoo en una página emergente del navegador. Haga clic en Allow para continuar.

Una vez autorizado, aparecerá el siguiente mensaje que indica que la autenticación se ha realizado correctamente. Esta pestaña del navegador se puede cerrar sin problema.


Cintoo-tokens.json - archivo json de autenticación creado por el script para establecer la conexión. Se recomienda eliminarlo cada vez que el usuario termine el trabajo.


Nota: el tiempo necesario para generar los archivos BCF y las anotaciones depende del valor del nivel de cobertura y de la cantidad de elementos del modelo.


Limitaciones

Advertencia: tenga en cuenta que, al usar las API, el script puede crear un máximo de 3000 anotaciones.


Resultados

  • Archivos BCF guardados localmente en la ruta definida por el usuario, que pueden utilizarse en Revit (por ejemplo) para detectar elementos problemáticos del modelo y corregirlos

  • Anotaciones de Cintoo en el proyecto, vinculadas visualmente a los elementos del modelo, con todos los datos necesarios en la descripción (editable).


Explicación del script CreateBCF.py


A continuación se muestra el script con comentarios sobre cada parte.


Advertencia: se pueden realizar ajustes en el código si es necesario, pero Cintoo no asume ninguna responsabilidad sobre el funcionamiento de este código una vez modificado.


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)

Importación de todas las bibliotecas necesarias y de los scripts complementarios para respaldar el script principal. Creación de un png de plantilla, necesario para crear un archivo BCF.



def create_bcfzip(input_csv,
                  output_dir,
                  min_coverage,
                  tenant=None,
                  account_id=None,
                  project_id=None,
                  workzone_id=None,
                  camera_state=None,
                  headers=None):
    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)
        elements_to_be_created = [row for row in reader if float(row['Coverage by Scan Data (%)']) < min_coverage]
        if len(elements_to_be_created) > 1000:
            print("More than 1000 elements have a lower score than the minimum coverage score. Only the top 1000 will be created.")
            elements_to_be_created = sorted(
                elements_to_be_created,
                key=lambda x: float(x['Coverage by Scan Data (%)']),
                reverse=True
            )[0:999]
        for row in elements_to_be_created:
            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
                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)
                if all([tenant, account_id, project_id, workzone_id, camera_state]):
                    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)

Función que crea los archivos BCF relacionados con cada elemento que se toma en función del parámetro Coverage definido por el usuario.

Analiza todos los datos necesarios a partir de los parámetros proporcionados por el usuario, como el directorio de salida, la cobertura mínima, el tenant, etc., y de los GUID existentes en las líneas del informe CIPM para extraerlos según la comparación de cobertura.


Advertencia: no se recomienda cambiar la función create_bcfzip, salvo que sea necesario modificar el funcionamiento de la condición que elige los datos (por ejemplo, coverage<min_coverage). Para cambiar el texto, la descripción o el etiquetado de la anotación, consulte la función create_issue_xml.



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'))

    # Limpieza
    os.remove(viewpoint_filename)
    os.remove(issue_filename)
    os.remove(project_filename)
    os.remove(version_filename)
    os.remove(snapshot_filename)
    os.rmdir(bcf_dir)

Creación de archivos BCF individuales.



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>"""

Configuración de la posición predeterminada de la cámara.



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>
    """

Creación de archivos XML con los metadatos de los problemas.

Introduzca la información necesaria sobre el autor en los campos:

  • CreationAuthor - línea 154
  • ModifiedAuthor - línea 156
  • AssignedTo - línea 158
  • Author - línea 163
  • ModifiedAuthor - línea 167



def create_project_xml(projectGuid):
    return f"""<?xml version="1.0" encoding="UTF-8"?>
<ProjectExtension>
	<Project ProjectId="{projectGuid}" />
	<ExtensionSchema></ExtensionSchema>
</ProjectExtension>"""

Creación de un archivo que hace referencia a un proyecto.



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>
    """

Descripción de la estructura de BCF (categorías).



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>"""

Definición de la versión de BCF (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']

    # Autenticar y obtener el token de acceso
    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()

Definición de la parte principal del script, que solicita al usuario los datos de entrada, obtiene el token de acceso y se autentica con la cuenta de Cintoo según Autenticación de Cintoo API, la posición predeterminada de la cámara, que cambia una vez importada en Cintoo, crea los archivos BCFzip de salida y ejecuta la función principal.


Explicación del script CreateAnnotation.py

A continuación se muestra el script con comentarios sobre cada parte.


Advertencia: se pueden realizar ajustes en el código si es necesario, pero Cintoo no asume ninguna responsabilidad sobre el funcionamiento de este código una vez modificado.



import login
import requests
from cintooUtils import parse_cintoo_workzone_context_url

TIMEOUT = 10

Importación de todas las bibliotecas necesarias y de los datos de los scripts complementarios para respaldar el script principal. Definición de un parámetro de tiempo de espera.



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]

Extracción del ID de la zona de trabajo a partir de la entrada del usuario.



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()

Análisis de la información de ubicación de Cintoo a partir de la entrada del usuario, asociando cada anotación con el elemento del modelo correspondiente.



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']

    # Autenticar y obtener el token de acceso
    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)

Función principal con el proceso de autenticación, la configuración de la posición de la cámara y la creación de anotaciones en el proyecto de Cintoo.

¿Le fue útil este artículo?

¡Qué bueno!

Gracias por sus comentarios

¡Sentimos mucho no haber sido de ayuda!

Gracias por sus comentarios

¡Díganos cómo podemos mejorar este artículo!

Seleccione al menos una de las razones
La verificación de CAPTCHA es obligatoria.

Comentarios enviados

Agradecemos su iniciativa, e intentaremos corregir el artículo