## Script Banbou pour le prétraitement des données des dossiers de recollement ## de fibre optique. import os, shutil, datetime # DEFINITIONS DE FONCTIONS def formatter(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 ' ' et les traits d'union '-' 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 def creer_liste(dossier): """ Construit une liste avec les fichiers de 'dossier' Parcourt le dossier et ses sous-dossier, ajoute TOUS les fichiers dans une liste de _Fichier. Chaque _Fichier ajouté a ses attributs mis a jour. Retourne la liste. :param user_input: nom complet du dossier :return: liste d'élements de type _Fichier """ liste = [] for dossier_courant, list_sousdossiers, list_fichiers in os.walk(dossier): for fichier_courant in list_fichiers: print(f"\nfichier courant : {fichier_courant}") ce_Fichier = _Fichier.lire(dossier_courant, fichier_courant) liste.append(ce_Fichier) return liste # TODO surcharger la fonction print native pour cet affichage def afficher(liste): """ Affiche le nom des fichiers de la liste des _Fichiers """ for courant in liste: print("\n" + courant.nom + "." + courant.extension) def controller_projection(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. 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" """ # Valeurs constantes définie par IGN projections_conformes = [ { "nom" : "GPS", "E0" : 0, "N0" : 0, "fenetreE0" : 180, "fenetreN0" : 90, "EPSG" : 4326 # Correspond à la projection des GPS }, { "nom" : "Lambert93", "E0" : 700000, "N0" : 6600000, "fenetreE0" : 600000, "fenetreN0" : 600000, "EPSG" : 2154 # Attention la plus recente est 9794 }, { "nom" : "CC42", "E0" : 1700000, "N0" : 1200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3942 }, { "nom" : "CC43", "E0" : 1700000, "N0" : 2200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3943 }, { "nom" : "CC44", "E0" : 1700000, "N0" : 3200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3944 }, { "nom" : "CC45", "E0" : 1700000, "N0" : 4200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3945 }, { "nom" : "CC46", "E0" : 1700000, "N0" : 5200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3946 }, { "nom" : "CC47", "E0" : 1700000, "N0" : 6200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3947 }, { "nom" : "CC48", "E0" : 1700000, "N0" : 7200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3948 }, { "nom" : "CC49", "E0" : 1700000, "N0" : 8200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3949 }, { "nom" : "CC50", "E0" : 1700000, "N0" : 9200000, "fenetreE0" : 600000, "fenetreN0" : 111000, "EPSG" : 3950 } # TODO : ajouter les projections DOMTOMs ] # 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 if 6089000 < point_y < 6311000: if 1100000 < point_x < 1300000: print("\nAvertissement : ces coordonnées peuvent être interpréter correctement comme du CC47 et du Lambert93") print("Normalement vous devrez projeter la Table de Point en CC47") # définie la projection conique Nord en regardant dans quelle intervalle la valeur se situe for elem in projections_conformes: borne_basse = elem["N0"] - elem["fenetreN0"] borne_haute = elem["N0"] + elem["fenetreN0"] if borne_basse < point_y < borne_haute : projection = elem["nom"] print(f"\nProj. trouvé : {projection}") # 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"\nAvertissement : Longitude du point id {id_point} pas en métropole.") return projection # REPRÉSENTATION DES DONNÉES 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", taille=0): self.nom_original = nom_original # - son nom original self.chemin = chemin # - son chemin absolue (dossier+fichier) self.extension = extension # - son extension self.nom = nom # - son nom formaté self.implication = implication # - son implication dans le projet self.taille = taille def afficher(self): """ Affiche dans la sortie standard les éléments du fichier """ print("") 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("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 """ # initialiser un _Fichier ce_Fichier = _Fichier() # lire son chemin ce_Fichier.chemin = dossier + "\\" + fichier # déterminer son nom original et son extension ce_Fichier.nom_original, ce_Fichier.extension = fichier.split(".", maxsplit=1) # maxsplit permet de spliter qu'une fois au cas ou on a plusieurs . dans le nom de fichier) # formatter et écrire le nom ce_Fichier.nom = formatter(ce_Fichier.nom_original) # déterminer son implication ce_Fichier.impliquer() # calculer sa taille ce_Fichier.taille = os.path.getsize(ce_Fichier.chemin) return ce_Fichier def impliquer(self): """Définir l'implication d'un fichier Les fichiers nécessaires seront copiés dans le répertoire "Travail" L'implication est définie en fonction de l'extension du fichier. 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), """ match self.extension: case "pdf" | "dwg" | "csv" | "doc" | "docs" | "odt" | "PDF" | "DWG" | "CSV" | "DOC" | "ODT" | "DOCS": self.implication = "Necessaire" case _: self.implication = "A-ignorer" 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_csv=0, nb_dwgs=0, nb_points = 0, notifs=[]): 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_csv = nb_csv # nb de fichiers CSV self.nb_dwgs = nb_dwgs # nb de fichiers DWG self.nb_pdfs = nb_pdfs # nb de fichiers PDF self.nb_points = nb_points # nb de points des CSVs self.notifs = notifs # liste contenant les notifs du VISA def enraciner(self): """ récupère le repertoire de travail (working directory) courant met à jour l'attribut 'racine' """ self.racine = os.getcwd() print("\nRacine : ".ljust(16), f"{self.racine}\n") def calculer_taille(self): """ calcule la taille des fichiers necessaires d'un liste d'élements _Fichier. Met à jour l'attribut 'taille' dans le projet """ taille = 0.0 for courant in self.fichiers: if courant.implication in "Necessaire": taille += courant.taille self.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"\nTaille totale : {taille:.2f} {unite}.\n") def dater(self): """ recupère la date du jour met à jour l'attribut 'date' du projet """ self.date = datetime.datetime.today().strftime('%Y%m%d') def nommer(self): """ met à jour l'attribut 'nom' en composant un nom. Le nom est constitué du nom du dossier racine et de la date courante formatté. """ self.nom = f"{os.path.basename(os.getcwd())}_{self.date}" def preparer_dossier_travail(self): """ Créer un dossier "Travail" dans la racine du working directory et le peuple des fichiers nécessaires Met à jour le nombre de fichiers CSVs, DWGs et """ travail = "Travail" # création du dossier "Travail et dossier" chemin = f"{self.racine}\\{travail}" print(f"\nCHEMIN FABRIQUE : {chemin}") try: os.mkdir(chemin) print (f'\nDossier "{travail}" créé.') except FileExistsError as erreur: print(f'\nAvertissement: Le dossier "{travail}" existe déja.') except OSError as err: print(f"\nFichier non trouvé. Surement un pb de chemin en amont.") # peuplement du dossier Travail avec les fichiers necessaires for fichier in self.fichiers: source = fichier.chemin print(f"\nsource : {source}") # 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. match fichier.extension: case "csv": dest = self.racine + "\\" + travail + "\\" + "Point_" + fichier.nom + "_IN" + "." + fichier.extension case _: dest = self.racine + "\\" + travail + "\\" + fichier.nom + "." + fichier.extension print(f"dest : {dest}") if fichier.implication in "Necessaire": try: shutil.copyfile( source , dest) print("\n" + fichier.nom.ljust(40,".") + "copié") except shutil.SameFileError as err : print(f"\nLe fichier {fichier.nom + fichier.extension} 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. #TODO : verifier aussi la longueur des noms de fichiers. Notifier si nécessaire def formatter_vers_ArcGIS(self, fichier): """ 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 puis change le séparateur d'élements en ',' -- séparateur adapté pour ArcGIS -- Notifie si nécessaire les points de controles qui ne passent pas. Renomme le fichier formatté en "Point_fichier_IN.csv" Contraintes : - Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements. - Le fichier doit avoir exactement 5 colonnes. param user_input: nom complet d'un fichier csv return: Le nombre de points du csv """ # titres des colonnes correctement formattés. titres = "id_point;TYPE;X;Y;Z" # On va traiter ligne par ligne le fichier, puis cette ligne sera ajouté # a une liste. Finalement, on écrasera chaque ligne du fichier avec cette liste df = open(fichier) sortie = [] # analyser la premier ligne et formatter cette ligne ligne = df.readline() # 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) if mots[0].isalpha(): #insérer ligne sortie.append(titres) else: sortie.append(ligne) # 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 : # enlever les espaces ligne_courante = ligne_courante.replace(" ", "") # si la ligne n'est pas vides alors # TODO : changer le match en if ligne_courante not in ["", ";;;;"]: match ligne_courante: case "" | ";;;;": pass case _: # controler la projection de ce point id_point, type_, point_x, point_y, *autres = ligne_courante.split(sep=";") projection = controller_projection(id_point, float(point_x), float(point_y)) #TODO: notifier si pas bonne projection # puis ajouter la ligne a la liste sortie.append(ligne_courante) # compter ce point self.nb_points += 1 df.close() # je reouvre le descripteur en mode w only pour ecrire le fichier df # structure représetant des messages à mentionner dans le rapport VISA # chaque notification possède un texte et sa catégorie concernée. class _Notification: def __init__(self, categorie="Pas de catégorie", texte="Pas de texte"): self.categorie = categorie self.texte = texte # -------------------------------------------------------------------------- # # -------------------------------------------------------------------------- # # MAIN racine = os.getcwd() print("\nRépertoire courant : ".center(18), racine) # --recupérer le chemin du dossier à traiter pas_de_dossier = True for a in os.scandir(): print("\ncourant scandir() = ", a.name.ljust(25), " ", a.is_dir()) if a.is_dir(): #il y a un dossier pas_de_dossier = False print(f"\nDossier à Traiter trouvé : {a.name}") racine = a if pas_de_dossier: print("\nPas de dossier trouvé...\nFin de programme.\n") os.system("pause") exit() # Création entité projet et maj de ses attributs projet = _Projet() projet.dater() projet.nommer() # controle de la longueur du nom du projet if len(projet.nom) >=46 : print(f"\n{projet.nom} est un nom de dossier trop long (+ de 46 caractères). Veuillez raccourcir son nom\nFin de programme.\n") os.system("pause") exit() projet.enraciner() projet.rapport = f"{racine}\\Visa_lot_2.xlsx" projet.fichiers = creer_liste(projet.racine)