diff --git a/NEOBANBOU.py b/NEOBANBOU.py index 9e32bf2..fa53419 100644 --- a/NEOBANBOU.py +++ b/NEOBANBOU.py @@ -1,1175 +1,1253 @@ -## Version en un seul fichier du script banbou ## - - -import subprocess - - -# Vérifier s'il existe une version plus récente de pip -try: - result = subprocess.run(['pip', 'install', '--upgrade', 'pip', '--disable-pip-version-check'], capture_output=True, text=True) - if result.returncode == 0: - print("pip a été mis à jour avec succès.") -except subprocess.CalledProcessError: - print("La mise à jour de pip a échoué.") - -bibliotheques = ['os', 'getpass', 're', 'shutil', 'datetime', 'openpyxl'] - -for bibliotheque in bibliotheques: - try: - __import__(bibliotheque) - except ImportError: - # Installer la bibliothèque - print(f"{bibliotheque} n'est pas installée. Installation en cours...") - subprocess.check_call(['pip', 'install', bibliotheque]) - print(f"{bibliotheque} a été installée avec succès.") - - - - -import os, getpass -import re # module RegEx -import shutil, datetime -import openpyxl as xls - - - - - - -### CONSTANTES ### - - -# chemin absolue en dur du modèle -prenom = getpass.getuser() -MODELE = f"C:\\Users\\{prenom}\\Desktop\\Banbou\\VISA_BANBOU.xlsx" - - -# nom du dossier créé pour l'opérateur -TRAVAIL = "Travail" - - -# séparateur d'un CSV -SEP = ";" - - -# regex, motifs à chercher dans une chaine. Explication : -# (MOTIF)+ matche 1 OU PLUS occurence du mot MOTIF -# (?:) le groupe ne sera pas capturé, du coup dans python le groupe ne créé pas de chaine vide si pas de match du groupe en question -# | OR -EXPRESSION = r"(?:fiche)+|(?:relev)+|(?:topo)+|(?:ouvr)+|(?:\.doc)+|(?:\.dwg)+|(?:\.csv)+|(?:\.odt)+|(?:\.pdf)+|(?:\.doc)+" - - -# Masques Binaires représentants les controles de chaques catégorie traitées -# les bits sont allumés à chaque position où un test existe -FAIL_CSV = 7 # Explication : il existe pour le moment 3 tests pour les CSVs donc : "0000 0000 0000 0111" -FAIL_DWG = 16 # un seul test pour les DWGs : "0000 0000 0001 0000" -FAIL_PDF = 256 # un seul test pour les PDFs : "0000 0001 0000 0000" -FAIL_INFO = 4096 # un seul test pour les fiches infos : "0001 0000 0000 0000" - - -# Valeurs constantes définie par IGN -PROJECTIONS = [ - { - "nom" : "GPS", - "E0" : 0, - "N0" : 0, - "offsetE0" : 180, - "offsetN0" : 90, - "EPSG" : 4326 # Correspond à la projection des GPS - }, - { - "nom" : "Lambert93", - "E0" : 700000, - "N0" : 6600000, - "offsetE0" : 600000, - "offsetN0" : 600000, - "EPSG" : 2154 # Attention la plus recente est 9794 - }, - { - "nom" : "CC42", - "E0" : 1700000, - "N0" : 1200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3942 - }, - { - "nom" : "CC43", - "E0" : 1700000, - "N0" : 2200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3943 - }, - { - "nom" : "CC44", - "E0" : 1700000, - "N0" : 3200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3944 - }, - { - "nom" : "CC45", - "E0" : 1700000, - "N0" : 4200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3945 - }, - { - "nom" : "CC46", - "E0" : 1700000, - "N0" : 5200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3946 - }, - { - "nom" : "CC47", - "E0" : 1700000, - "N0" : 6200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3947 - }, - { - "nom" : "CC48", - "E0" : 1700000, - "N0" : 7200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3948 - }, - { - "nom" : "CC49", - "E0" : 1700000, - "N0" : 8200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3949 - }, - { - "nom" : "CC50", - "E0" : 1700000, - "N0" : 9200000, - "offsetE0" : 600000, - "offsetN0" : 111000, - "EPSG" : 3950 - } - # TODO : ajouter les projections DOMTOMs -] - - - - - - - -### CLASSES ET FONCTIONS - - -class _Fichier: - - - def __init__(self, - #nom_original="Pas de nom original", - chemin="Pas de chemin", - extension="Pas d'extension", - nom= "Pas de nom", - implication="Non-conforme", - categorie="Pas de catégorie", - taille=0): - #self.nom_original = nom_original # - son nom original - self.chemin = chemin # - son chemin absolue (dossier+fichier+extension) - self.extension = extension # - son extension (ecrit en minuscule) - self.nom = nom # - son nom formaté - self.implication = implication # - son implication dans le projet {"Necessaire", "Non-conforme", "A-ignorer"} - self.categorie = categorie # - sa categorie {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"} - self.taille = taille # taille en octets - - - def afficher(self): - """ - Affiche dans la sortie standard les éléments du fichier - """ - print("\nfichier.afficher()") - print("nom :".ljust(16) + self.nom) - #print("nom orig :".ljust(16) + self.nom_original) - print("ext :".ljust(16) + self.extension) - print("chemin :".ljust(16) + self.chemin) - print("implication :".ljust(16) + self.implication) - print("categorie :".ljust(16) + self.categorie) - print("taille :".ljust(16) + str(self.taille) ) - - -def lire(dossier, fichier): - """ - Lit le nom du fichier et du dossier - - Construit un élement _Fichier ET met à jour TOUS ses attributs - :param user_input: chemin absolue du dossier, nom du fichier - :return: un element _Fichier - """ - print("\nfichier.lire()") - # initialiser un _Fichier - ce_Fichier = _Fichier() - - # son chemin absolue - ce_Fichier.chemin = dossier + "\\" + fichier - - # déterminer son nom original et son extension - # vérifie qu'il y est au moins un point dans le nom - if "." in fichier : - - *nom_original, ce_Fichier.extension = fichier.rsplit(".", maxsplit=1) - # reconstitue une chaine simple depuis la liste - nom_original = "".join(nom_original) - - # lettrer l'extension en minuscule - ce_Fichier.extension = ce_Fichier.extension.casefold() - - # formatter et écrire le nom - ce_Fichier.nom = formater(nom_original) - # déterminer son implication - ce_Fichier.implication, ce_Fichier.categorie = impliquer(fichier) - # calculer sa taille - ce_Fichier.taille = os.path.getsize(ce_Fichier.chemin) - print(f"implication : {ce_Fichier.implication} {ce_Fichier.categorie}") - else: - print(f'"{fichier}" est un fichier sans extension --> Non-conforme.') - ce_Fichier.nom = fichier - ce_Fichier.implication = "Non-conforme" - #TODO: voir si il manque pas des données à lire ici pour pas bloquer le reste du prog - - return ce_Fichier - - - - - -class _Notification: - def __init__(self, - categorie="Pas de catégorie", - texte="Pas de texte"): - self.categorie = categorie # vu comme une énumération de l'ensemble {"CSV", "DWG", "PDF", "FRONT"} seule ces valeurs sont donc possible et traité par le script - self.texte = texte - - - - - - - - - -def controler(id_point, point_x, point_y): - """ - regarde si les coordonnées d'un point sont bien dans une des projections - autorisées - - Contrainte : - - Pour le moment, ne peut reconnaitre que des projections - de points situés en France métropolitaine (Lambert93 et les 9 zones CC). - - Ce sont toutes des projections coniques depuis le pole nord --> la longitude (axe Ouest Est) n'est pas déformé par ces projections et donc reste valide. - Mais je vais aussi contraindre la longitude à la France métropolitaine, pour pas qu'un point situé en Russie par exemple apparaisse comme valide. - NOTE : On ne controle pas l'élevation (coord Z), ces projections ne prennent pas en compte l'élevation. - - param user_input: un point de type (int, float, float) TODO: avoir si conversion nécessaire ou faire en chaine de carac - return: Une chaine nommant la projection trouvé sinon "Mauvaise projection" - """ - - #print("\nprojection.controler()") - - # projection à retourner - projection = "Mauvaise projection" - - # NOTE : Une partie des ensembles de coordonnées de CC47 et de Lambert93 s'intersectionne : - # Lorsqu'un point de coordonnées (X, Y) se situe dans la plage 1100000, 1300000 pour X et - # la plage 3089000, 3311000 pour Y, alors ce point est à la fois valide en CC47 et en Lambert93 - # Avant de déterminer la projection, je vais donc vérifier si le point à controler n'est pas dans cette plage - # Et notifier si necessaire - # TODO : exporter la notif. En fait non, pas d'export juste un avertissement dans le shell - if 6089000 < point_y < 6311000: - if 1100000 < point_x < 1300000: - print(f"AVERTISSEMENT : ID {ID_POINT} : SES COORDONNÉES PEUVENT ÊTRE INTERPRÉTER CORRECTEMENT COMME DU CC47 ET DU LAMBERT93.") - print("DANS LA PLUPART DES CAS, VOUS DEVREZ PROJETER LE FICHIER CSV EN CC47.") - - - # définie la projection conique Nord en regardant dans quelle intervalle la valeur se situe - for P in PROJECTIONS: - borne_basse = P["N0"] - P["offsetN0"] - borne_haute = P["N0"] + P["offsetN0"] - if borne_basse < point_y < borne_haute : - projection = P["nom"] - - #print(f"Proj. conique Nord trouvé : {projection}") - print(f" --> {projection:10}") - - # Controle la longitude - # TODO: refaire cette partie avec des variables et non des entiers literraux - longitude_correcte = False - match projection: - case "Lambert93": - if 100000 < point_x < 1300000 : - longitude_correcte = True - case "CC42" | "CC43" | "CC44" | "CC45" | "CC46" | "CC47" | "CC48" | "CC49" | "CC50": - if 1100000 < point_x < 2300000 : - longitude_correcte = True - case "GPS": - if -180 < point_x < 180: - longitude_correcte = True - case _: - pass - - if not longitude_correcte : - projection = "Mauvaise projection" - print(f"AVERTISSEMENT : LONGITUDE DU POINT ID {ID_POINT} PAS EN MÉTROPOLE.") - - return projection - - - - - - - - -## Analyse lexicale - - - -def tokeniser(ligne): #Pas utilisé, je le laisse au cas ou. - """ - Tokenise une ligne de texte. - - Lit en entrée une "ligne" d'un fichier texte, découpe la ligne en mots, - chaque mot étant séparé par le séparateur "sep". - Attendu : si un champ est vide alors la liste reçoit quand même un - item pour representer ce champ (une chaine vide "" qui peut servir - pour tester sa présence) - Retourne une liste des mots trouvés, ou une liste vide si aucun mot. - """ - sortie = [] - - # enleve les espaces superflues avant et après la chaine - tempo = ligne.strip() - tempo = tempo.split(sep=SEP) - - #affichage test - print("tokeniser") - print(tempo) - - # enlever les mots vides - for i in tempo: - print - if i not in '': - sortie.append(i) - - return sortie - - - - -## Conversion, Formatage - - -def formater(chaine): - """Formate selon nomenclature. - - Formate la chaine de caractères passée en paramètre : - Enlève tous les accents français. Enlève la cédille du C. - Remplace les espaces ' ', les traits d'union '-' et les ''' apostrophes - par des tirets bas '_'. - Ne traite pas pour le moment le AE et OE ligaturé. - Si plusieurs '_' se suivent, les réduire à un seul. - """ - #TODO : gérer les accents sur majuscules - #TODO : amélioration ou exercice, utiliser la méthode str.translate() et maketrans - resultat = "" - precedent = None - for c in chaine: - match c: - case "à" | "â" | "ä": - resultat+= "a" - case "é" | "è" | "ê" | "ë": - resultat+= "e" - case "î" | "ï": - resultat+= "i" - case "ô" | "ö": - resultat+= "o" - case "ù" | "û" | "ü": - resultat+= "u" - case "ÿ": - resultat+= "y" - case "ç": - resultat+= "c" - case " " | "-" | "_" | "'": - if('_' not in precedent ): - resultat+= "_" - c = "_" - case _: - resultat+= c - precedent = c - return resultat - - - - - - -## Analyse semantique - - -def impliquer(chaine): - """ - Définir l'implication d'une chaine (attendu : un nom de fichier avec son extension). - - Retourne un tuple de str (Necessité, Catégorie). - - La necessité peut prendre une des valeurs suivantes : - {"Necessaire", "A-ignorer}. - La catégorie peut prendre une des valeurs suivantes: - {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}. - Il est attendu que lorsqu'un fichier obtient une valeur de catégorie - Alors il est aussi considéré "Necessaire". - - Les fichiers "Necessaire" seront les fichiers copiés dans le - répertoire "Travail". - - La catégorie est définie par une recherche de motif dans le nom du fichier. - # TODO: voir si elle peut pas être définie en lisant les métadonnées des fichiers - - Les fichiers nécessaires sont les DWGs, les CSVs pour les datas. - Pour les shémas et relevés de topo : - - PDFs, DOCs, ODTs (Doc LibreOffice), - """ - - print(f"\nimpliquer({chaine})") - - # mettre la chaine en minuscule - chaine = chaine.casefold() - - # cherche la PREMIERE correspondance du motif (alors que findall() cherche toutes les correspodances) - motif = re.search(EXPRESSION, chaine) - - if motif: - print("Il y a un match") - motif = motif.group() - else: - print("Pas de match") - - match motif : - # On va traiter les mots clés d'abord - case "fiche" | "relev" | "topo" | "ouvr" | ".doc" | ".odt" : - return ("Necessaire", "TOPO") - case ".dwg" : - return ("Necessaire", "DWG") - case ".csv" : - return ("Necessaire", "CSV") - case ".pdf" : - return ("Necessaire", "SHEMAS") - case _ : - return ("A-ignorer","Non-definie") - - - - - - - - - -class _Projet: - - - def __init__(self, - nom="Pas de nom", - date="Pas de date", - racine="Pas de chemin", - fichiers=[], - nb_fichiers=0, - taille=0, - rapport="Pas de fichier", - nb_shemas=0, - nb_releves=0, - nb_csvs=0, - points=[], - nb_points=0, - nb_dwgs=0, - notifs=[], - controles=0): - self.nom = nom # nom du projet - self.date = date # date du traitement - self.racine = racine # chemin racine du projet - self.fichiers = fichiers # liste de _Fichier - self.nb_fichiers = nb_fichiers # nb de fichiers dans "Travail" - self.taille = taille # taille des fichiers dans "Travail" - self.rapport = rapport # chemin vers le visa - self.nb_shemas = nb_shemas # nb de plans de la mise en place - self.nb_releves = nb_releves # nb de rapports de relevés topo - self.nb_csvs = nb_csvs # nb de fichiers CSV - self.points = points # liste des points du CSV - self.nb_points = nb_points # nb de points des CSVs TODO: REDONDANT avec len(points) - self.nb_dwgs = nb_dwgs # nb de fichiers DWG - self.notifs = notifs # liste contenant les notifs du VISA - self.controles = controles # variable drapeau contenant toutes les validations de controles - # (voir Document pour plus d'info sur sa représentation) - - def notifier(self, categorie, texte): - """ - Ajoute une notification au _Projet - """ - print("\n_Projet.notifier()") - print(f"Incident ajouté :{texte} ") - incident = _Notification( categorie, texte) - self.notifs.append(incident) - - - def lister_fichiers(self, dossier): - """ - Construit une liste avec les fichiers de 'dossier' - - - Parcours le dossier et ses sous-dossiers, ajoute TOUS les fichiers - dans une liste de _Fichier. - - Chaque _Fichier ajouté a ses attributs mis a jour. - - Les nombres des différentes catégories de fichiers nécessaires - au projet sont mis a jour. - :param user_input: nom complet du dossier - :return: liste d'élements de type _Fichier - """ - print("\n_Projet.lister_fichiers()") - self.fichiers = [] - - print("Création liste de _Fichier...") - - for dossier_courant, list_sousdossiers, list_fichiers in os.walk(dossier): - for fichier_courant in list_fichiers: - ce_Fichier = lire(dossier_courant, fichier_courant) - self.fichiers.append(ce_Fichier) - print(f'"{fichier_courant}" ajouté.') - - # mettre à jour les nbs des catégories de fichiers necessaires - if ce_Fichier.implication in "Necessaire": - ### sa categorie {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"} - match ce_Fichier.categorie : - case "CSV": - self.nb_csvs += 1 - case "DWG": - self.nb_dwgs += 1 - case "SHEMAS": - self.nb_shemas += 1 - case "TOPO": - self.nb_releves += 1 - case _: - pass - - print("Fin création liste.") - - -# TODO surcharger la fonction print native pour cet affichage -def afficher_liste(liste): - """ - Affiche le nom des fichiers de la liste des _Fichiers - """ - print("\nprojet.afficher_liste()") - for courant in liste: - print("\n" + courant.nom + "." + courant.extension) - - -def enraciner(projet): - """ - récupère le repertoire de travail (working directory) courant - met à jour l'attribut 'racine' d'un projet - """ - print("\nprojet.enraciner()") - projet.racine = os.getcwd() - print("Racine : ".ljust(16), f"{projet.racine}") - - -def calculer_taille(projet): - """ - calcule la taille des fichiers necessaires d'un liste - d'élements _Fichier. - Met à jour l'attribut 'taille' dans le projet - """ - print("\nprojet.calculer_taille()") - taille = 0.0 - for courant in projet.fichiers: - if courant.implication in "Necessaire": - taille += courant.taille - projet.taille = taille - # Affichage adapté à la bonne unité - if taille < 1024 : - unite = "octets" - elif taille < 1024**2 : - unite = "Ko" - taille /= 1024 - else : - unite = "Mo" - taille /= 1024**2 - print(f"Taille totale : {taille:.2f} {unite}.") - - -def dater(projet): - """ - recupère la date du jour - met à jour l'attribut 'date' du projet - """ - print("\nprojet.dater()") - projet.date = datetime.datetime.today().strftime('%Y%m%d') - print(f"Date : {projet.date}") - - -def nommer(projet): - """ - Met à jour l'attribut 'nom' en composant un nom. - - Le nom est constitué du NOM du dossier racine ET de la DATE - courante formatté. - - """ - print("\nprojet.nommer()") - projet.nom = f"{os.path.basename(os.getcwd())}_{projet.date}" - projet.nom = formater(projet.nom) - print(f'Nom : "{projet.nom}"') - - -def preparer_dossier_travail(projet): - """ - Créer un dossier "Travail" dans la racine du working directory et - le peuple des fichiers nécessaires - - Préfixe et suffixe les fichiers le nécessitant (le CSV et le DWG) - """ - print("\nprojet.preparer_dossier_travail()") - #travail = "Travail" - - # création du dossier "Travail" et de ses sous-dossiers " - _chemin = f"{projet.racine}\\{TRAVAIL}" - #nomemclaturer les noms des sousdossiers - sousdossier = f"{_chemin}\\{projet.nom}" - - # pour le moment tous les sous dossiers sont - dossierCSVs = _chemin - dossierDWGs = _chemin - dossierPDFs = _chemin - dossierTOPOs = _chemin - - print(f'Chemin du dossier de préparation : "{_chemin}"') - try: - os.mkdir(_chemin) - print (f'Dossier "{TRAVAIL}" créé.') - os.mkdir(sousdossier) # dossier pour le projet ARCGIS - - if projet.nb_csvs > 1: - dossierCSVs = f"{sousdossier}_CSV" - os.mkdir(dossierCSV) - print (f'Dossier "CSV" créé.') - if projet.nb_dwgs > 1: - dossierDWGs = f"{sousdossier}_DWG" - os.mkdir(dossierDWGs) - print (f'Dossier "DWG" créé.') - if projet.nb_shemas > 1: - dossierPDFs = f"{sousdossier}_PDF" - os.mkdir(dossierPDFs) - print (f'Dossier "PDF" créé.') - if projet.nb_releves > 1: - dossierTOPOs = f"{sousdossier}_TOPO" - os.mkdir(dossierTOPOs) - print (f'Dossier "TOPO" créé.') - - except FileExistsError as erreur: - print(f'AVERTISSEMENT: LE DOSSIER "{TRAVAIL}" EXISTE DÉJA. SUPPRIMER LE, PUIS RELANCER LE SCRIPT SVP.') - purger_et_finir_programme(projet) - except OSError as erreur: - print(f"FICHIER NON TROUVÉ. SUREMENT UN PB DE CHEMIN EN AMONT.") - purger_et_finir_programme(projet) - - - - # peuplement du dossier Travail avec les fichiers necessaires - print("Copie des fichiers nécessaires et nomenclature de leurs noms...") - for fichier in projet.fichiers: - if fichier.implication in "Necessaire": - source = fichier.chemin - print(f"source : {source}") - - # fabrication du nom du fichier de destination - dest = _chemin + "\\" - - # lors du peuplement préfixer et suffixer les noms des fichiers concernés comme suit : "Plan_nomfichierdwg.dwg" et "Point_nomfichiercsv_IN.csv" - # Confirmation par Audrey qu'il n'y a nécessairement qu'un fichier CSV par projet --> Le script s'occupe de le renommer. - # Mais parfois il peut y avoir plusieurs DWGs dont un seul est utile --> PASS -- Je laisse l'opérateur choisir lequel utiliser et le renommer manuellement. - - #{"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"} - match fichier.categorie : - case "CSV": - dest = f"{dossierCSVs}\\Point_{fichier.nom}_IN" - case "DWG": - dest = f"{dossierDWGs}\\Plan_{fichier.nom}" - case "SHEMAS": - dest = f"{dossierPDFs}\\{fichier.nom}" - case "TOPO": - dest = f"{dossierTOPOs}\\{fichier.nom}" - case _: - pass # TODO: voir pour les autres cas, normalement il n'y en a pas pour le moment - - dest = dest + f".{fichier.extension}" - print(f"dest : {dest}") - - if fichier.implication in "Necessaire": - try: - shutil.copyfile( source , dest) - print("" + fichier.nom.ljust(40,".") + "copié") - except shutil.SameFileError as err : - print(f"Le fichier existe déjà.") - #TODO : - # Confirmation par Audrey qu'il n'y a necessairement qu'un fichier CSV par projet --> On peut le renommer. - # Mais parfois il peut y avoir plusieurs DWGs dont un seul est utile --> PASS -- Je laisse l'opérateur choisir lequel utiliser et le renommer manuellement. EDIT : Nathalie prefere que tous les fichier DWG soient préfixé avec Plan_ - #TODO : verifier aussi la longueur des noms de fichiers. Notifier si nécessaire - - -def controler_longueur_noms(projet): - """ - controle la longueur des noms des fichiers du projet. - - Notifie si necessaire les noms trop longs - """ - #print("\nprojet.controler_longueur_noms()") - for fichier in projet.fichiers: - match fichier.extension: - case "dwg": - if len(fichier.nom) > 25 : # Plan_ + 25 carac --> 30 carac - projet.notifier("DWG", f'"Plan_{fichier.nom}.dwg" : Nom trop long.') - case "csv" : - if len(fichier.nom) > 35 : # Point_ + 35 carac + _OUT --> 45 carac - projet.notifier("CSV", f'"Point_{fichier.nom}_IN.csv" : Nom trop long.') - case "pdf" : - if len(fichier.nom) > 45 : - projet.notifier("PDF", f'"{fichier.nom}.pdf" : Nom trop long.') - case _: - if len(fichier.nom) > 45 : - projet.notifier("FRONT", f'"{fichier.nom}.{fichier.extension}" : Nom trop long.') - -def formater_vers_ArcGIS(projet, fichier_entree, fichier_sortie): - """ - Lit et formatte un fichier CSV pour son importation dans ArcGIS. - - Lit la 1ère ligne et s'assure de la présence des titres de colonnes, - formatte cette 1ère ligne. - Supprime les lignes vides. - Change le séparateur décimal ',' en '.' (necessaire car sinon je ne peux convertir les strings en float) - Notifie si nécessaire les points de controles qui ne passent pas. - Met à jour l'attribut nb_points - Contraintes : - - Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements. (Normalement c'est toujours le cas de toutes façon) - - Le fichier doit avoir exactement 5 colonnes. TODO : vois si plus de souplesse avec le catchage de paramètres restants - param user_input: nom complet d'un fichier csv - NOTE: Ne pas formatter le fichier original, mais celui qui est deja copié dans le dossier Travail - """ - print("\nprojet.formatter_vers_ArcGIS()") - - # titres des colonnes correctement formattés. - titres = "id_point;TYPE;X;Y;Z\n" - - sortie = [titres] # le fichier de sortie ( representé comme une liste de lignes ) contient un premiere ligne de titre - - - df = open(fichier_entree, "r") - ligne = df.readline() - - # analyser la premiere ligne et formatter cette ligne - # Si le premier mot est un nombre Alors il manque les titres, dans - # ce cas insérer une ligne. - mots = ligne.split(sep=";", maxsplit=1) - print(f"Mots[0] = {mots[0]}") - - - if mots[0].isnumeric(): - #insérer ligne - sortie.append(ligne) - projet.nb_points += 1 - - # la tête de lecture du descripteur de fichier ne reset pas sa position - # donc on peut continuer le parcours des lignes directement - for ligne_courante in df : - - # lit et controle la ligne courante - courant = lire_et_controler_ligne(projet, ligne_courante) - # ajoute la ligne - if courant not in "": - sortie.append(f"{courant}\n") - # compter ce point - projet.nb_points += 1 - - df.close() - - # je reouvre le descripteur en mode w only pour ecrire le fichier - df = open(fichier_sortie, "w") - # écriture - for ligne in sortie: - #print(f"{ligne}") - df.write(ligne) # NOTE: write() - - df.close() - - print("Fin formatage du CSV.") - - -def lire_et_controler_ligne(projet, ligne): - """ - Analyse une chaine de caractère (concrètement, une ligne du CSV)/ - - remplace les séparateurs décimaux ',' par '.' - Notifie (dans le projet lié) les points de controles qui ne passent pas. - Met à jour l'attribut nb_points du projet lié - Met à jour la variable de controles du projet lié - Contraintes : - - Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements. - :return: une chaine de carac formatté pour ArcGIS - """ - #print("\nprojet.lire_et_controler_ligne()") - #print(f'ENTREE : "{ligne}"') - # enlever les espaces - ligne = ligne.replace(" ", "") - ligne = ligne.replace(",", ".") - ligne = ligne.replace("\n", "") - # si la ligne n'est pas vides alors - # TODO : changer le match en if ligne not in ["", ";;;;"]: - match ligne: - case ";;;;;;" | ";;;;;" | ";;;;" | ";;;" | ";;" | ";" | "" : - ligne = "" - case _: - # controler la projection de ce point - # NOTE: split() tokenise la ligne en items (des strings), si un champ est manquant, alors il tokenise une chaine vide( "" ) - # On peut donc tester cette chaine vide pour savoir si une champ est manquant.abs - # Je vais dont controler la présence du champ ID, car il apparait que parfois il est manquant. Si tel est le cas, - # Notifier et terminer le programme --> dossier FAIL - # - # NOTE: split() n'enleve pas le \n de la fin de ligne dans la chaine --> ligne = ligne.replace("\n", "") que j'ai rajouter au dessus - try : - id_point, type_, point_x, point_y, point_z, *autres = ligne.split(sep=";") - except ValueError: - print("LE FICHIER SOURCE CSV EST MAL FORMATÉ, IL DOIT MANQUER DES CHAMPS ET DE SÉPARATEURS DE CHAMPS.") - purger_et_finir_programme(projet) - - - #PRINT de controle - print(f'ID:"{id_point}", TYPE:"{type_}", X:"{point_x}", Y:"{point_y}", Z:"{point_z}", *autres={autres}') - - # controler qu'il n'y est pas un champ vide pour id_point, TYPE, X et Y. (Z étant optionnel) - # Si tel est le cas, on quitte FAIL - if id_point in "": - print('AVERTISSEMENT : COLONNE "ID_POINT" MANQUANTE.') - purger_et_finir_programme(projet) - if type_ in "": - print('AVERTISSEMENT : COLONNE "TYPE" MANQUANTE.') - purger_et_finir_programme(projet) - if point_x in "": - print('AVERTISSEMENT : COLONNE "X" MANQUANTE.') - purger_et_finir_programme(projet) - if point_y in "": - print('AVERTISSEMENT : COLONNE "Y" MANQUANTE.') - purger_et_finir_programme(projet) - if point_z in "": - print('AVERTISSEMENT : COLONNE "Z" MANQUANTE.') - purger_et_finir_programme(projet) - - # controler que id_point est un entier - # Si c'est un flottant alors ya un pb, on quitte FAIL - try : - if float(id_point)/int(id_point) != 1.0 : - print("AVERTISSEMENT : LA PREMIÈRE COLONNE EST UN FLOTTANT. ATTENDU UN ID_POINT DE VALEUR ENTIÈRE.") - print('IL DOIT MANQUER LA COLONNE "ID_POINT AINSI QUE LE SÉPARATEUR DE CHAMP ";".') - purger_et_finir_programme(projet) - except ValueError : - print("AVERTISSEMENT : LA COLONNE ID_POINT N'A PAS UN NOMBRE.") - print(f'ATTENDU UN ENTIER. TROUVÉ ID_POINT DE TYPE "{type(id_point)}"') - purger_et_finir_programme(projet) - - - # A partir d'ici il est attendu que la tokenisation se soit bien déroulée - try : - projo = controler(id_point, float(point_x), float(point_y)) - except ValueError : - print("AVERTISSEMENT : UNE COLLONNE (X OU Y) EST MAL CONVERTIE.") - print(f'ATTENDU VALEURS DÉCIMALES. TROUVÉ X : DE TYPE "{type(point_x)}", Y DE TYPE "{type(point_y)}"') - purger_et_finir_programme(projet) - # notifier si pas bonne projection - if projo in "Mauvaise projection": - incident = _Notification("CSV", f"Point ID {id_point} : Mauvaise projection") - projet.notifs.append(incident) - projet.controles |= 2**2 - - #print(f'SORTIE : "{ligne}"') - - return ligne - - - - - - - - - -def remplir(modele, projet): - """ - Complète le Visa du projet à partir d'un fichier modele. - - Controle le nb de fichiers de chaque catégorie et notifie. - Parcours la liste de notifications du projet et ajoute chaque - notification dans la feuille concerné du classeur - A défaut, une notification de la liste qui n'a pas de catégorie - avec sa propre feuille sera ajoutée à la première feuille (FRONT) - - Enregistre le Visa dans le repertoire de Travail - Contraintes : - - le modele du VISA doit être NÉCESSAIREMENT dans le même - dossier que le script. TODO : traité l'exception si fichier non trouvé - - le modele du VISA NE doit PAS avoir eu de modification notable - dans son design ( emplacements des cellules utilisées pour - notifier, nom des feuilles, etc) - """ - - print("\nvisa.remplir()") - - - - # creation d'un workbook à partir d'un modele - classeur = xls.load_workbook(modele) - - - ## definition personnalisé vers chaque feuille du classeur - FRONT = classeur[classeur.sheetnames[0]] - PDF = classeur[classeur.sheetnames[1]] - CSV = classeur[classeur.sheetnames[2]] - DWG = classeur[classeur.sheetnames[3]] - - - # Nom dossier - # Date de reception - # Date Visa - FRONT['C1'].value = f'Analyse : {projet.nom}\nDate de réception :\nDate visa : {projet.date}' - - # Taille du dossier - taille = projet.taille - # Affichage adapté à la bonne unité - if taille < 1024 : - unite = "octets" - elif taille < 1024**2 : - unite = "Ko" - taille /= 1024 - else : - unite = "Mo" - taille /= 1024**2 - - - FRONT['C10'].value = f"Taille totale : {taille:.2f} {unite}." - - - # TODO: factoriser les instructions ci dessous (pas sur...) - - - - ### FRONT DWG - # Je ne peux pas controler automatiquement la projection du DWG (c'est un binaire illisible hors de AutoCAD) - # J'ajoute donc automatiquement la mention comme dans le script précedent. - FRONT['C3'].value = f"{projet.nb_dwgs} fichier(s) DWG présent(s).\nProjection de tous les fichiers DWG : RFG93-CC43 (EPSG:3943)." - - # Controle le nb de fichiers DWG - if projet.nb_dwgs == 0: - projet.controles |= 2**4 # allume le bit 5ième bit - FRONT['C3'].value = "Pas de fichier DWG présent." - - # Regarde si des fails ont été détectés sur les DWGs - fail = projet.controles & FAIL_DWG # operation logique avec le masque - # notifie en consequence - if fail : - FRONT['B3'].value = "FAIL" - else : - FRONT['B3'].value = "OK" - - - - ### FRONT PDF - FRONT['C4'].value = f"{projet.nb_shemas} fichier(s) PDF présent(s)." - - # Controle le nb de fichiers PDF - if projet.nb_shemas == 0: - projet.controles |= 2**8 # allume le 9ième bit - FRONT['C4'] = "Pas de fichier PDF présent." - - # Regarde si des fails ont été détectés sur les PDFs - fail = projet.controles & FAIL_PDF - # notifie en consequence - if fail : - FRONT['B4'].value = "FAIL" - else : - FRONT['B4'].value = "OK" - - - - ### FRONT CSV - FRONT['C5'].value = f"1 fichier(s) CSV présent(s).\n{projet.nb_points} point(s) en RFG93-CC43 (EPSG:3943)." - - # Controle le nb de fichiers CSV - if projet.nb_csvs == 0: - projet.controles |= 2**0 # allume le 1er bit - FRONT['C5'].value = "Pas de fichier CSV présent." - - # Regarde si des fails ont été détectés sur les CSVs - fail = projet.controles & FAIL_CSV - # notifie en consequence - if fail : - FRONT['B5'].value = "FAIL" - else : - FRONT['B5'].value = "OK" - - - - # FRONT FICHE INFO TOPOLOGIE - FRONT['C6'].value = f"{projet.nb_releves} fiche(s) Info. présente(s)." - - # Controle le nb de fichiers Fiche Topologique - if projet.nb_releves == 0: - projet.controles |= 2**12 # allume le 13ième bit - FRONT['C6'].value = "Pas de fiche d'Info. Topologie présente." - - # Regarde si des fails ont été détectés sur les Fiches Topo - fail = projet.controles & FAIL_INFO - # notifie en consequence - if fail : - FRONT['B6'].value = "FAIL" - else : - FRONT['B6'].value = "OK" - - - - - # Notifier dans les autres Feuilles - - # lignes en cours à remplir pour chaque feuille - # initialisé aux lignes où on va commencer à notifier - A = 2 # "B2" # Pour la feuille PDF - B = 2 # "B2" # Pour la feuille CSV - C = 2 # "B2" # Pour la feuille DWG - D = 12 # "C12" # Pour la feuille FRONT - - cell = "" # cellule courante qui va être notifier - - for notif in projet.notifs : - match notif.categorie : - case "PDF" : - cell = PDF["B"+str(A)] - A += 1 - case "CSV" : - cell = CSV["B"+str(B)] - B += 1 - case "DWG" : - cell = DWG["B"+ str(C)] - C += 1 - case _: - cell = FRONT["C"+str(D)] - D += 1 - - cell.value = notif.texte - - - # Ici le VISA doit être correctement rempli - # on sauvegarde dans un fichier le classeur - classeur.save(f"Travail\\{projet.nom}_VISA.xlsx") - - -def purger_et_finir_programme(projet): - """ - Efface le dossier "Travail" si il existe puis termine le programme. - - Cette fonction est utilisée quand des expections sont levées et - qu'elles nécessitent la fermeture prématurée du programme. - """ - print("\npurger_et_finir_programme()") - print("\nFIN DE PROGRAMME.\n") - # Ne fonctionne pas car des descripteurs de fichiers sont utilisés au moment de la demande - ## shutil.rmtree(f"{projet.racine}\\{TRAVAIL}" , ignore_errors=True) - quit() - - - - - - - - - - - -## NOTE : Pour traiter le cas des sous-dossiers lors du dézippage -# il vaut mieux que l'opérateur copie le fichier banbou.py dans chaque -# sous-dossiers puis l'execute. -## Donc : -# - Il est attendu que le fichier script se situe dans le dossier, au -# même niveau que les fichiers originaux. -# - Il est attendu que le modele du visa soit dans un autre dossier -# BIEN SPÉCIFIÉ (je vais remettre un chemin absolue "Desktop\Banbou" car suivant l'opérateur, le dézippage crée des sous-sous-dossiers) -# De plus, pour simplifier le dossier Travail se créera, dans le dossier -# ou se situe le script, aux coté des fichiers originaux -##TODO: Automatiser le cas des sous dossiers, avec un FOR each dossier FAIRE -## Et créer des dossiers Travail01, Travail02, etc - - - -# Création entité projet et mise à jour de ses attributs -ce_Projet = _Projet() - -enraciner(ce_Projet) - -dater(ce_Projet) - -nommer(ce_Projet) -# controle de la longueur du nom du projet -if len(ce_Projet.nom) >=46 : - print(f"\n{ce_Projet.nom} EST UN NOM DE DOSSIER TROP LONG (+ DE 46 CARACTÈRES). VEUILLEZ RACCOURCIR SON NOM.") - purger_et_finir_programme(ce_Projet) - -# explorer le dossier Trouvé et fabriquer la liste des fichiers nécessaires -ce_Projet.lister_fichiers(ce_Projet.racine) - -calculer_taille(ce_Projet) - -# Ici le projet doit avoir toutes les données nécessaires pour fabriquer -# et remplir les dossiers attendus -preparer_dossier_travail(ce_Projet) - - -# il faut UN SEUL fichier CSV NOTE: a voir si ce controle est obligatoire -if ce_Projet.nb_csvs > 1: - print("\nAVERTISSEMENT : IL Y A PLUSIEURS FICHIERS CSV.") - print("VEUILLEZ NE GARDER QU'UN SEUL FICHIER CSV DANS LE DOSSIER ORIGINAL.") - purger_et_finir_programme(ce_Projet) - -# On formatte le fichier CSV pour ArcGIS -for fichier in ce_Projet.fichiers: - if fichier.categorie in "CSV": - entree = ce_Projet.racine + "\\Travail\\Point_" + fichier.nom + "_IN" + ".csv" - sortie = ce_Projet.racine + "\\Travail\\Point_" + fichier.nom + "_IN" + ".csv" - formater_vers_ArcGIS(ce_Projet, entree, sortie) - - - -# On controle les noms des fichiers copiés -controler_longueur_noms(ce_Projet) - - -# On fabrique le VISA -#modele = ce_Projet.racine + "\\VISA_BANBOU.xlsx" -remplir(MODELE, ce_Projet) - - - - - - -#Fin de Programme Attendu +## Version en un seul fichier du script banbou ## + + +import subprocess + + +# Vérifier s'il existe une version plus récente de pip +try: + result = subprocess.run(['pip', 'install', '--upgrade', 'pip', '--disable-pip-version-check'], capture_output=True, text=True) + if result.returncode == 0: + print("pip a été mis à jour avec succès.") +except subprocess.CalledProcessError: + print("La mise à jour de pip a échoué.") + +bibliotheques = ['os', 'getpass', 're', 'shutil', 'datetime', 'openpyxl'] + +for bibliotheque in bibliotheques: + try: + __import__(bibliotheque) + except ImportError: + # Installer la bibliothèque + print(f"{bibliotheque} n'est pas installée. Installation en cours...") + subprocess.check_call(['pip', 'install', bibliotheque]) + print(f"{bibliotheque} a été installée avec succès.") + + + + +import os, getpass +import re # module RegEx +import shutil, datetime +import openpyxl as xls + + + + + + +### CONSTANTES ### + + +# chemin absolue en dur du modèle +prenom = getpass.getuser() +MODELE = f"C:\\Users\\{prenom}\\Desktop\\Banbou\\VISA_BANBOU.xlsx" + + +# nom du dossier créé pour l'opérateur +TRAVAIL = "Travail" + + +# séparateur d'un CSV +SEP = ";" + + +# regex, motifs à chercher dans une chaine. Explication : +# (MOTIF)+ matche 1 OU PLUS occurence du mot MOTIF +# (?:) le groupe ne sera pas capturé, du coup dans python le groupe ne créé pas de chaine vide si pas de match du groupe en question +# | OR +EXPRESSION = r"(?:fiche)+|(?:relev)+|(?:topo)+|(?:info)+|(?:ouvr)+|(?:\.doc)+|(?:\.dwg)+|(?:\.csv)+|(?:\.odt)+|(?:\.pdf)+|(?:\.doc)+" + + +# Masques Binaires représentants les controles de chaques catégorie traitées +# les bits sont allumés à chaque position où un test existe +FAIL_CSV = 7 # Explication : il existe pour le moment 3 tests pour les CSVs donc : "0000 0000 0000 0111" +FAIL_DWG = 16 # un seul test pour les DWGs : "0000 0000 0001 0000" +FAIL_PDF = 256 # un seul test pour les PDFs : "0000 0001 0000 0000" +FAIL_INFO = 4096 # un seul test pour les fiches infos : "0001 0000 0000 0000" + + +# Valeurs constantes définie par IGN +PROJECTIONS = [ + { + "nom" : "GPS", + "E0" : 0, + "N0" : 0, + "offsetE0" : 180, + "offsetN0" : 90, + "EPSG" : 4326 # Correspond à la projection des GPS + }, + { + "nom" : "Lambert93", + "E0" : 700000, + "N0" : 6600000, + "offsetE0" : 600000, + "offsetN0" : 600000, + "EPSG" : 2154 # Attention la plus recente est 9794 + }, + { + "nom" : "CC42", + "E0" : 1700000, + "N0" : 1200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3942 + }, + { + "nom" : "CC43", + "E0" : 1700000, + "N0" : 2200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3943 + }, + { + "nom" : "CC44", + "E0" : 1700000, + "N0" : 3200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3944 + }, + { + "nom" : "CC45", + "E0" : 1700000, + "N0" : 4200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3945 + }, + { + "nom" : "CC46", + "E0" : 1700000, + "N0" : 5200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3946 + }, + { + "nom" : "CC47", + "E0" : 1700000, + "N0" : 6200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3947 + }, + { + "nom" : "CC48", + "E0" : 1700000, + "N0" : 7200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3948 + }, + { + "nom" : "CC49", + "E0" : 1700000, + "N0" : 8200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3949 + }, + { + "nom" : "CC50", + "E0" : 1700000, + "N0" : 9200000, + "offsetE0" : 600000, + "offsetN0" : 111000, + "EPSG" : 3950 + } + # TODO : ajouter les projections DOMTOMs +] + + + + + + + +### CLASSES ET FONCTIONS + + +class _Fichier: + + + def __init__(self, + #nom_original="Pas de nom original", + chemin="Pas de chemin", + extension="Pas d'extension", + nom= "Pas de nom", + implication="Non-conforme", + categorie="Pas de catégorie", + taille=0): + #self.nom_original = nom_original # - son nom original + self.chemin = chemin # - son chemin absolue (dossier+fichier+extension) + self.extension = extension # - son extension (ecrit en minuscule) + self.nom = nom # - son nom formaté + self.implication = implication # - son implication dans le projet {"Necessaire", "Non-conforme", "A-ignorer"} + self.categorie = categorie # - sa categorie {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"} + self.taille = taille # taille en octets + + + def afficher(self): + """ + Affiche dans la sortie standard les éléments du fichier + """ + print("\nfichier.afficher()") + print("nom :".ljust(16) + self.nom) + #print("nom orig :".ljust(16) + self.nom_original) + print("ext :".ljust(16) + self.extension) + print("chemin :".ljust(16) + self.chemin) + print("implication :".ljust(16) + self.implication) + print("categorie :".ljust(16) + self.categorie) + print("taille :".ljust(16) + str(self.taille) ) + + +def lire(dossier, fichier): + """ + Lit le nom du fichier et du dossier + + Construit un élement _Fichier ET met à jour TOUS ses attributs + :param user_input: chemin absolue du dossier, nom du fichier + :return: un element _Fichier + """ + print("\nfichier.lire()") + # initialiser un _Fichier + ce_Fichier = _Fichier() + + # son chemin absolue + ce_Fichier.chemin = dossier + "\\" + fichier + + # déterminer son nom original et son extension + # vérifie qu'il y est au moins un point dans le nom + if "." in fichier : + + *nom_original, ce_Fichier.extension = fichier.rsplit(".", maxsplit=1) + # reconstitue une chaine simple depuis la liste + nom_original = "".join(nom_original) + + # lettrer l'extension en minuscule + ce_Fichier.extension = ce_Fichier.extension.casefold() + + # formatter et écrire le nom + ce_Fichier.nom = formater(nom_original) + # déterminer son implication et sa categorie + ce_Fichier.implication, ce_Fichier.categorie = impliquer(fichier) + # calculer sa taille + ce_Fichier.taille = os.path.getsize(ce_Fichier.chemin) + print(f"implication : {ce_Fichier.implication} {ce_Fichier.categorie}") + else: + print(f'"{fichier}" est un fichier sans extension --> Non-conforme.') + ce_Fichier.nom = fichier + ce_Fichier.implication = "Non-conforme" + #TODO: voir si il manque pas des données à lire ici pour pas bloquer le reste du prog + + return ce_Fichier + + + + + +class _Notification: + def __init__(self, + categorie="Pas de catégorie", + texte="Pas de texte"): + self.categorie = categorie # vu comme une énumération de l'ensemble {"CSV", "DWG", "PDF", "FRONT"} seule ces valeurs sont donc possibles et traitées par le script + self.texte = texte + + + + + + + + + +def controler(id_point, point_x, point_y): + """ + regarde si les coordonnées d'un point sont bien dans une des projections + autorisées + + Contrainte : + - Pour le moment, ne peut reconnaitre que des projections + de points situés en France métropolitaine (Lambert93 et les 9 zones CC). + - Ce sont toutes des projections coniques depuis le pole nord --> la longitude (axe Ouest Est) n'est pas déformé par ces projections et donc reste valide. + Mais je vais aussi contraindre la longitude à la France métropolitaine, pour pas qu'un point situé en Russie par exemple apparaisse comme valide. + NOTE : On ne controle pas l'élevation (coord Z), ces projections ne prennent pas en compte l'élevation. + + param user_input: un point de type (int, float, float) TODO: avoir si conversion nécessaire ou faire en chaine de carac + return: Une chaine nommant la projection trouvé sinon "Mauvaise projection" + """ + + #print("\nprojection.controler()") + + # projection à retourner + projection = "Mauvaise projection" + + # NOTE : Une partie des ensembles de coordonnées de CC47 et de Lambert93 s'intersectionne : + # Lorsqu'un point de coordonnées (X, Y) se situe dans la plage 1100000, 1300000 pour X et + # la plage 3089000, 3311000 pour Y, alors ce point est à la fois valide en CC47 et en Lambert93 + # Avant de déterminer la projection, je vais donc vérifier si le point à controler n'est pas dans cette plage + # Et avertir si necessaire + if 6089000 < point_y < 6311000: + if 1100000 < point_x < 1300000: + print(f"AVERTISSEMENT : ID {ID_POINT} : SES COORDONNÉES PEUVENT ÊTRE INTERPRÉTER CORRECTEMENT COMME DU CC47 ET DU LAMBERT93.") + print("DANS LA PLUPART DES CAS, VOUS DEVREZ PROJETER LE FICHIER CSV EN CC47.") + + + # définie la projection conique Nord en regardant dans quelle intervalle la valeur se situe + for P in PROJECTIONS: + borne_basse = P["N0"] - P["offsetN0"] + borne_haute = P["N0"] + P["offsetN0"] + if borne_basse < point_y < borne_haute : + projection = P["nom"] + + #print(f"Proj. conique Nord trouvé : {projection}") + print(f" --> {projection:10}") + + # Controle la longitude + longitude_correcte = False + match projection: + case "Lambert93": + if 100000 < point_x < 1300000 : + longitude_correcte = True + case "CC42" | "CC43" | "CC44" | "CC45" | "CC46" | "CC47" | "CC48" | "CC49" | "CC50": + if 1100000 < point_x < 2300000 : + longitude_correcte = True + case "GPS": + if -180 < point_x < 180: + longitude_correcte = True + case _: + pass + + if not longitude_correcte : + projection = "Mauvaise projection" + print(f"AVERTISSEMENT : LONGITUDE DU POINT ID {ID_POINT} PAS EN MÉTROPOLE.") + + return projection + + + + + +## Analyse lexicale + + +def tokeniser(ligne): #Pas utilisé, je le laisse au cas ou. + """ + Tokenise une ligne de texte. + + Lit en entrée une "ligne" d'un fichier texte, découpe la ligne en mots, + chaque mot étant séparé par le séparateur "sep". + Attendu : si un champ est vide alors la liste reçoit quand même un + item pour representer ce champ (une chaine vide "" qui peut servir + pour tester sa présence) + Retourne une liste des mots trouvés, ou une liste vide si aucun mot. + """ + sortie = [] + + # enleve les espaces superflues avant et après la chaine + tempo = ligne.strip() + tempo = tempo.split(sep=SEP) + + #affichage test + print("tokeniser") + print(tempo) + + # enlever les mots vides + for i in tempo: + print + if i not in '': + sortie.append(i) + + return sortie + + + + +## Conversion, Formatage + + +def formater(chaine): + """Formate selon nomenclature. + + Formate la chaine de caractères passée en paramètre : + Enlève tous les accents français. Enlève la cédille du C. + Remplace les espaces ' ', les traits d'union '-' et les ''' apostrophes + par des tirets bas '_'. + Ne traite pas pour le moment le AE et OE ligaturé. + Si plusieurs '_' se suivent, les réduire à un seul. + """ + #TODO : gérer les accents sur majuscules + #TODO : amélioration ou exercice, utiliser la méthode str.translate() et maketrans + resultat = "" + precedent = None + for c in chaine: + match c: + case "à" | "â" | "ä": + resultat+= "a" + case "é" | "è" | "ê" | "ë": + resultat+= "e" + case "î" | "ï": + resultat+= "i" + case "ô" | "ö": + resultat+= "o" + case "ù" | "û" | "ü": + resultat+= "u" + case "ÿ": + resultat+= "y" + case "ç": + resultat+= "c" + case " " | "-" | "_" | "'": + if('_' not in precedent ): + resultat+= "_" + c = "_" + case _: + resultat+= c + precedent = c + return resultat + + + + +## Analyse semantique + + +def impliquer(chaine): + """ + Définir l'implication d'une chaine (attendu : un nom de fichier avec son extension). + + Retourne un tuple de str (Necessité, Catégorie). + + La necessité peut prendre une des valeurs suivantes : + {"Necessaire", "A-ignorer}. + La catégorie peut prendre une des valeurs suivantes: + {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}. + Il est attendu que lorsqu'un fichier obtient une valeur de catégorie + Alors il est aussi considéré "Necessaire". + + Les fichiers "Necessaire" seront les fichiers copiés dans le + répertoire "Travail". + + La catégorie est définie par une recherche de motif dans le nom du fichier. + # TODO: voir si elle peut pas être définie en lisant les métadonnées des fichiers + + Les fichiers nécessaires sont les DWGs, les CSVs pour les datas. + Pour les shémas et relevés de topo : + - PDFs, DOCs, ODTs (Doc LibreOffice), + """ + + print(f"\nimpliquer({chaine})") + + # mettre la chaine en minuscule + chaine = chaine.casefold() + + # cherche la PREMIERE correspondance du motif (alors que findall() cherche toutes les correspondances) + motif = re.search(EXPRESSION, chaine) # re.search() retourne un objet Match si trouvé sinon None + + if motif: + # print("Il y a un match") + motif = motif.group() # Match.group() permet de retourner la valeur matché + else: + # print("Pas de match") + pass + + match motif : + # On va traiter les mots clés d'abord + case "fiche" | "relev" | "topo" | "ouvr" | "info" | ".doc" | ".odt" : + return ("Necessaire", "TOPO") + case ".dwg" : + return ("Necessaire", "DWG") + case ".csv" : + return ("Necessaire", "CSV") + case ".pdf" : + return ("Necessaire", "SHEMAS") + case _ : + return ("A-ignorer","Non-definie") + + + + +## Fonctions Input Utilisateur + +def saisir_annee_topo(): + """ + demande à l'utilisateur de saisir l'année du relevé topographique. + + Demande à l'utilisateur l'année affichée dans le relevé topograhique + du projet. + Lit la valeur saisi dans l'entrée standard. + + Les fichiers n'étant pas encore créés dans le dossier "Travail" à + ce moment du programme, l'utilisateur doit donc ouvrir la fiche + du relevé topographique dans le dossier original. + + Pour ne pas être bloquant (cas ou l'année n'est pas trouvable), + la saisi de '0' permet de ne rien mettre. + + Retourne la valeur saisi. + """ + print("\nVeuillez saisir l'année de la date du relevé topographique.") + print("Pour cela ouvrez la fiche topo du dossier/sous-dossier original.") + + correct = False + while(not correct): + saisi = input("Saisir 4 chiffres : ") + if saisi in "0": + return "" + + correct = saisi.isdecimal() and len(saisi) == 4 + + return saisi + + + + + + + + + +class _Projet: + + + def __init__(self, + nom="Pas de nom", + date="Pas de date", + racine="Pas de chemin", + fichiers=[], + nb_fichiers=0, + taille=0, + rapport="Pas de fichier", + nb_shemas=0, + nb_releves=0, + nb_csvs=0, + points=[], + nb_points=0, + nb_dwgs=0, + notifs=[], + controles=0): + self.nom = nom # nom du projet + self.date = date # date du traitement + self.racine = racine # chemin racine du projet + self.fichiers = fichiers # liste de _Fichier + self.nb_fichiers = nb_fichiers # nb de fichiers dans "Travail" + self.taille = taille # taille des fichiers dans "Travail" + self.rapport = rapport # chemin vers le visa + self.nb_shemas = nb_shemas # nb de plans de la mise en place + self.nb_releves = nb_releves # nb de rapports de relevés topo + self.nb_csvs = nb_csvs # nb de fichiers CSV + self.points = points # liste des points du CSV + self.nb_points = nb_points # nb de points des CSVs TODO: REDONDANT avec len(points) + self.nb_dwgs = nb_dwgs # nb de fichiers DWG + self.notifs = notifs # liste contenant les notifs du VISA + self.controles = controles # variable drapeau contenant toutes les validations de controles + # (voir Document pour plus d'info sur sa représentation) + + def notifier(self, categorie, texte): + """ + Ajoute une notification au _Projet + """ + print("\n_Projet.notifier()") + print(f"Incident ajouté :{texte} ") + incident = _Notification( categorie, texte) + self.notifs.append(incident) + + + def lister_fichiers(self, dossier): + """ + Construit une liste avec les fichiers de 'dossier' + + - Parcours le dossier et ses sous-dossiers, ajoute TOUS les fichiers + dans une liste de _Fichier. + - Chaque _Fichier ajouté a ses attributs mis a jour. + - Les nombres des différentes catégories de fichiers nécessaires + au projet sont mis a jour. + :param user_input: nom complet du dossier + :return: liste d'élements de type _Fichier + """ + print("\n_Projet.lister_fichiers()") + self.fichiers = [] + + print("Création liste de _Fichier...") + + for dossier_courant, list_sousdossiers, list_fichiers in os.walk(dossier): + for fichier_courant in list_fichiers: + ce_Fichier = lire(dossier_courant, fichier_courant) + self.fichiers.append(ce_Fichier) + print(f'"{fichier_courant}" ajouté.') + + # mettre à jour les nbs des catégories de fichiers necessaires + if ce_Fichier.implication in "Necessaire": + ### sa categorie {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"} + match ce_Fichier.categorie : + case "CSV": + self.nb_csvs += 1 + case "DWG": + self.nb_dwgs += 1 + case "SHEMAS": + self.nb_shemas += 1 + case "TOPO": + self.nb_releves += 1 + case _: + pass + + print("Fin création liste.") + + +# TODO surcharger la fonction print native pour cet affichage +def afficher_liste(liste): + """ + Affiche le nom des fichiers de la liste des _Fichiers + """ + print("\nprojet.afficher_liste()") + for courant in liste: + print("\n" + courant.nom + "." + courant.extension) + + +def enraciner(projet): + """ + récupère le repertoire de travail (working directory) courant + met à jour l'attribut 'racine' d'un projet + """ + print("\nprojet.enraciner()") + projet.racine = os.getcwd() + print("Racine : ".ljust(16), f"{projet.racine}") + + +def calculer_taille(projet): + """ + calcule la taille des fichiers necessaires d'un liste + d'élements _Fichier. + Met à jour l'attribut 'taille' dans le projet + """ + print("\nprojet.calculer_taille()") + taille = 0.0 + for courant in projet.fichiers: + if courant.implication in "Necessaire": + taille += courant.taille + projet.taille = taille + # Affichage adapté à la bonne unité + if taille < 1024 : + unite = "octets" + elif taille < 1024**2 : + unite = "Ko" + taille /= 1024 + else : + unite = "Mo" + taille /= 1024**2 + print(f"Taille totale : {taille:.2f} {unite}.") + + +def dater(projet): + """ + recupère la date du jour + met à jour l'attribut 'date' du projet + """ + print("\nprojet.dater()") + projet.date = datetime.datetime.today().strftime('%Y%m%d') + print(f"Date : {projet.date}") + + +def nommer(projet): + """ + Met à jour l'attribut 'nom' en composant un nom. + + Le nom est constitué du NOM du dossier racine ET de la DATE + courante formatté. + + """ + print("\nprojet.nommer()") + projet.nom = f"{os.path.basename(os.getcwd())}_{projet.date}" + projet.nom = formater(projet.nom) + print(f'Nom : "{projet.nom}"') + + +def preparer_dossier_travail(projet): + """ + Créer un dossier "Travail" dans la racine du working directory et + le peuple des fichiers nécessaires + + Préfixe et suffixe les fichiers le nécessitant (le CSV et le DWG) + """ + print("\nprojet.preparer_dossier_travail()") + #travail = "Travail" + + # création du dossier "Travail" et de ses sous-dossiers " + _chemin = f"{projet.racine}\\{TRAVAIL}" + #nomemclaturer les noms des sousdossiers + sousdossier = f"{_chemin}\\{projet.nom}" + + # pour le moment tous les sous dossiers sont + dossierCSVs = _chemin + dossierDWGs = _chemin + dossierPDFs = _chemin + dossierTOPOs = _chemin + + print(f'Chemin du dossier de préparation : "{_chemin}"') + try: + os.mkdir(_chemin) + print (f'Dossier "{TRAVAIL}" créé.') + os.mkdir(sousdossier) # dossier pour le projet ARCGIS + + if projet.nb_csvs > 1: + dossierCSVs = f"{sousdossier}_CSV" + os.mkdir(dossierCSV) + print (f'Dossier "CSV" créé.') + if projet.nb_dwgs > 1: + dossierDWGs = f"{sousdossier}_DWG" + os.mkdir(dossierDWGs) + print (f'Dossier "DWG" créé.') + if projet.nb_shemas > 1: + dossierPDFs = f"{sousdossier}_PDF" + os.mkdir(dossierPDFs) + print (f'Dossier "PDF" créé.') + if projet.nb_releves > 1: + dossierTOPOs = f"{sousdossier}_TOPO" + os.mkdir(dossierTOPOs) + print (f'Dossier "TOPO" créé.') + + except FileExistsError as erreur: + print(f'AVERTISSEMENT: LE DOSSIER "{TRAVAIL}" EXISTE DÉJA. SUPPRIMER LE, PUIS RELANCER LE SCRIPT SVP.') + purger_et_finir_programme(projet) + except OSError as erreur: + print(f"FICHIER NON TROUVÉ. SUREMENT UN PB DE CHEMIN EN AMONT.") + purger_et_finir_programme(projet) + + + + # peuplement du dossier Travail avec les fichiers necessaires + print("Copie des fichiers nécessaires et nomenclature de leurs noms...") + + # demande l'annnée du revelé topo à l'utilisateur + annee_topo = saisir_annee_topo() + + # nom du dossier projet + dossier = f"{os.path.basename(os.getcwd())}" + dossier = formater(dossier) + print(f"dossier : {dossier}") + + # les suffixes ajoutés en cas de plusieurs fichiers dans la même catégorie + letterCSV = "\0" if projet.nb_csvs <= 1 else "A" # condition ternaire en python + letterDWG = "\0" if projet.nb_dwgs <= 1 else "A" + letterSHEMAS = "\0" if projet.nb_shemas <= 1 else "A" + letterTOPO = "\0" if projet.nb_releves <= 1 else "A" + + for fichier in projet.fichiers: + if fichier.implication in "Necessaire": + source = fichier.chemin + print(f"source : {source}") + + # fabrication du nom du fichier de destination + dest = _chemin + "\\" + + # lors du peuplement préfixer et suffixer les noms des fichiers concernés comme suit : "Plan_nomfichierdwg.dwg" et "Point_nomfichiercsv_IN.csv" + # Confirmation par Audrey qu'il n'y a nécessairement qu'un fichier CSV par projet --> Le script s'occupe de le renommer. + # Mais parfois il peut y avoir plusieurs DWGs dont un seul est utile --> PASS -- Je laisse l'opérateur choisir lequel utiliser et le renommer manuellement. + + #{"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"} + #ANCIENNE VERSION + #----------------------------------------------------------------- + # match fichier.categorie : + # case "CSV": + # dest = f"{dossierCSVs}\\Point_{fichier.nom}_IN" + # case "DWG": + # dest = f"{dossierDWGs}\\Plan_{fichier.nom}" + # case "SHEMAS": + # dest = f"{dossierPDFs}\\{fichier.nom}" + # case "TOPO": + # dest = f"{dossierTOPOs}\\{fichier.nom}" + # case _: + # pass # TODO: voir pour les autres cas, normalement il n'y en a pas pour le moment + #----------------------------------------------------------------- + + #{"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"} + match fichier.categorie : + case "CSV": + if projet.nb_csvs <= 1 : + dest = f"{dossierCSVs}\\{dossier}_IN_Points_{annee_topo}" + else : + dest = f"{dossierCSVs}\\{dossier}_IN_Points_{annee_topo}_{letterCSV}" + letterCSV = chr(ord(letterCSV)+1) # on incremente la lettre + case "DWG": + if projet.nb_dwgs <= 1 : + dest = f"{dossierDWGs}\\{dossier}_Plan_{annee_topo}" + else : + dest = f"{dossierDWGs}\\{dossier}_IN_Points_{annee_topo}_{letterDWG}" + letterDWG = chr(ord(letterDWG)+1) + case "SHEMAS": + if projet.nb_shemas <= 1 : + dest = f"{dossierPDFs}\\{dossier}_Plan_{annee_topo}" + else : + dest = f"{dossierPDFs}\\{dossier}_IN_Points_{annee_topo}_{letterSHEMAS}" + letterSHEMAS = chr(ord(letterSHEMAS)+1) + case "TOPO": + if projet.nb_releves <= 1 : + dest = f"{dossierTOPOs}\\{dossier}_Infos_{annee_topo}" + else : + dest = f"{dossierTOPOs}\\{dossier}_IN_Points_{annee_topo}_{letterTOPO}" + letterTOPO = chr(ord(letterTOPO)+1) + case _: + pass # TODO: voir pour les autres cas, normalement il n'y en a pas pour le moment + + # renomme le fichier en conséquence + fichier.nom = os.path.basename(dest) + + dest = dest + f".{fichier.extension}" + print(f"dest : {dest}") + + if fichier.implication in "Necessaire": + try: + shutil.copyfile( source , dest) + print("" + fichier.nom.ljust(40,".") + "copié") + except shutil.SameFileError as err : + print(f"Le fichier existe déjà.") + #TODO : + # Confirmation par Audrey qu'il n'y a necessairement qu'un fichier CSV par projet --> On peut le renommer. + # Mais parfois il peut y avoir plusieurs DWGs dont un seul est utile --> PASS -- Je laisse l'opérateur choisir lequel utiliser et le renommer manuellement. EDIT : Nathalie prefere que tous les fichier DWG soient préfixé avec Plan_ + #TODO : verifier aussi la longueur des noms de fichiers. Notifier si nécessaire + + +def controler_longueur_noms(projet): + """ + controle la longueur des noms des fichiers du projet. + + Notifie si necessaire les noms trop longs + """ + #print("\nprojet.controler_longueur_noms()") + for fichier in projet.fichiers: + match fichier.extension: + case "dwg": + if len(fichier.nom) > 38 : # 40 - 2 si lettres de suffixe "_A", "_B", etc + projet.notifier("DWG", f'"{fichier.nom}.dwg" : Nom trop long.') + case "csv" : + if len(fichier.nom) > 43 : + projet.notifier("CSV", f'"{fichier.nom}.csv" : Nom trop long.') + case "pdf" : + if len(fichier.nom) > 43 : + projet.notifier("PDF", f'"{fichier.nom}.pdf" : Nom trop long.') + case _: + if len(fichier.nom) > 45 : + projet.notifier("FRONT", f'"{fichier.nom}.{fichier.extension}" : Nom trop long.') + +def formater_vers_ArcGIS(projet, fichier_entree, fichier_sortie): + """ + Lit et formatte un fichier CSV pour son importation dans ArcGIS. + + Lit la 1ère ligne et s'assure de la présence des titres de colonnes, + formatte cette 1ère ligne. + Supprime les lignes vides. + Change le séparateur décimal ',' en '.' (necessaire car sinon je ne peux convertir les strings en float) + Notifie si nécessaire les points de controles qui ne passent pas. + Met à jour l'attribut nb_points + Contraintes : + - Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements. (Normalement c'est toujours le cas de toutes façon) + - Le fichier doit avoir exactement 5 colonnes. TODO : vois si plus de souplesse avec le catchage de paramètres restants + param user_input: nom complet d'un fichier csv + NOTE: Ne pas formatter le fichier original, mais celui qui est deja copié dans le dossier Travail + """ + print("\nprojet.formatter_vers_ArcGIS()") + + # titres des colonnes correctement formattés. + titres = "id_point;TYPE;X;Y;Z\n" + + sortie = [titres] # le fichier de sortie ( representé comme une liste de lignes ) contient un premiere ligne de titre + + + df = open(fichier_entree, "r") + ligne = df.readline() + + # analyser la premiere ligne et formatter cette ligne + # Si le premier mot est un nombre Alors il manque les titres, dans + # ce cas insérer une ligne. + mots = ligne.split(sep=";", maxsplit=1) + print(f"Mots[0] = {mots[0]}") + + + if mots[0].isnumeric(): + #insérer ligne + sortie.append(ligne) + projet.nb_points += 1 + + # la tête de lecture du descripteur de fichier ne reset pas sa position + # donc on peut continuer le parcours des lignes directement + for ligne_courante in df : + + # lit et controle la ligne courante + courant = lire_et_controler_ligne(projet, ligne_courante) + # ajoute la ligne + if courant not in "": + sortie.append(f"{courant}\n") + # compter ce point + projet.nb_points += 1 + + df.close() + + # je reouvre le descripteur en mode w only pour ecrire le fichier + df = open(fichier_sortie, "w") + # écriture + for ligne in sortie: + #print(f"{ligne}") + df.write(ligne) # NOTE: write() + + df.close() + + print("Fin formatage du CSV.") + + +def lire_et_controler_ligne(projet, ligne): + """ + Analyse une chaine de caractère (concrètement, une ligne du CSV)/ + + remplace les séparateurs décimaux ',' par '.' + Notifie (dans le projet lié) les points de controles qui ne passent pas. + Met à jour l'attribut nb_points du projet lié + Met à jour la variable de controles du projet lié + Contraintes : + - Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements. + :return: une chaine de carac formatté pour ArcGIS + """ + #print("\nprojet.lire_et_controler_ligne()") + #print(f'ENTREE : "{ligne}"') + # enlever les espaces + ligne = ligne.replace(" ", "") + ligne = ligne.replace(",", ".") + ligne = ligne.replace("\n", "") + # si la ligne n'est pas vides alors + # TODO : changer le match en if ligne not in ["", ";;;;"]: + match ligne: + case ";;;;;;" | ";;;;;" | ";;;;" | ";;;" | ";;" | ";" | "" : + ligne = "" + case _: + # controler la projection de ce point + # NOTE: split() tokenise la ligne en items (des strings), si un champ est manquant, alors il tokenise une chaine vide( "" ) + # On peut donc tester cette chaine vide pour savoir si une champ est manquant.abs + # Je vais dont controler la présence du champ ID, car il apparait que parfois il est manquant. Si tel est le cas, + # Notifier et terminer le programme --> dossier FAIL + # + # NOTE: split() n'enleve pas le \n de la fin de ligne dans la chaine --> ligne = ligne.replace("\n", "") que j'ai rajouter au dessus + try : + id_point, type_, point_x, point_y, point_z, *autres = ligne.split(sep=";") + except ValueError: + print("LE FICHIER SOURCE CSV EST MAL FORMATÉ, IL DOIT MANQUER DES CHAMPS ET DE SÉPARATEURS DE CHAMPS.") + purger_et_finir_programme(projet) + + + #PRINT de controle + print(f'ID:"{id_point}", TYPE:"{type_}", X:"{point_x}", Y:"{point_y}", Z:"{point_z}", *autres={autres}') + + # controler qu'il n'y est pas un champ vide pour id_point, TYPE, X et Y. (Z étant optionnel) + # Si tel est le cas, on quitte FAIL + if id_point in "": + print('AVERTISSEMENT : COLONNE "ID_POINT" MANQUANTE.') + purger_et_finir_programme(projet) + if type_ in "": + print('AVERTISSEMENT : COLONNE "TYPE" MANQUANTE.') + purger_et_finir_programme(projet) + if point_x in "": + print('AVERTISSEMENT : COLONNE "X" MANQUANTE.') + purger_et_finir_programme(projet) + if point_y in "": + print('AVERTISSEMENT : COLONNE "Y" MANQUANTE.') + purger_et_finir_programme(projet) + if point_z in "": + print('AVERTISSEMENT : COLONNE "Z" MANQUANTE.') + purger_et_finir_programme(projet) + + # controler que id_point est un entier + # Si c'est un flottant alors ya un pb, on quitte FAIL + try : + if float(id_point)/int(id_point) != 1.0 : + print("AVERTISSEMENT : LA PREMIÈRE COLONNE EST UN FLOTTANT. ATTENDU UN ID_POINT DE VALEUR ENTIÈRE.") + print('IL DOIT MANQUER LA COLONNE "ID_POINT AINSI QUE LE SÉPARATEUR DE CHAMP ";".') + purger_et_finir_programme(projet) + except ValueError : + print("AVERTISSEMENT : LA COLONNE ID_POINT N'A PAS UN NOMBRE.") + print(f'ATTENDU UN ENTIER. TROUVÉ ID_POINT DE TYPE "{type(id_point)}"') + purger_et_finir_programme(projet) + + + # A partir d'ici il est attendu que la tokenisation se soit bien déroulée + try : + projo = controler(id_point, float(point_x), float(point_y)) + except ValueError : + print("AVERTISSEMENT : UNE COLLONNE (X OU Y) EST MAL CONVERTIE.") + print(f'ATTENDU VALEURS DÉCIMALES. TROUVÉ X : DE TYPE "{type(point_x)}", Y DE TYPE "{type(point_y)}"') + purger_et_finir_programme(projet) + # notifier si pas bonne projection + if projo in "Mauvaise projection": + incident = _Notification("CSV", f"Point ID {id_point} : Mauvaise projection") + projet.notifs.append(incident) + projet.controles |= 2**2 + + #print(f'SORTIE : "{ligne}"') + + return ligne + + + + + + + + + +def remplir(modele, projet): + """ + Complète le Visa du projet à partir d'un fichier modele. + + Controle le nb de fichiers de chaque catégorie et notifie. + Parcours la liste de notifications du projet et ajoute chaque + notification dans la feuille concerné du classeur + A défaut, une notification de la liste qui n'a pas de catégorie + avec sa propre feuille sera ajoutée à la première feuille (FRONT) + + Enregistre le Visa dans le repertoire de Travail + Contraintes : + - le modele du VISA doit être NÉCESSAIREMENT dans le même + dossier que le script. TODO : traité l'exception si fichier non trouvé + - le modele du VISA NE doit PAS avoir eu de modification notable + dans son design ( emplacements des cellules utilisées pour + notifier, nom des feuilles, etc) + """ + + print("\nvisa.remplir()") + + + + # creation d'un workbook à partir d'un modele + classeur = xls.load_workbook(modele) + + + ## definition personnalisé vers chaque feuille du classeur + FRONT = classeur[classeur.sheetnames[0]] + PDF = classeur[classeur.sheetnames[1]] + CSV = classeur[classeur.sheetnames[2]] + DWG = classeur[classeur.sheetnames[3]] + + + # Nom dossier + # Date de reception + # Date Visa + FRONT['C1'].value = f'Analyse : {projet.nom}\nDate de réception :\nDate visa : {projet.date}' + + # Taille du dossier + taille = projet.taille + # Affichage adapté à la bonne unité + if taille < 1024 : + unite = "octets" + elif taille < 1024**2 : + unite = "Ko" + taille /= 1024 + else : + unite = "Mo" + taille /= 1024**2 + + + FRONT['C10'].value = f"Taille totale : {taille:.2f} {unite}." + + + # TODO: factoriser les instructions ci dessous (pas sur...) + + + + ### FRONT DWG + # Je ne peux pas controler automatiquement la projection du DWG (c'est un binaire illisible hors de AutoCAD) + # J'ajoute donc automatiquement la mention comme dans le script précedent. + FRONT['C3'].value = f"{projet.nb_dwgs} fichier(s) DWG présent(s).\nProjection de tous les fichiers DWG : RFG93-CC43 (EPSG:3943)." + + # Controle le nb de fichiers DWG + if projet.nb_dwgs == 0: + projet.controles |= 2**4 # allume le bit 5ième bit + FRONT['C3'].value = "Pas de fichier DWG présent." + + # Regarde si des fails ont été détectés sur les DWGs + fail = projet.controles & FAIL_DWG # operation logique avec le masque + # notifie en consequence + if fail : + FRONT['B3'].value = "FAIL" + else : + FRONT['B3'].value = "OK" + + + + ### FRONT PDF + FRONT['C4'].value = f"{projet.nb_shemas} fichier(s) PDF présent(s)." + + # Controle le nb de fichiers PDF + if projet.nb_shemas == 0: + projet.controles |= 2**8 # allume le 9ième bit + FRONT['C4'] = "Pas de fichier PDF présent." + + # Regarde si des fails ont été détectés sur les PDFs + fail = projet.controles & FAIL_PDF + # notifie en consequence + if fail : + FRONT['B4'].value = "FAIL" + else : + FRONT['B4'].value = "OK" + + + + ### FRONT CSV + FRONT['C5'].value = f"1 fichier(s) CSV présent(s).\n{projet.nb_points} point(s) en RFG93-CC43 (EPSG:3943)." + + # Controle le nb de fichiers CSV + if projet.nb_csvs == 0: + projet.controles |= 2**0 # allume le 1er bit + FRONT['C5'].value = "Pas de fichier CSV présent." + + # Regarde si des fails ont été détectés sur les CSVs + fail = projet.controles & FAIL_CSV + # notifie en consequence + if fail : + FRONT['B5'].value = "FAIL" + else : + FRONT['B5'].value = "OK" + + + + # FRONT FICHE INFO TOPOLOGIE + FRONT['C6'].value = f"{projet.nb_releves} fiche(s) Info. présente(s)." + + # Controle le nb de fichiers Fiche Topologique + if projet.nb_releves == 0: + projet.controles |= 2**12 # allume le 13ième bit + FRONT['C6'].value = "Pas de fiche d'Info. Topologie présente." + + # Regarde si des fails ont été détectés sur les Fiches Topo + fail = projet.controles & FAIL_INFO + # notifie en consequence + if fail : + FRONT['B6'].value = "FAIL" + else : + FRONT['B6'].value = "OK" + + + + + # Notifier dans les autres Feuilles + + # lignes en cours à remplir pour chaque feuille + # initialisé aux lignes où on va commencer à notifier + A = 2 # "B2" # Pour la feuille PDF + B = 2 # "B2" # Pour la feuille CSV + C = 2 # "B2" # Pour la feuille DWG + D = 12 # "C12" # Pour la feuille FRONT + + cell = "" # cellule courante qui va être notifier + + for notif in projet.notifs : + match notif.categorie : + case "PDF" : + cell = PDF["B"+str(A)] + A += 1 + case "CSV" : + cell = CSV["B"+str(B)] + B += 1 + case "DWG" : + cell = DWG["B"+ str(C)] + C += 1 + case _: + cell = FRONT["C"+str(D)] + D += 1 + + cell.value = notif.texte + + + # Ici le VISA doit être correctement rempli + # on sauvegarde dans un fichier le classeur + classeur.save(f"Travail\\{projet.nom}_VISA.xlsx") + + +def purger_et_finir_programme(projet): + """ + Efface le dossier "Travail" si il existe puis termine le programme. + + Cette fonction est utilisée quand des expections sont levées et + qu'elles nécessitent la fermeture prématurée du programme. + """ + print("\npurger_et_finir_programme()") + print("\nFIN DE PROGRAMME.\n") + # Ne fonctionne pas car des descripteurs de fichiers sont utilisés au moment de la demande + ## shutil.rmtree(f"{projet.racine}\\{TRAVAIL}" , ignore_errors=True) + quit() + + + + + + + + + + + +## NOTE : Pour traiter le cas des sous-dossiers lors du dézippage +# il vaut mieux que l'opérateur copie le fichier banbou.py dans chaque +# sous-dossiers puis l'execute. +## Donc Il est attendu que : +# - le fichier script se situe DANS le dossier contenant que +# les fichiers originaux. +# - le modele du visa soit dans un autre dossier BIEN SPÉCIFIÉ +# (je vais remettre un chemin absolue en dur "Desktop\Banbou" +# car suivant l'opérateur, le dézippage crée des sous-sous-dossiers, +# et serait problématique pour un chemin relatif) + + + +# Création entité projet et mise à jour de ses attributs +ce_Projet = _Projet() + +enraciner(ce_Projet) + +dater(ce_Projet) + +nommer(ce_Projet) +# controle de la longueur du nom du projet +if len(ce_Projet.nom) >=46 : + print(f"\n{ce_Projet.nom} EST UN NOM DE DOSSIER TROP LONG (+ DE 46 CARACTÈRES). VEUILLEZ RACCOURCIR SON NOM.") + purger_et_finir_programme(ce_Projet) + +# explorer le dossier Trouvé et fabriquer la liste des fichiers nécessaires +ce_Projet.lister_fichiers(ce_Projet.racine) + +# calcule la taille des fichiers nécessaires +calculer_taille(ce_Projet) + +# Ici le projet doit avoir toutes les données nécessaires pour fabriquer +# et remplir les dossiers attendus +preparer_dossier_travail(ce_Projet) + + +# Règle nouvellement ajouté : il faut normalement UN SEUL fichier CSV, +# l'opérateur ne devrait plus fusionner manuellement des fichiers CSV. +if ce_Projet.nb_csvs > 1: + print("\nAVERTISSEMENT : IL Y A PLUSIEURS FICHIERS CSV.") + print("VEUILLEZ NE GARDER QU'UN SEUL FICHIER CSV DANS LE DOSSIER ORIGINAL.") + purger_et_finir_programme(ce_Projet) + + +# On formatte le/les fichier/s CSV pour ArcGIS +for fichier in ce_Projet.fichiers: + if fichier.categorie in "CSV": + entree = ce_Projet.racine + "\\Travail\\" + fichier.nom + ".csv" + sortie = ce_Projet.racine + "\\Travail\\" + fichier.nom + ".csv" + formater_vers_ArcGIS(ce_Projet, entree, sortie) + + +# On controle les noms des fichiers copiés +controler_longueur_noms(ce_Projet) + + +# On fabrique le VISA +#modele = ce_Projet.racine + "\\VISA_BANBOU.xlsx" +remplir(MODELE, ce_Projet) + + + + + + +#Fin de Programme Attendu print("\nProgramme terminé correctement. ") \ No newline at end of file diff --git a/README.md b/README.md index 2f4e9aa..4617427 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ recollement de fibre optiques. Lorsque vous débutez sur Banbou, la première fois. -1. Sur le bureau, créez un dossier 📁 nommé *Banbou* (B majuscule). +1. **Sur le bureau**, créez un dossier 📁 nommé *Banbou* (B majuscule). 2. Copiez/collez 📋 dans celui ci, les fichiers : - `NEOBANBOU.py` 📄 - `VISA_BANBOU.xlsx` 📄 @@ -32,7 +32,7 @@ Vous pouvez fermer les fenêtres apparues une fois le script terminé. Bien jou Après avoir lu et compris le contenu de l'ancien script, et avoir apporté quelques modifications, j'ai plutot décidé de repartir de zéro. Ma première version était divisé en plusieurs modules et fichiers. -Mais pour une question d'ergonomie dans l'utilisation, j'ai tout regroupé sur un seul fichier. +Mais pour une question d'ergonomie d'utilisation pour l'opérateur, j'ai tout regroupé sur un seul fichier (comme l'ancien script). ### 🧪 Tests unitaires 🧪 @@ -67,31 +67,35 @@ Voici un explicatif du déroulé du programme : #### 3. Classes et fonctions -Il s'agit du regroupement des modules que j'avais externalisés dans l'ancienne version : +Il s'agit du regroupement des modules que j'avais externalisés dans l'ancienne version. +Lignes : -- lignes 172-245 : Une représentation d'un fichier. -- lignes 251-256 : Une représentation d'une notification. -- lignes 266-328 : Une fonction importante qui controle la validité des coordonnées d'un point géographique. -- lignes 337-472 : Des fonctions *Input/Output* sur des chaines de caractères (ex: formater un nom suivant une nomenclature donné, etc) -- lignes 482-906 : Une représentation du projet. -- lignes 916-1081 : Une fonction qui génère le Visa de controle. +- 172-245 : Représentation d'un fichier. +- 251-256 : Représentation d'une notification. +- 266-328 : Fonction importante qui controle la validité des coordonnées d'un point géographique. +- 333-502 : Fonctions E/S sur des chaines de caractères (ex: formater un nom suivant une nomenclature donné, etc) +- 511-595 : Représentation du projet. +- 608-663 : Fonctions pour obtenir des elements d'environnement pour le projet : date, nom du dossier, etc. +- 666-808 : Fonction importante qui crée les dossiers nécessaires et copie les fichiers de travail. +- 811-831 : Fonction qui controle la longueur des noms de fichier. +- 833-985 : Fonctions importantes qui formate le fichier CSV. +- 995-1060 : Fonction qui génère le Visa de controle. #### 4. Exécution du programme >Note : Le programme principal parait donc très court car il va ensuite faire appel à toutes les autres fonctions vu plus haut. -- lignes 1123-1134 : Le programme commence par créer un Projet et définie des valeurs utiles (date, chemin, etc) -- ligne 1136 : Le programme parcourt le dossier où se trouve le script et analyse tous les fichiers trouvés (quel est son nom, son extension, est-il est utile pour le projet ? ) et stocke toutes ces infos dans une liste pour être utiliser plus tard. - - ligne 1138 : calcule la taille **seulement** des fichiers qui vont être copiés dans le dossier de Travail. - - ligne 1142 : reprend la liste créée et décide des dossiers à créer puis y copie les fichiers attendus. Ajoute les préfixes et suffixes aux noms de fichiers si nécessaire. -- lignes 1151-1156 : le fichier CSV contenant les points est lu. Chaque ligne, contenant une coordonnée, est : +- 1200-1210 : Le programme commence par créer un Projet et définie des valeurs utiles (date, chemin, etc) +- 1213 : Le programme parcourt le dossier où se trouve le script et analyse tous les fichiers trouvés (quel est son nom, son extension, est-il est utile pour le projet ? ) et stocke toutes ces infos dans une liste pour être utiliser plus tard. +- 1216 : calcule la taille **seulement** des fichiers qui vont être copiés dans le dossier de Travail. +- 1220 : reprend la liste créée et décide des dossiers à créer puis y copie les fichiers attendus. +- 1232-1236 : le fichier CSV contenant les points est lu. Chaque ligne, contenant une coordonnée, est : - analysée, on vérifie de sa projection. - formatée, on corrige certains défauts de syntaxe. Les lignes vides, etc. - ajoutée à une liste. Cette liste est ensuite écrite dans un fichier. -- ligne 1161 : Le programme controle la longueur de tous les noms de fichiers. -- ligne 1166 : finalement le programme crée un visa à partir d'un modèle, et ajoute toutes les informations nécessaires, ainsi que toutes les notifications -sur des points de controles qui ne seraient pas passés. +- ligne 1240 : Le programme controle la longueur de tous les noms de fichiers. +- ligne 1245 : finalement le programme crée un visa à partir d'un modèle, et ajoute toutes les informations nécessaires, ainsi que toutes les notifications sur des points de controles qui ne seraient pas passés. ### 🛂🚧 Les points de controles réalisés 🚧🛂