Exemple de script de rapport CIPM vers BCF/annotations

Cet article décrit comment créer un script Python qui permet de transformer un rapport CIPM extrait de l'outil de Suivi de progression en :

  • Fichiers BCF à utiliser avec un modèle Revit ou tout autre outil
  • Formaté Annotations à l'intérieur d'un projet Cintoo lié à des éléments de modèle problématiques basés sur le paramètre Couverture .


Autorisations : les utilisateurs ayant des rôles incluant l'autorisation Gérer les annotations peuvent créer des annotations avec ce script.


Un exemple de script est fourni en pièce jointe. Tous les scripts supplémentaires sont également joints.


TABLE DES MATIÈRES


Prérequis

Installez Python en suivant les instructions de l'outil de chatbot AI de votre choix. Demandez une instruction pour exécuter des scripts Python depuis l'invite de commande si aucun environnement dédié n'est installé.


Dans ce script Python, les bibliothèques suivantes sont utilisées pour exécuter le flux de travail. 
Installez-les avant d'utiliser le script :

  • png - exécutez la commande suivante pour connecter la bibliothèque
pip install pypng
  • requests exécutez la commande suivante pour connecter la bibliothèque
pip install requests


Informations d'entrée

Exécutez le script createBCF.py dans l'invite de commande. Tous les autres scripts sont des suppléments.

python createBCF.py

Une fois exécuté, le script invite l'utilisateur à fournir les informations suivantes :

  • Insérez le chemin vers le rapport CIPM .csv - chemin complet vers le rapport CIPM au format CSV avec extension
    Exemple : C:\Projects\ProgressMonitoring\Walls_0.0500(Meters).csv


  • Insérez le chemin où les fichiers BCF doivent être créés - chemin complet pour créer et stocker localement les fichiers BCF
    Exemple : C:\Projects\ProgressMonitoring\BCF


  • Insérez la couverture minimale - limite définie par l'utilisateur pour prendre toutes les valeurs de Couverture strictement en dessous en pourcentage.
    Exemple : pour créer des BCF/Annotations pour les éléments de modèle avec moins de 25 % de couverture de balayage, insérez la valeur 25

  • Insérez le chemin vers la zone de travail où créer les annotations - adresse URL complète vers la zone de travail dans le projet Cintoo.

    Remarque : il est fortement recommandé de créer une zone de travail dédiée pour les annotations créées par ce script, car cela permettrait un meilleur contrôle des résultats et un nettoyage plus facile si nécessaire.

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

    Copiez cette URL directement depuis le navigateur.

Autorisation

Une fois tous les paramètres insérés, l'utilisateur est invité à autoriser l'utilisation d'un compte Cintoo dans une page du navigateur qui s'est ouverte. Cliquez sur Autoriser pour continuer.
 

Une fois l'autorisation accordée, le message suivant apparaîtra pour signaler une authentification réussie. Cette onglet de navigateur pourrait être fermé en toute sécurité.


Cintoo-tokens.json - authentification JSON créé par le script pour établir la connexion. Recommandé à supprimer chaque fois qu'un utilisateur termine le travail.


Remarque : le temps passé à générer des BCF et des annotations dépend de la valeur du niveau de couverture et du nombre d'éléments de modèle.


Limitations

Avertissement : sachez que le script peut créer un maximum de 3000 annotations via les API.


Résultats

  • Les fichiers BCF enregistrés localement à un chemin défini par l'utilisateur peuvent être utilisés dans Revit (par exemple) pour détecter et corriger les éléments de modèle problématiques

  • Cintoo Annotations  dans le projet visuellement lié aux éléments de modèle avec toutes les données nécessaires dans la description (modifiable).


CreateBCF.py Script Explained


Vous trouverez ci-dessous le script avec des commentaires sur chaque partie.


Avertissement : si des ajustements du code doivent être effectués, c'est possible, mais Cintoo n'assume aucune responsabilité sur la fonctionnalité de ce code une fois modifié.



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)

Importation de toutes les bibliothèques nécessaires et des scripts supplémentaires pour prendre en charge le script principal. Création d'un modèle png nécessaire pour créer un fichier 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("Plus de 1000 éléments ont une note inférieure au score de couverture minimal. Seuls les 1000 premiers seront créés.")
            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'Problème de faible couverture : {element_name}',
                                     f"L'élément {element_name} a un score de couverture de {coverage}, selon le rapport {report_name}",
                                     {"x": 0, "y": 0, "z": 0},
                                     camera_state,
                                     headers,
                                     modelElementId=element_id)

