From cf8d29daf03bb80956efc0e3fc2912e3fa01cca4 Mon Sep 17 00:00:00 2001 From: David Castex Date: Mon, 30 Jun 2025 14:08:32 +0200 Subject: [PATCH] Version finale de production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contient le nouveau fichier README et 3 fichiers : - le script sur un seul fichier. - le nouveau modèle de Visa. - le fichier de style de couche (aucune modif, juste renommé) Ces 3 fichiers sont à copier dans le NextCloud pour mise à disposition. --- NEOBANBOU.py | 1175 +++++++++++++++++++++++++++++++++ README.md | 147 ++++- STYLE_SYMBOLOGIE.lyrx | 1458 +++++++++++++++++++++++++++++++++++++++++ VISA_BANBOU.xlsx | Bin 0 -> 25630 bytes 4 files changed, 2757 insertions(+), 23 deletions(-) create mode 100644 NEOBANBOU.py create mode 100644 STYLE_SYMBOLOGIE.lyrx create mode 100644 VISA_BANBOU.xlsx diff --git a/NEOBANBOU.py b/NEOBANBOU.py new file mode 100644 index 0000000..9e32bf2 --- /dev/null +++ b/NEOBANBOU.py @@ -0,0 +1,1175 @@ +## 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 +print("\nProgramme terminé correctement. ") \ No newline at end of file diff --git a/README.md b/README.md index 4367c78..be10558 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,139 @@ -# NeoBanbou +# NeoBanbou 🐼🐼 -Script Python d'assistance dans le controle des dossiers de plan de recollement de fibre optiques. +Script Python utilisé pour le controle qualité de dossiers de plan de +recollement de fibre optiques. + +## 🎍 Mise en place 📖 + +Lorsque vous débutez sur Banbou, la première fois. + +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` 📄 + - `STYLE_SYMBOLOGIE.lyrx` 📄 + +Et voilà ! + +## 🎍 Utilisation 📖 + +A faire à chaque dossier à traiter. + +1. Copiez/collez 📋 l'archive 🗃 du dossier à traiter dans le dossier *Banbou*, et dézipper la 🤐. +2. A l'intérieur de ⮩ **chaque sous-dossier** ainsi dézippé (ou à défaut dans le dossier dézippé), copiez/collez-y 📋 le fichier `NEOBANBOU.py` 📄. +3. Exécutez ⚙ le script `NEOBANBOU.py` ainsi déposé. + +Vous pouvez fermer les fenêtres apparues une fois le script terminé. Bien joué ! 👍 + +## 🎍 Fonctionnement 📖 + +### ⛪ Historique ⛪ + +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ée en plusieurs modules et fichiers. +Mais pour une question d'ergonomie dans l'utilisation, j'ai tout regroupé sur un seul fichier. + +### 🧪 Tests unitaires 🧪 + +Dans l'optique de m'exercer, j'ai créé dans l'ancienne version des tests unitaires pour chaque fonction et classe. Malheuresement, je ne les ai pas mis a jour avec la dernière version, ils ne sont donc pour le moment plus fonctionnels. + +### 🔃 Déroulé du programme 🔃 + +Voici un explicatif du déroulé du programme : + +#### 1. Imports des bibliothèques et modules. + + - lignes 4-14 : Le script commence par vérifier la présence de la commande `pip` (commande qui permet d'installer des modules *PyPI* (Python Package Index, le dépot communautaire des modules Python) et l'installe si nécessaire. + - lignes 15-25 : Le script vérifie la présence des modules additionnels utilisés dans le script et les installe si nécessaire. + +> Note : Cette partie est en vérité à exécuter qu'une seule fois, lors de la première utilisation du script. Si vous souhaitez accélérer l'exécution du script pour les prochaines fois, effacez cette partie (lignes 4 à 25). + + - lignes 29-32 : Import des modules utilisés. + + #### 2. Constantes + + - lignes 39-162 : Vous trouverez une liste de variables constantes : + - Le nom de l'utilisateur Windows. + - Le chemin vers le modèle du Visa. + - Le nom du dossier qui receptionne les fichiers créés. + - Le caractère choisi comme séparateur d'élements d'un fichier CSV. + - L'expression régulière utilisée pour filtrer les fichiers nécessaires des fichiers inutiles. (VOir lien wiki sur les RegEx) + - Les masques binaires utilisés pour les opérations logiques de validation de point de controle. TODO Voir lien plus loin pour les explications. + - Une liste de projections polaires coniques utilisé en France Métropolitaine (TODO Voir lien réf IGN). Elles sont, chacunes, définies et contraintes par leurs plages des valeurs (les coordonnées géographiques). -## Lancer les Tests +> Note : Vous pouvez modifier facilement le comportement du programme en modifiant directement ces variables. -Pour lancer les tests, il faudra préalablement installer un environnement de travail isolé +#### 3. Classes et fonctions -```bash - python -m venv venv -``` +Il s'agit du regroupement des modules que j'avais externalisés dans l'ancienne version : -Puis basculer vers cet environnement (a faire à chaque fois qu'on ouvre un shell) -```bash - source venv/Scripts/activate -``` -Sous Windows ce sera `env\Scripts\activate.bat` +- 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. + +#### 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 : + - 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. + +### 🛂🚧 Les points de controles réalisés 🚧🛂 + +#### 1. 🚦Intersection CC47 et Lambert93 🚦 + +Voir shémas (TODO) + +#### 2. 🧭 Longitude pas en métropole 🧭 +Si la projection polaire est correcte, je vérifie et avertie si la longitude du point ne se situe pas en métropole. + +#### 3. 📏 Longueur des noms de fichiers 📏 +>Note : Par nom de fichier je veux dire le nom du fichier sans son point ni son extension. Il faut cependant compter les préfixes et suffixes ajoutés. +Ex : pour le fichier `Plan_monfichierDWG.dwg`, le nom retenu sera *Plan_monfichierDWG* simplement. + +La règle retenu est : +- Pour le fichier DWG, longueur nom strictement inférieure à 31. +- Tous les autres noms de fichiers et dossiers, longueur nom strictement inférieure à 46. + +#### 4. 🛸 Fichier CSV imcomplet, inconnu 🛸 +Le formatage du fichier ne résout pas tout. Si il manque des données, des champs entiers, etc, je ne vais pas les créer. Dans ce cas, le programme s'arrete +en affichant un avertissement. -J'ai utiliser Pytest pour les tests et son module pytest-mock +#### 5. 🚧 Controle la présence des fichiers nécessaires +Un projet doit necessairement comporter au moins ces 4 fichiers : +- un fichier PDF 📄 de plan de situation +- un fichier PDF 📄 (ou autres formats), la fiche de topographie +- un fichier DWG 📄, le fichier de couches AutoCad +- un **seul** fichier CSV 📄, le fichier contenant le relevé des coordonnées de chaque points. -```bash - pip install pytest - pip install pytest-mock -``` +>Note: Si un de ces 4 fichiers est absent, le projet devrait être considéré comme **incomplet**. Des FAILs se notifieront dans la page FRONT. -Les tests sont situés dans le dossier `test/` pour les lancer tous, depuis la racine du projet : +## 🎍 A l'attention des utilisateurs avancés 📖 -``` - pytest test/ -``` +J'ai considéré l'écriture de ce script comme un exercice d'algorithmie et de d'apprentissage du Python. +De ce point de vue, il y a certainement beaucoup d'optimisation. +*C'est vrai ça : Pourquoi s'embêter à faire des classes pour des objets qui ont deja nativement dans le langage des classes et méthodes ( wtf une classe _Fichier ??), ou alors pourquoi implementer une variable binaire pour comptabiliser les controles ? (alors que de simples variables ou une liste de variables aurait pu aussi bien fait l'affaire...).* -## Auteurs +**Réponse :** pour la pratique et la curiosité ! -- [@david.castex](https://gitea.digitanie.org/david.castex) \ No newline at end of file +Donc oui, ce script n'est pas optimal. Mais, il est robuste (plutot) et fonctionnel pour l'usage prévu en production. + +Notez également qu'il n'y a eu aucun recours à de l'IA. L'exercice consistait aussi à conceptualiser les besoins. +TODO Explication avancées. \ No newline at end of file diff --git a/STYLE_SYMBOLOGIE.lyrx b/STYLE_SYMBOLOGIE.lyrx new file mode 100644 index 0000000..a935774 --- /dev/null +++ b/STYLE_SYMBOLOGIE.lyrx @@ -0,0 +1,1458 @@ +{ + "type" : "CIMLayerDocument", + "version" : "2.4.0", + "build" : 19948, + "layers" : [ + "CIMPATH=carte/c_zurlescuresro002fi091640000v1_210426_in_xytabletopoint.xml" + ], + "layerDefinitions" : [ + { + "type" : "CIMFeatureLayer", + "name" : "BANBOU_DOE_GC_ZURLESCURESRO002FI091640000V1_210426_IN_XYTableToPoint", + "uRI" : "CIMPATH=carte/c_zurlescuresro002fi091640000v1_210426_in_xytabletopoint.xml", + "sourceModifiedTime" : { + "type" : "TimeInstant" + }, + "useSourceMetadata" : true, + "description" : "BANBOU_DOE_GC_ZURLESCURESRO002FI091640000V1_210426_IN_XYTableToPoint", + "layerElevation" : { + "type" : "CIMLayerElevationSurface", + "mapElevationID" : "{CC73DA4E-A2D2-4B9E-91FB-5DC812875CDD}" + }, + "expanded" : true, + "layerType" : "Operational", + "showLegends" : true, + "visibility" : true, + "displayCacheType" : "Permanent", + "maxDisplayCacheAge" : 5, + "showPopups" : true, + "serviceLayerID" : -1, + "refreshRate" : -1, + "refreshRateUnit" : "esriTimeUnitsSeconds", + "autoGenerateFeatureTemplates" : true, + "featureElevationExpression" : "0", + "featureTable" : { + "type" : "CIMFeatureTable", + "displayField" : "TYPE", + "editable" : true, + "dataConnection" : { + "type" : "CIMStandardDataConnection", + "workspaceConnectionString" : "DATABASE=..\\..\\..\\ArcGIS\\MyProject\\MyProject.gdb", + "workspaceFactory" : "FileGDB", + "dataset" : "BANBOU_DOE_GC_ZURLESCURESRO002FI091640000V1_210426_IN_XYTableToPoint", + "datasetType" : "esriDTFeatureClass" + }, + "studyAreaSpatialRel" : "esriSpatialRelUndefined", + "searchOrder" : "esriSearchOrderSpatial" + }, + "featureTemplates" : [ + { + "type" : "CIMFeatureTemplate", + "name" : "BANBOU_DOE_GC_ZURLESCURESRO002FI091640000V1_210426_IN_XYTableToPoint", + "tags" : "Point", + "toolProgID" : "2a8b3331-5238-4025-972e-452a69535b06" + } + ], + "htmlPopupEnabled" : true, + "selectable" : true, + "featureCacheType" : "Session", + "labelClasses" : [ + { + "type" : "CIMLabelClass", + "expression" : "$feature.TYPE", + "expressionEngine" : "Arcade", + "featuresToLabel" : "AllVisibleFeatures", + "maplexLabelPlacementProperties" : { + "type" : "CIMMaplexLabelPlacementProperties", + "featureType" : "Point", + "avoidPolygonHoles" : true, + "canOverrunFeature" : true, + "canPlaceLabelOutsidePolygon" : true, + "canRemoveOverlappingLabel" : true, + "canStackLabel" : true, + "connectionType" : "Unambiguous", + "constrainOffset" : "NoConstraint", + "contourAlignmentType" : "Page", + "contourLadderType" : "Straight", + "contourMaximumAngle" : 90, + "enableConnection" : true, + "enablePointPlacementPriorities" : true, + "featureWeight" : 0, + "fontHeightReductionLimit" : 4, + "fontHeightReductionStep" : 0.5, + "fontWidthReductionLimit" : 90, + "fontWidthReductionStep" : 5, + "graticuleAlignmentType" : "Straight", + "keyNumberGroupName" : "Par défaut", + "labelBuffer" : 15, + "labelLargestPolygon" : true, + "labelPriority" : -1, + "labelStackingProperties" : { + "type" : "CIMMaplexLabelStackingProperties", + "stackAlignment" : "ChooseBest", + "maximumNumberOfLines" : 3, + "minimumNumberOfCharsPerLine" : 3, + "maximumNumberOfCharsPerLine" : 24, + "separators" : [ + { + "type" : "CIMMaplexStackingSeparator", + "separator" : " ", + "splitAfter" : true + }, + { + "type" : "CIMMaplexStackingSeparator", + "separator" : ",", + "visible" : true, + "splitAfter" : true + } + ] + }, + "lineFeatureType" : "General", + "linePlacementMethod" : "OffsetCurvedFromLine", + "maximumLabelOverrun" : 36, + "maximumLabelOverrunUnit" : "Point", + "minimumFeatureSizeUnit" : "Map", + "multiPartOption" : "OneLabelPerPart", + "offsetAlongLineProperties" : { + "type" : "CIMMaplexOffsetAlongLineProperties", + "placementMethod" : "BestPositionAlongLine", + "labelAnchorPoint" : "CenterOfLabel", + "distanceUnit" : "Percentage", + "useLineDirection" : true + }, + "pointExternalZonePriorities" : { + "type" : "CIMMaplexExternalZonePriorities", + "aboveLeft" : 4, + "aboveCenter" : 2, + "aboveRight" : 1, + "centerRight" : 3, + "belowRight" : 5, + "belowCenter" : 7, + "belowLeft" : 8, + "centerLeft" : 6 + }, + "pointPlacementMethod" : "AroundPoint", + "polygonAnchorPointType" : "GeometricCenter", + "polygonBoundaryWeight" : 0, + "polygonExternalZones" : { + "type" : "CIMMaplexExternalZonePriorities", + "aboveLeft" : 4, + "aboveCenter" : 2, + "aboveRight" : 1, + "centerRight" : 3, + "belowRight" : 5, + "belowCenter" : 7, + "belowLeft" : 8, + "centerLeft" : 6 + }, + "polygonFeatureType" : "General", + "polygonInternalZones" : { + "type" : "CIMMaplexInternalZonePriorities", + "center" : 1 + }, + "polygonPlacementMethod" : "CurvedInPolygon", + "primaryOffset" : 1, + "primaryOffsetUnit" : "Point", + "removeExtraWhiteSpace" : true, + "repetitionIntervalUnit" : "Map", + "rotationProperties" : { + "type" : "CIMMaplexRotationProperties", + "rotationType" : "Arithmetic", + "alignmentType" : "Straight" + }, + "secondaryOffset" : 100, + "strategyPriorities" : { + "type" : "CIMMaplexStrategyPriorities", + "stacking" : 1, + "overrun" : 2, + "fontCompression" : 3, + "fontReduction" : 4, + "abbreviation" : 5 + }, + "thinningDistanceUnit" : "Point", + "truncationMarkerCharacter" : ".", + "truncationMinimumLength" : 1, + "truncationPreferredCharacters" : "aeiou" + }, + "name" : "Classe\u00A01", + "priority" : -1, + "standardLabelPlacementProperties" : { + "type" : "CIMStandardLabelPlacementProperties", + "featureType" : "Line", + "featureWeight" : "Low", + "labelWeight" : "High", + "numLabelsOption" : "OneLabelPerName", + "lineLabelPosition" : { + "type" : "CIMStandardLineLabelPosition", + "above" : true, + "inLine" : true, + "parallel" : true + }, + "lineLabelPriorities" : { + "type" : "CIMStandardLineLabelPriorities", + "aboveStart" : 3, + "aboveAlong" : 3, + "aboveEnd" : 3, + "centerStart" : 3, + "centerAlong" : 3, + "centerEnd" : 3, + "belowStart" : 3, + "belowAlong" : 3, + "belowEnd" : 3 + }, + "pointPlacementMethod" : "AroundPoint", + "pointPlacementPriorities" : { + "type" : "CIMStandardPointPlacementPriorities", + "aboveLeft" : 2, + "aboveCenter" : 2, + "aboveRight" : 1, + "centerLeft" : 3, + "centerRight" : 2, + "belowLeft" : 3, + "belowCenter" : 3, + "belowRight" : 2 + }, + "rotationType" : "Arithmetic", + "polygonPlacementMethod" : "AlwaysHorizontal" + }, + "textSymbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMTextSymbol", + "blockProgression" : "TTB", + "depth3D" : 1, + "extrapolateBaselines" : true, + "fontEffects" : "Normal", + "fontEncoding" : "Unicode", + "fontFamilyName" : "Tahoma", + "fontStyleName" : "Regular", + "fontType" : "Unspecified", + "haloSize" : 1, + "height" : 10, + "hinting" : "Default", + "horizontalAlignment" : "Left", + "kerning" : true, + "letterWidth" : 100, + "ligatures" : true, + "lineGapType" : "ExtraLeading", + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + } + ] + }, + "textCase" : "Normal", + "textDirection" : "LTR", + "verticalAlignment" : "Bottom", + "verticalGlyphOrientation" : "Right", + "wordSpacing" : 100, + "billboardMode3D" : "FaceNearPlane" + } + }, + "useCodedValue" : true, + "visibility" : true, + "iD" : -1 + } + ], + "renderer" : { + "type" : "CIMUniqueValueRenderer", + "colorRamp" : { + "type" : "CIMRandomHSVColorRamp", + "colorSpace" : { + "type" : "CIMICCColorSpace", + "url" : "Default RGB" + }, + "maxH" : 360, + "minS" : 15, + "maxS" : 30, + "minV" : 99, + "maxV" : 100, + "minAlpha" : 100, + "maxAlpha" : 100 + }, + "defaultLabel" : "", + "defaultSymbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPoint" : { + "x" : 0, + "y" : 0, + "z" : 0 + }, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Y", + "size" : 10, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : 0, + "ymin" : 0, + "xmax" : 17, + "ymax" : 17 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "rings" : [ + [ + [ + 13.35, + 8.48000000000000043 + ], + [ + 8.5, + 0 + ], + [ + 3.64999999999999991, + 8.48000000000000043 + ], + [ + 8.5, + 17 + ], + [ + 13.35, + 8.48000000000000043 + ] + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 115, + 223, + 255, + 100 + ] + } + } + ] + } + } + ], + "scaleSymbolsProportionally" : true, + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "defaultSymbolPatch" : "Default", + "fields" : [ + "TYPE" + ], + "groups" : [ + { + "type" : "CIMUniqueValueGroup", + "classes" : [ + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "RCCC", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Z", + "size" : 7, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : -2, + "ymin" : -2, + "xmax" : 2, + "ymax" : 2 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "curveRings" : [ + [ + [ + 0, + 2 + ], + { + "a" : [ + [ + 0, + 2 + ], + [ + 5.091292279098724e-16, + 0 + ], + 0, + 1 + ] + } + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1.2249999999999999, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 169, + 0, + 230, + 100 + ] + } + } + ] + } + } + ], + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "RCCC" + ] + } + ], + "visible" : true + }, + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "RCIC", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Z", + "size" : 7, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : -2, + "ymin" : -2, + "xmax" : 2, + "ymax" : 2 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "curveRings" : [ + [ + [ + 0, + 2 + ], + { + "a" : [ + [ + 0, + 2 + ], + [ + 5.091292279098724e-16, + 0 + ], + 0, + 1 + ] + } + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1.2249999999999999, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 92, + 230, + 100 + ] + } + } + ] + } + } + ], + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "RCIC" + ] + } + ], + "visible" : true + }, + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "TEIC", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPoint" : { + "x" : 0, + "y" : 0, + "z" : 0 + }, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Y", + "size" : 10, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : 0, + "ymin" : 0, + "xmax" : 17, + "ymax" : 17 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "rings" : [ + [ + [ + 0, + 17 + ], + [ + 17, + 17 + ], + [ + 17, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 17 + ] + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 230, + 0, + 0, + 100 + ] + } + } + ] + } + } + ], + "scaleSymbolsProportionally" : true, + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "TEIC" + ] + } + ], + "visible" : true + }, + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "TACC", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPoint" : { + "x" : 0, + "y" : 0, + "z" : 0 + }, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Y", + "size" : 10, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : 0, + "ymin" : 0, + "xmax" : 17, + "ymax" : 17 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "rings" : [ + [ + [ + 0, + 0 + ], + [ + 8.60999999999999943, + 14.85 + ], + [ + 17, + 0 + ], + [ + 0, + 0 + ] + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 230, + 0, + 0, + 100 + ] + } + } + ] + } + } + ], + "scaleSymbolsProportionally" : true, + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "TACC" + ] + } + ], + "visible" : true + }, + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "TEIE", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPoint" : { + "x" : 0, + "y" : 0, + "z" : 0 + }, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Y", + "size" : 10, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : 0, + "ymin" : 0, + "xmax" : 17, + "ymax" : 17 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "rings" : [ + [ + [ + 0, + 17 + ], + [ + 17, + 17 + ], + [ + 17, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 17 + ] + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 85, + 255, + 0, + 100 + ] + } + } + ] + } + } + ], + "scaleSymbolsProportionally" : true, + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "TEIE" + ] + } + ], + "visible" : true + }, + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "TACE", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPoint" : { + "x" : 0, + "y" : 0, + "z" : 0 + }, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Y", + "size" : 10, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : 0, + "ymin" : 0, + "xmax" : 17, + "ymax" : 17 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "rings" : [ + [ + [ + 0, + 0 + ], + [ + 8.60999999999999943, + 14.85 + ], + [ + 17, + 0 + ], + [ + 0, + 0 + ] + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 85, + 255, + 0, + 100 + ] + } + } + ] + } + } + ], + "scaleSymbolsProportionally" : true, + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "TACE" + ] + } + ], + "visible" : true + }, + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "FDIE", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPoint" : { + "x" : 0, + "y" : 0, + "z" : 0 + }, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Y", + "size" : 10, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : 0, + "ymin" : 0, + "xmax" : 17, + "ymax" : 17 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "rings" : [ + [ + [ + 0, + 17 + ], + [ + 17, + 17 + ], + [ + 17, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 17 + ] + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 168, + 112, + 0, + 100 + ] + } + } + ] + } + } + ], + "scaleSymbolsProportionally" : true, + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "FDIE" + ] + } + ], + "visible" : true + }, + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "RACC", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPoint" : { + "x" : 0, + "y" : 0, + "z" : 0 + }, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Y", + "size" : 10, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : 0, + "ymin" : 0, + "xmax" : 17, + "ymax" : 17 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "rings" : [ + [ + [ + 0, + 0 + ], + [ + 8.60999999999999943, + 14.85 + ], + [ + 17, + 0 + ], + [ + 0, + 0 + ] + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 168, + 112, + 0, + 100 + ] + } + } + ] + } + } + ], + "scaleSymbolsProportionally" : true, + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "RACC" + ] + } + ], + "visible" : true + }, + { + "type" : "CIMUniqueValueClass", + "editable" : true, + "label" : "", + "patch" : "Default", + "symbol" : { + "type" : "CIMSymbolReference", + "symbol" : { + "type" : "CIMPointSymbol", + "symbolLayers" : [ + { + "type" : "CIMVectorMarker", + "enable" : true, + "anchorPoint" : { + "x" : 0, + "y" : 0, + "z" : 0 + }, + "anchorPointUnits" : "Relative", + "dominantSizeAxis3D" : "Y", + "size" : 10, + "billboardMode3D" : "FaceNearPlane", + "frame" : { + "xmin" : 0, + "ymin" : 0, + "xmax" : 17, + "ymax" : 17 + }, + "markerGraphics" : [ + { + "type" : "CIMMarkerGraphic", + "geometry" : { + "rings" : [ + [ + [ + 6, + 11 + ], + [ + 5.98000000000000043, + 17 + ], + [ + 10.98, + 17 + ], + [ + 11, + 11 + ], + [ + 17, + 11 + ], + [ + 17, + 6 + ], + [ + 11, + 6 + ], + [ + 11, + 0 + ], + [ + 6, + 0 + ], + [ + 6, + 6 + ], + [ + 0, + 5.98000000000000043 + ], + [ + 0, + 10.98 + ], + [ + 6, + 11 + ] + ] + ] + }, + "symbol" : { + "type" : "CIMPolygonSymbol", + "symbolLayers" : [ + { + "type" : "CIMSolidStroke", + "enable" : true, + "capStyle" : "Round", + "joinStyle" : "Round", + "lineStyle3D" : "Strip", + "miterLimit" : 10, + "width" : 1, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 0, + 0, + 0, + 100 + ] + } + }, + { + "type" : "CIMSolidFill", + "enable" : true, + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 255, + 255, + 0, + 100 + ] + } + } + ] + } + } + ], + "scaleSymbolsProportionally" : true, + "respectFrame" : true + } + ], + "haloSize" : 1, + "scaleX" : 1, + "angleAlignment" : "Display" + } + }, + "values" : [ + { + "type" : "CIMUniqueValue", + "fieldValues" : [ + "" + ] + } + ], + "visible" : true + } + ], + "heading" : "TYPE" + } + ], + "useDefaultSymbol" : true, + "polygonSymbolColorTarget" : "Fill" + }, + "scaleSymbols" : true, + "snappable" : true, + "symbolLayerDrawing" : { + "type" : "CIMSymbolLayerDrawing" + } + } + ], + "elevationSurfaces" : [ + { + "type" : "CIMMapElevationSurface", + "elevationMode" : "BaseGlobeSurface", + "name" : "Sol", + "verticalExaggeration" : 1, + "mapElevationID" : "{CC73DA4E-A2D2-4B9E-91FB-5DC812875CDD}", + "color" : { + "type" : "CIMRGBColor", + "values" : [ + 255, + 255, + 255, + 100 + ] + }, + "surfaceTINShadingMode" : "Smooth", + "visibility" : true, + "expanded" : true + } + ] +} \ No newline at end of file diff --git a/VISA_BANBOU.xlsx b/VISA_BANBOU.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2499dfd5f8457aec8fe54350cad8fbd67576d599 GIT binary patch literal 25630 zcmeFYQ*bZN*7q6j*tYFt$F^`7fXpxHl#%EpV#V(xwZmzSC>#p9e-=SOhK+6*jB)h!`!*`xk+GxQVI-HHF^8@P&mbJD1Z6`^9>&3@%piZ1fXW9rjBxqFiYj|IypA&ZnsbaFk0zStT=^p!MumX?On(SG9OVDtzv-Gv;?Xzl`# zCGnAvxz0tyW6_uTqm}bHd4i%K{R=WAgEiy&pF@}d0RmF^Umg>}KRY~Z7~Jihtc~pL zt?4~%ZK9PoV>TEOeQwl6e8Y6wP!SD9LC6_!P*BVZzXh~*D!9(_dA>jCls0dq$o|=Ode~t}>ctv{f;3Ke%Em^)O1f?rv zDtGvU5O_&Lncq&j+2{&N+bL*}k4H+$OM9xey1G<(>GVJ{ugyx~~?*#Da zr?esIGB0G1&g6=H<^AWb{nWDLhgpsqxj0T1s=V{alQER?5!{|MAEKO4vhE+$BY)$L zlC1m0U_3qKdIYWNNG3QPnXUQ=ytc`Y5TGp?kr(so*{ZN`AMO2onxkHn>Ti@VCsNB3 zvciVSzQ}OUtu)>lW*hLABFQ_L@Y!Wd?h%o7=FiT(>qg-wWddL>-66I|+z@QE)8b%1 z4Fzz86GeH;;vRW|k7X>46E)Byv%5e}D(%T`P#O&|E!bgSxfGjJ9+ye=(o-r!d|{?Aun87+>y zROcaB3YY}r-F@+IPq1i5vCP7eR4smYy%A?lxxnnGVH?jpv$dZ47Cjcmy%=ZC5vRRJ zvzi5av>uf{2bX9to$~lpoMZt^o?J z)!OKRTrc3a`(Q1eP!nI70C`(k`q1p%7S`7J+arvBxtGqPY%%dau0;s;-?yrPYA0p7{b*I&T0!`j6~F}w}oGO=I2&x zbrdjeP_O|<6FXz$Zj3LsiA`ePEDU7&Fz=p?1p}t`_W_4ylRwqiP8`{g4hsn@cohFa zdm*`#v*BDb-I<+?5&SWGI1zZn zRTRX`O1}joiN|TFy>!1toKIs)_4V7&kn!6|Ra z!`WQUZRLf#&%T+K`r~$wOh@D_h1iAXjc>A?^}IJfY|!4+yw0jKs48o!e(WP%?;TPW zGWLUOi_W1=XPhPzh;yLey(wptIpKJoZsbnlFC9qnAg~$CSmAkZqU$JHaNGe*$+xfp zF~ay9`9B87f4JrE!1c5KIJgqU>S9O|x}LQwT)liS*E7fsEQX#WMCtekV_df)>2~+Q za!@}%gX$xYq><)^^zSx0DL(Sx+wodozdL;Qv6mV#ZTlwWv>+I?%dnvkXC2v_QF)}Y zucK^}Tf``9SieRfLy)A{2Rh$b07Ztb1S&j6ccR~%ytyq>Jx{@k3XyW#Ys1#8>jwj$ zg&2F~8F-F{kaa_jdK%PBqNDDAa5D=QXDl|+8frjATpQ~6>*S?ns%dduXhu`Hk1C(4 zHh^P*$mPCTJ<1^~Ph=HCQwJ!%+_BR=5G18?e_uVcw2-;8BjZ(7e7S4H)=R?VY zG`33KT<>|Pa%4B!Ex<6I>v|1=$EmTOg&f7pc0@gH^S=H@gw46Vdh;(by?RqU8!J6$CEnXa@$PPUw-dB9W$E@?sS#Cc zlTCAbSoMVFp5Y~#V^64fKA&@3;Ek>~maMDx0)4WLH)Vli&vckonL?PQ$JFa_(=7*Y zHw9;h)k#l{CqlBEc(2zOjWVyar{J=fI_GN+%WUbS38kBcHrKf$l8R4g!?*G@Cp10AtoOH>j-FP1>OJu_ZhoK(7>1?9O%=61>a&N>A zjUi2O3MD!A27xG&juMe=hY*yo1|O$v5R;^`LetITGpu)Yz*Fppy(Q`;7PwR)S}~${ z=E8~DY{^I3v|iPB&UW;~HA<0Dn^dC1Y*8t5`j+qOVegAY3}zoBdP+`09lxJ?Q zClNN>!P9jtzBjiYeZ^3css1!|tr*c-lTvONbuNq8JZ}}`SR@&wI@vnN&`~~d_%lu2 z&~lZA_IQ|Xw+4K#BJpyW*9MwCRb|dp6}Qtpl9%jYQ{@;SdJuK_;!&qn(pJ$jeH3N4 zv6`s&Pq8oD;oWmQz$)omGSzk34mT4!JNhEh&nkNE?BFlrUz=c^>ND#P5-HrLwRb65 zee1>54ajya7PsM4`j`#!qtuR4!@nO5A}YgwJ*q?)SMJckYAPQ}2FYn%97T!_+g4&* z_=NFiLATTRQiwWo&RG;!m3dc%3QuHp49oCsb#cp;N9T`YmdaU)OPAO>SBs%D?I13G z8dNP+>{d6Wn`JTRRPKZn5sZXM4OJjUoCunYbFilTOj)FqmdE1kIjPRGvrQ>=)qS z>-q#0r!Q5)?}r>KD9Jc$AgV~gr%0ntHTp>Oxm3rYVwOvIoZ~LwvvC&(R`BIst-b3= z^N~2s=cy?|=HxiW>ww->r6wz$pD}wU^r!A1idMX}P-mZH4rgJwzMZfg)#^oHiBoW_ zbdyoc8%iJI3-uq4LI_=DO-mH6Xk+yXnMXf;RegbW$wba$%v5ZW4FG(q-@Q&W`RRn8 zFn@@~p=PK-oXPYOaENr~HODO4w5+Ck6L6?rNzGxSOIk;bCzYmx8J%)FgM^ zP$ON{3;Z!O(3X!nW=EPKl*+ki$@x*+M5t!YQTLcsqoy@e>(O$L+LlgyyqKV+LSybB zOR3E_G}OkdvMyMsCBHK7Uar7b5i_WfOmo@7UoCG}5QlKGB?eY}6*P{Q(hJ(RV8)Fd zAeV_tk{1cZH46d`jIdCJN?8@d>!cV{uw~*julUUkC^}JwbSeIx zh%;}iGEs_Fj<#L1`kOSuM3obwn#So1c77`f^Q0M3ErmlW&w#~K;ZdO5KeC23B~&qt3uUpzWD5;pi`x6jT}Qjrp4~2QOhKNC zbnX@kSpde0Mt=-Mw`Wf4sz)udkkJz9_9-y;3OQ{_$@?OMX&kbqyF)n2AS-JowMe-H z@AD%9vS9YO(0R{D+a*TF7wMygay$E-vJJ(-o?=LmE7X+Z|cY|r%*gjv&7bac9xNtFQsPjE;-f; z>O?iaKjUzF_9V%~Aj4}?#{z#WwC-^aJU@ptBYZ5Z4smz2=g{8@o+dwd(ckLbvJf*Q zpSa72*ps;sJaZJ!TvV@I4dRz3~*eBZ%w6u0KBLK3fLt>PR=Nv0g zx#W_CM$46aXH_RGe-jd`4XLngIF$5*)1|ngTut$FCh{&)zO_t*leLhgug70DtG1{* zO>m%TKmEx~4mKtNgdZ7_ZHB(aRF9uwS2YAtcU~^+ux&8`V=p1Hi5DSSO*uN&LIm;z z4!0bP7F|P=cZ6{Ld>IZ85v=Auoi3GM-6Av@z4u0a@nHh+MlecK)qAHn{|2#GIlASN zobdn*PL%gF!!MM!r8TJf?}bcP&-Jc1w!%_y7P9tGllba|_H2N8pzNEE1>*A*-3R7n zKN!*&sDR8Xu&&|7N_g!?0+X#!TMqJbl$MQCSZ#wVJH`=D(?q=u+E*%PmLP`IqbB3goOq zqf|SOU8{(e7*$+)!7WR_S6C_Gcr7gNFBy5*5rE}Jaqx&h-wNq6eF67~R;G{?HjUQb zsXaSnHz$Rl#4ryoJ_$0;1|2^go`1$~5+N_gbvO+QQQ%xt18Z?>w4R8;RTKVTi zMHA(z%bJhSElA3?ZSUkSH2i7%;7>(h0?gnNMxKR}WUyJ=N*tXw%R{x}#R*i8QJ!|p zspl4c&S_F10b8R$y%V~x)D1@Y_CDL^!M|A3R_0(0Rk2SHBC!IRl5PWQ zxN!(77`w(3jS5=4;6_5moai!;+UA(3Za4Z8jO|cC>i%B3hym2alFVs8S24ZOTD%0~ zNN#eeZp*$}IR5514Q&ywOhwKP#ykQ4u>m8pa;{@`*eM@zM6fJ7j4CLG(3vhzj`_TnZa+t?1Q!Y5i0kx@V`iZbZMZN%@cHl1*v)Gwi> z6q0&}E1C$q&Cr#YJ7;&2B`;8j;VZM`i%2&%xkbwi@^y;@({Z>QD&VQ7;X^NaOfx$(#^4%0kFdxn`sy zk8jemm&nX-B&b_peAY{lj};KBj~44qr{nZ81h`Rd8|8~79#LqEHrLT+&f19uC`1Ll zl(IHf>6*ug3WQq9FJr%s?gw1Tr&u_>xPHtq{Q0ntyExd=FMhFyAK-sQe^`DHVH+qA zkPg)UAo^MUAEV!(^8a)6Px19E5wEcnbB=kYiAiEIY62ZQaU=Bd((%g-3g8e^ArY?g z@G#@~PWffx3GiM-GNBOqFFn*Tm$b(k>#9_da|MP<>QD(T5ACVg8gEmIm2s>5;gC*{ z)Ip&aj&-Qr2?919hj|7Xt}$EiKpX~RO6#fdDCBO!c&F#77z38Ry2mPd-Bb7KdOQKC zJqZDUD~^CJ+gzfr?D}&vbMCFUK1)<=OzS#Zt*rS=4dbS~f&AQa)mz{aB6-81e|jDb z_k<4Oc&?#7d#ANt|ClV$zTWptmp+u zq&>~lZG3V`7^_L6Ss+^IJWIy%45W!B!3A76en3OQWpF(wAwd~^&uG$Vev=pUQ1FNo zZ5m?B3?t>`PH&un6ADBv&{GE?h^kbg3FFTuo*z!L7heL&WfuKa#a9y>&@7vYBg#br z&EWYa;s;iXW-xI7-<&rah5P<30=P%N86s5$nylRo z`QoM{14_qzy5{t$AzREyo5E#NKnIN1mEL>-{Q@?S-luqrIf$T6!XReDd&Xg7(LEM) z^|n4uJ7F|_+j;w@4X^7kmFYco^p56iU z*TflVMvO1-aal30Zuvc1^vJc9ho{dCN$3#^c$fgEB~Pt;K`A!J4|#+&#y)ZVpYu8k zX>)$6Pg0gCLOOL4|9QoDDkcN)?pm%fh!9o@GOB@&) z8;;F)=}8ms?~+y|o49VQyjL0t;UN%xiV}#GeH3dC$9BXI`j3z@I&q`@4Rz%07PxA> z!_#XqjavxV$jxc$RkSOctdd1m*@k)2$pjo8WstNBX~*4lcev-o(DL7Y!_tmn32A#}h1y1lTd|Yb7h?o{^|?4i_=j&U(qkD1>=kw2F?FDLQ8;sswt9 z@1x%?fCnsva{`<$!XPGi-U5V|Ln17>55K7U{~$nUPidh0e~JLs|BZnEje!3@5Rl&` zTUg(Tt*5v95l-|b(@&%zhdq0d{kI%qrdRS{L$poevVe~5!I?fR{In+5+8E8AXEXFZ|hD+lw>g0i=s}3>LGqY-hf%q^v_aOOX2q5{tRd)WPmHk(J=ijPXbnGPb03)L4i?3)} zx8#M8NQN7UP^%I@A?YIP8jr-m#m%%>{ocUwRJU{Jq6-3dBF>0O*P+59G$hwqvSY@e zCr6F@Tyy0~aN}5U0yzpY6+69SniD)=Bq3`qQp86==b3Bp#>>^SC^K{+bB#)K^sPFAgZGAiNG1)1f0zEC{Ks$?3sYNDhJW|}sb;C`I%P5<`rc5nc2OJqf zl(N`*0ck=ikbngx75QG^c}46LvY$zIK-%=hEwMCf141P#?R&qPD$!<^pH_A9DAcI* z#)^-1Hqea{(3w)t7T&#kd(t!|EQyVYp~q8JQm8Jxo=zfZT}GTj`*bV7HMP=LjzNq? z`(>6)6`7%|B^8fF2AiZDb>hwQror88xraFS7p%T2jX(d8?k8=dKQfssv6G^qhy=ML z-zjNIB;c~{nDhJjHz7d8>TKT4)!OeG?scqKm3~$m{En`GQ&Gt+TbC1_d+GZ7dGM?B zyRgE=nS}aLWN4vbdUT+Yrt23Z?+Dq+;(?aH=U$9M55X(Z`a?V1F6JmPdV|BPIZ&|D z+B`?gjX3?2whh7(EJfQP<#QHr&*)9`EooUVXxHlgSA@Tz@;#w?(2j~P?D#9T&M83Ft=eBTyP*-N~EGU1(7Wz)U!wu^1ZzTff5~us(vK1h< zkkCfj_?;A5B2;g?@V(5)Lhc;14I#4l5Rk^J4eQMy^RK7ru`tyd%NBA@3~hgnaI+#M z8DZsBqw&Cad}rf=^S)pNuw$KX#qEDCVmcjMRBgF>iI-jc`$FCe+Wpr_@zv1%hrcnW zhx(p}X<0C}LLGY-rrzZ&MLX)PER{nhgqFFzLNF4Z(|Q>WM$S1pJ3+1q>0OQC3Tv=J ziItWSP%%#rgVy^+>c+U~(~NV8H>N!4u&+%~G9csMlTlt}O^PUyXloue6QG~t6{CYW zMK*Y79=4hYy%oxpRpDapJgl96WK=I=!Y3rA;HN2SHof`K=rg`rJN6wxVK}q$%`LDQ z%4(671~$R2$uLjIz*L!=5s|1qlkz*(Z8fQUq*YcfrygAkX-_j$C2nzNEcv^I+zfHq z8-66S+I?2O_6BJiNCOJG49-gGR^nu8a`2-{lWZ5B#%mvqF(Iz-yOxkB0LqVnp31VE zxmVSTtBbq4BD_$Il2Feu$a1f0XCW~9#k?=ST|6#t_uL`yzi{dst9<`mEOyIJSHSuw zt5Eqz&HG;~$*BK{EN2TtCsPw;7bi+-jU9`duKpg(#3Jf$LMNdHy6I8B6Z2Rdou6BHFBubKN>#UZ z1^@h*XgGx8cemDdk6Qn=;E^`WIu&tJSKV&va7+>GGL>H^Y^xdPpkBhM^}~emJ~BIX zX=j4UnRBcz0wy<)PW5dIJ8OA)F}enpAA*7ym&Re06r)P2;Oi9<4J}TWdF(YRSzIbw zLi`lmo!C`e9cli>-9TnR6t+ftRHmj`+hp7xFC*ISSzD8e9*hyURF0_U^_w2eSz5}{ zf`$|_ycOr@y4lpC(qXp zUMU7&UyD^;S6-kGalJdUq^@hZ@_qHuw{FJA&i-&8uXZW#f8b6RZ)pq65A(8qu}@o{ z9vGX0%y!0Fwo5adR2-YD_x}6BC?eSqk@62MG5%W#_x~iOt*MEnA%mr@p}8p&y@Q>3 zHNaFkOLp-Yiq`IrwF5;Pgf1jLvP=BlwVZ>yJEXcpAbkS|n7izpLH{;gTW@>iPVViL zjJ*3(YtGsE;opjMi)H!yHO!-bBnAT->ant)MdhC%T)5+1IcmPiclrD@-?&s@y&v`m zZYkv-&$(7Q>SiCWtRwF@+U$>+Y@4G8 z(MSl<8!dfPNXYFkOxyiD0)B+lEX^#PNAeiyU1-bCR-ee)m5j-T{37k$0l!z=@Qdjl?n1r0U2U`{ow^Q>o{nywIooNP?)g`z*UqfzHh$<>J^GlW(Z`=Y z4)BOiG5c~#?j(&tK#nowr5~Qxg4JJ9u_P$jq4>O1Vt1^XXjH*lk&2qin8<-u808}} z+Wx{(!)O~Y;fUMl1B3M`CbjmSv)F+g*OzL5B+g` zMxYvN7%(7F^QV~2CVr1ra&A2KF8^rvPSDbB;=C@Lv+R#6+jqbHaE*7>Z6|SeNM(1J z(%)FGeK^ccoKk?S7LyBn>%}y;qCd)-uuM&Mou5b_c)vCkn!dTAMK+fep)p~D4`k69 z#A8V?E$3+A#Y{dj4LtoKYufvPwLDkP9-?;k%T9qxhQ0hY4A$kB`iFhPq&sPrHMlAB zO6(wR6GkT$sSj01wJ~lV1>*e}W*Xb2ad6Hva8%HYQAA!x&5ReJ>}L*tiP8D&B zI3|aoYL%@dAASw1;iEN@&fQov+S5wL^L><{1#~z%Cx)Q$D@{z7%C(;1Mrr(w{KB-0 zhLYQHU~5KS^=oq%@C(tQKkj)P7ezwxGag+E=Clk7OFa=JC(P^rSWwz+|K*v#or$0T zG%36CnlH2!2$H=J$-4L_dQ2UV1hT1ibdK?niJOuYTam&3iz@380naM6b7pNaMGiVu zZiGZXmAs!63uRHgR(1x)LRp}qbmwIE(9d0okWP}T?T*2WuWxHK4405h&_a-e;MJ(a zjN(ftAK*a2NAj-4e!fXJd=q`~QR$$8#`j})kU&s5D<17YZAR)fD6`mQ927F;L`Edc z26Wxob!QQ8bHcQrmYA8zIH^J2$*<FTxB}3}(FMeXSaMbtKYtgU&xxPstS60N zYvVmw zjyYA9!*Eo~$F~bPKu8`@g71LW}bz1`VsQf!(d@pC&YfjG?0UtgqK zVyLKaZZ92p?KF}!l6h9$if;PZoVhIZFGPNH9vg1{L=A@Jil;sJ*0f|>YIb=WGO29p zN$PlggeBZf=C!%5AELucbQ6xT^(tl4VZE{A8?r0<`L_j5W;U#TqKcDy8@GK12{vKb z>WpT7lJqONg(D6NG_XQ!J0FUb_S$^J`GN}`c;_zTnHp&X=s`|Pv%A~fVFps{Uql9s z1;)>W=`rI?!*D|edM%s~5C^_aFGA`kTQZQChd5lA47cPS*p3?Y=dQ6`o1WH`{$0Ls zPrRC9uYK?3hL}KbKPoxR?CtUa#7*q*5bXIf2{^qmAkso}qOT=lcB<2~*Fv!hld%;C zolU719)oIoSEv5n>);{A$Jav2L!3|ka=UF3AGtn}T^=^jF+n0D`W-v!I6z*0=yOup znyA6Ittf*!{u>P^6vQcujdGE$a@xh3f|!4*;7Wm`cPCu}eji6*x24WuDw9yC7`I&xfAe5BqnCXtf52 zMLlcY6mVwv3w~FPje7P)Rf%X%azg~~oc>|he1u;qKB_gbs~XT zk2uosX;|p<8f~4?m09%8QsLfoAI&C23TP;;ksE}8b{ToAse{#5^NI`C%r;O3Tv(*5 z>+Ev~NF3oz#k&gQe)iQx!n1|l2QmHr1udX1m64Oosc}VgZ1T|eVyNtx+-zy*lpVzj z@-;Dhkth2Wu}WFO^1UkLMUCLDl(`9|0+*W?>kNF1etlwjgw>5|FgGc8)cD zBzw3Km7$Qy?95|;x~I?ndizD(EjhL-JC3RLNt3!=gPEBwVkhDjGZsF_X^tj5s^`24 z-cLb9D$V~@Wmu>BI<3aNODE5uRci;%j1jS=&L)m=>jWfpHMfnC1gZhv0lc z_OTFhsK}L!l@ym!Q%jI%)oBv8T1^w}wvo5H#nY^sCy(DSjr((M(_7@kfrjnBzuwq(eUXw$&f!d8)EV?A|{IdX=s+ ztbms&T1B;|Rv90UW^aA#0Uxr~wz#?Zd`jKI4b|9Z*Qez>~|V)_0J+998msQJ-rda`|;e!9tk z1MQw3l5G?2=*>YFpU|y#OC=-svL|eLTT#j#dGvT+zg)bFam<5TptTp7p1!feGW?{V z7En%-0@z&lWVI$_p1w4;77JH%Qr*m=1&rzlU3$ww@>z#-t>Cz+3DiZqTB+Lb*SIjt z>HWUaxr}--$xz6J_b8dvsH-Y-39L&wIg! zcJle!dv@xzNLi=RoL)ZFh?E&x^+~qQWQ*$}0==8^9G(!Y+K}yozc_ssEaW zCsNCyQ&}^h@3n z8?t^5_;q>@E?d!b1mlq6ix)QgTk3wvCVsf5drnmH7fX02wF37I2arWf7h&q_8hG7{ zf}<|EDK*$ShRPJiHr|LWRiITb3I_|Y^xgUfDhMDO9(GfcZk)}HGIYZ_N73qo>2@%m z%)fMI&>Ot6z%s|~!%nD&&WFUiD_eM(|I|GChjKPJN70>Ly<8L)tvAlPON5r^XJH-KCg4u@s!u z>r!`?h>Q~}l7Kc%m!pnFcFa=s+*!78-`t-=lC=%z?-dhr zhnP!pT0}+7G%k&KhIMuZ

)R0GbBxSbcrfUNW`&X!Tzh<3`v?shYfp$ zge}5>^VJR}6cRZdQtN71LTa887IwEQjMu-7EdNYw_s5Y`AsCD*S@Ch@W~HNu?Wu7y z`~_qx#6>Pv%5RFAVPkIlI9y?3?E!ladDiIX&-Td3dOchp#OfKqR1VcYqBFY!;Rh0(;Zu z2m0F>vtw>6z92&3jP)>(hCecZCr2zQ^{!q6 zbJ?uaZAk8tF@7Osm<<=w~gIb!uGyxE>?}L6`CjV435fuP)j`kSTOIULHhOBDN_Xi z&(Z1E%Wy`n2RZMG{W>y@URG zpTZnj4nNJwL-f|&umNkr{&OkOJ|VY>_3FYN`McNDf*so3b@P;}P&J@#UncKPV5`WE zozS>GlN-;i801+Q&DRJi85xPfz-V>i-G(h%kbKJoY}*}d=Fbt-Nf4izVgoL;tP8|M zUb$S~PV)C-owO#|HGf?Q6fBp2*ot*du%^64d*O4KA8WaWme$1UG6&YyK4=B|K>@=q zVKS@5sT6zZ;`K69CP_+BaqILQ38zx=J^cX`lVUe1Bn(R%GlQu8_5v*r;cQhK=(?hD zGT^%jRYab+srplcO3o=#ssLFQR7^d&p6&*O@|}fzD$@K<%VM_Y4sQjyXCoyTF`SL- zLr?0w7-8+8_(R87bjx^6`>(IJQG&Z)V36ONKe!9!Ak&*)AE;N`Q}YKS$B(s!E6@%_ zQ>mjT{L>1(DafQd@~k)B-PK?*271XR6ss4d%#6YV&zBDq*U>%A4e^&P~H@ z-0Mbio;=FabGvwh?xGN}@e^K@AuW=9Z6tw)@hPEy8&x+hV*^J)dlQ>Q-f|JwG||

?Kr1ipz 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