Fonction qui crée des fichiers BCF liés à chaque élément pris en fonction du paramètre Couverture défini par l'utilisateur.

Il analyse toutes les données nécessaires à partir des paramètres fournis par l'utilisateur, comme le répertoire de sortie, la couverture minimale, le locataire, etc., et les GUID existants dans les lignes de rapport CIPM pour les extraire en fonction de la comparaison des couvertures.


Avertissement : il n'est pas recommandé de changer la fonction create_bcfzip, sauf s'il est nécessaire de modifier la façon dont la condition pour choisir les données fonctionne (par exemple, couverture<min_couverture). Pour les changements de texte d'annotation, de description, d'étiquetage - veuillez vous référer à la fonction 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'))

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

Création de fichiers BCF individuels.



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

Mise en place de la position par défaut de la caméra.



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>Problème de faible couverture : {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>L'élément {element_name} a un score de couverture de {coverage}, selon le rapport {report_name}</Description>
	</Topic>
	<Comment Guid="{comment_guid}">
		<Date>2025-03-04T09:53:03+00:00</Date>
		<Author>youremail@domain.com</Author>
		<Comment>L'élément {element_name} a un score de couverture de {coverage}, selon le rapport {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>
    """

Création de fichiers XML pour les métadonnées des problèmes.

Entrez les informations nécessaires sur l'auteur dans les champs :

  • CreationAuthor - ligne 154
  • ModifiedAuthor - ligne 156
  • AssignedTo - ligne 158
  • Auteur - ligne 163
  • ModifiedAuthor - ligne 167



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

Création d'un fichier référenciant un projet.



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

Description de la structure BCF (catégories).



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

Définition de la version BCF (2.1).



def main():
    input_csv = input("Insérez le chemin vers le rapport CIPM en .csv :")
    output_dir = input("Insérez le chemin où les fichiers BCF doivent être créés :")
    min_coverage = float(input("Insérez la couverture minimale"))
    context_url = input("Insérez le chemin vers la zone de travail où créer des annotations (par ex : "
                        "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']

    # Authentification et obtention du jeton d'accès
    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()

Définition de la partie principale du script posant les questions d'entrée à l'utilisateur, obtenant le jeton d'accès et authentifiant avec le compte Cintoo selon l'authentification API de Cintoo, la position par défaut de la caméra qui est modifiée une fois importée dans Cintoo, créant les fichiers BCFzip de sortie et exécutant la fonction principale.


CreateAnnotation.py Script Explained

Vous trouverez ci-dessous le script avec des commentaires sur chaque partie. 


Attention : si des ajustements au code doivent être effectués, c'est possible mais Cintoo n'assume aucune responsabilité quant à la fonctionnalité de ce code une fois modifié.



import login
import requests
from cintooUtils import parse_cintoo_workzone_context_url

TIMEOUT = 10

Importation de toutes les bibliothèques nécessaires et des données à partir de scripts supplémentaires pour soutenir le script principal. Définition d'un paramètre de délai d'attente.



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]

Extraction de l'ID de la zone de travail à partir de l'entrée utilisateur.



def createAnnotation(tenant,
                     account_id,
                     project_id,
                     workzone_id,
                     title,
                     description,
                     position,
                     saved_view,
                     headers,
                     modelElementId=None,):
    """Crée un projet dans le compte spécifié."""
    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()

Analyse des informations de localisation Cintoo à partir de l'entrée utilisateur, en attachant chaque annotation à un élément de modèle correspondant.



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

    # Authentification et obtention du jeton d'accès
    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", "Créé avec l'API",
                     {"x": 0, "y": 0, "z": 0}, camera_state, headers)

Fonction principale avec processus d'authentification, configuration de la position de la caméra et création d'annotations dans le projet Cintoo.

Cet article a-t-il été utile ?

C'est super !

Merci pour votre commentaire

Désolé ! Nous n'avons pas pu vous être utile

Merci pour votre commentaire

Dites-nous comment nous pouvons améliorer cet article !

Sélectionner au moins l'une des raisons
La vérification CAPTCHA est requise.

Commentaires envoyés

Nous apprécions vos efforts et nous allons corriger l'article