""" ############################################################################################# # # # Script pour convertir des données topographiques au format .th .mak et .dat de compass # # au format th et th2 de Therion # # By Alexandre PONT (alexandre_pont@yahoo.fr) # # # # Définir les différentes variables dans fichier config.ini # # # # Usage : python pyCreateTh.py # # Commandes : pyCreateTh.py --help # # # ############################################################################################# Création Alex the 2025 06 09 : Version 2025 06 16 : Création fonction createThFolders Ajout des fonctions pour mettre en log Création de la fonction readMakFile A venir : - gérer les visées orphelines dans une même survey - grouper les surveys même dates et même auteur si elles ont des points communs - blocage compilation therion si erreur - gérer les updates des th2 files (th, dat, mak) - gérer les config.th pour en avoir qu'un par export """ Version ="2025.06.16" ################################################################################################# ################################################################################################# import os from os.path import isfile, join, abspath, splitext import sys import re import pandas as pd pd.set_option('future.no_silent_downcasting', True) import unicodedata import argparse import shutil from datetime import datetime import configparser import tkinter as tk from tkinter import filedialog from collections import defaultdict from charset_normalizer import from_path from Lib.survey import SurveyLoader, NoSurveysFoundException from Lib.therion import compile_template, Colors, compile_file from Lib.logger_config import setup_logger ################################################################################################# ## [Survey_Data] default values Author = "Created by pyCreateTh.py" Copyright = "# Copyright (C) pyCreateTh.py" Copyright_Short = "Licence (C) pyCreateTh.py" map_comment = "Created by pyCreateTh.py" cs = "UTM30" club = "Therion" thanksto = "Therion" datat = "https://therion.speleo.sk/" wpage = "https://therion.speleo.sk/" ## [Application_data] default values template_path = "./Template" station_by_scrap = 20 final_therion_exe = True therion_path = "C:/Therion/therion.exe" LINES = -1 NAMES = -1 configIni = "config.ini" # Default config file name debug_log = False # Mode debug des messages error_count = 0 # Compteur d'erreurs ################################################################################################# def sanitize_filename(th_name): """ Cleans a string to make it compatible with filenames on Windows, Linux, and macOS. Replaces special and accented characters with compatible characters. Args: th_name (str): The filename to clean. Returns: str: The cleaned and compatible string. """ # Unicode normalization to replace accented characters with their non-accented equivalents th_name = unicodedata.normalize('NFKD', th_name).encode('ASCII', 'ignore').decode('ASCII') # Replace illegal characters with an underscore (_) th_name = re.sub(r'[<>:"/\\|?*\']', '_', th_name) # Characters not allowed on Windows th_name = re.sub(r'[\s]', '_', th_name) # Replace spaces with underscores th_name = re.sub(r'[^a-zA-Z0-9._-]', '_', th_name) # Keep letters, digits, . _ - # Ensure the name is not empty or just underscores return th_name or "default_filename" ################################################################################################# def colored_help(parser): # Captures the help output help_text = parser.format_help() # Coloration des différentes parties colored_help_text = help_text.replace( 'usage:', f'{Colors.ERROR}usage:{Colors.ENDC}' ).replace( 'options:', f'{Colors.GREEN}options:{Colors.ENDC}' ).replace('positional arguments:', f'{Colors.BLUE}positional arguments:{Colors.ENDC}' ).replace(', --help', f'{Colors.BLUE}, --help:{Colors.ENDC}' ).replace('elp:', f'{Colors.BLUE}elp{Colors.ENDC}') # Surligner les arguments for action in parser._actions: if action.option_strings: # Colorer les options (--xyz) for opt in action.option_strings: colored_help_text = colored_help_text.replace(opt, f'{Colors.BLUE}{opt}{Colors.ENDC}').replace('--help', f'{Colors.BLUE}--help:{Colors.ENDC}') # Imprimer le texte coloré print(colored_help_text) sys.exit(1) ################################################################################################# def read_config(config_file): global Author global Copyright global Copyright_Short global map_comment global club global thanksto global datat global wpage global cs global template_path global station_by_scrap global final_therion_exe global therion_path global LINES global NAMES # Initialize the configparser to read .ini files config = configparser.ConfigParser() config.read(config_file, encoding="utf-8") if 'Survey_Data' in config and 'Author' in config['Survey_Data']: Author = config['Survey_Data']['Author'] if 'Survey_Data' in config and 'Copyright1' in config['Survey_Data']: Copyright = config['Survey_Data']['Copyright1'] + "\n" + config['Survey_Data']['Copyright2'] + "\n" + config['Survey_Data']['Copyright3'] + "\n" if 'Survey_Data' in config and 'Copyright_Short' in config['Survey_Data']: Copyright_Short = config['Survey_Data']['Copyright_Short'] if 'Survey_Data' in config and 'map_comment' in config['Survey_Data']: map_comment = config['Survey_Data']['map_comment'] if 'Survey_Data' in config and 'club' in config['Survey_Data']: club = config['Survey_Data']['club'] if 'Survey_Data' in config and 'thanksto' in config['Survey_Data']: thanksto = config['Survey_Data']['thanksto'] if 'Survey_Data' in config and 'datat' in config['Survey_Data']: datat = config['Survey_Data']['datat'] if 'Survey_Data' in config and 'wpage' in config['Survey_Data']: wpage = config['Survey_Data']['wpage'] if 'Survey_Data' in config and 'cs' in config['Survey_Data']: cs = config['Survey_Data']['cs'] if 'Application_Data' in config and 'template_path' in config['Application_Data']: template_path = config['Application_Data']['template_path'] if 'Application_Data' in config and 'station_by_scrap' in config['Application_Data']: station_by_scrap = int(config['Application_Data']['station_by_scrap']) if 'Application_Data' in config and 'final_therion_exe' in config['Application_Data']: final_therion_exe = bool(config['Application_Data']['final_therion_exe']) if 'Application_Data' in config and 'therion_path' in config['Application_Data']: therion_path = config['Application_Data']['therion_path'] if LINES == -1 : if 'Application_Data' in config and 'shot_lines_in_th2_files' in config['Application_Data']: LINES = 0 if config['Application_Data']['shot_lines_in_th2_files'] == "False" else 1 if NAMES == -1 : if 'Application_Data' in config and 'station_name_in_th2_files' in config['Application_Data']: NAMES = 0 if config['Application_Data']['station_name_in_th2_files'] == "False" else 1 ################################################################################################# def copy_template_if_not_exists(template_path, destination_path): # Check if the destination folder exists try: if not os.path.exists(destination_path): # If the destination folder does not exist, copy the template shutil.copytree(template_path, destination_path) log.info(f"The folder '{Colors.ENDC}{template_path}{Colors.GREEN}' has been copied to '{Colors.ENDC}~\\{os.path.relpath(destination_path)}") else: log.warning(f"The folder '{Colors.ENDC}~\\{os.path.relpath(destination_path)}{Colors.WARNING}' already exists. No files were copied.") except Exception as e: log.critical(f"Copy template error: {Colors.ENDC}{e}") exit(0) ################################################################################################# def add_copyright_header(file_path, copyright_text): # Lire le contenu du fichier with open(file_path, 'r', encoding="utf-8") as file: content = file.readlines() # Vérifier si le copyright est déjà présent if not any("copyright" in line.lower() for line in content): # Ajouter le copyright en en-tête content.insert(0, f"{copyright_text}\n") # Réécrire le fichier avec le copyright ajouté with open(file_path, 'w', encoding="utf-8") as file: file.writelines(content) ################################################################################################# def copy_file_with_copyright(th_file, destination_path, copyright_text): global error_count # Vérifier si le fichier existe if os.path.exists(th_file): # Créer le dossier de destination s'il n'existe pas os.makedirs(destination_path, exist_ok=True) # Copier le fichier vers le dossier de destination dest_file = os.path.join(destination_path, os.path.basename(th_file)) shutil.copy(th_file, dest_file) # Ajouter le copyright dans l'en-tête si nécessaire add_copyright_header(dest_file, copyright_text) log.debug(f"File '{Colors.ENDC}~\\{os.path.relpath(th_file)}{Colors.GREEN}' has been copied to '{Colors.ENDC}~\\{os.path.relpath(destination_path)}{Colors.GREEN}' with the copyright header added.{Colors.ENDC}") else: log.error(f"The file .th does not exist {Colors.ENDC}~\\{os.path.relpath(th_file)}") error_count += 1 ################################################################################################# def process_template(template_path, variables, output_path): """ Process a Therion template file by replacing variables. Args: template_path (str): Path to the original template file variables (dict): Dictionary of variables to replace output_path (str): Path for the new configuration file """ global error_count try: # Read the content of the template file with open(template_path, 'r', encoding='utf-8') as file: content = file.read() # Replace variables for var, value in variables.items(): # Use regex to replace {variable} with its value pattern = r'\{' + re.escape(var) + r'\}' content = re.sub(pattern, str(value), content) # Write the new file with open(output_path, 'w', encoding='utf-8') as file: file.write(content) log.info(f"Update template successfully: {Colors.ENDC}~\\{os.path.relpath(output_path)}") # Delete the original template file os.remove(template_path) except FileNotFoundError: log.error(f"Template file {Colors.ENDC}{template_path}{Colors.ERROR} not found") error_count += 1 except PermissionError: log.error(f"Insufficient permissions to write the file") error_count += 1 except Exception as e: log.error(f"An error occurred (process_template): {Colors.ENDC}{e}") error_count += 1 ################################################################################################# def parse_therion_surveys(file_path): """ Reads a Therion file and extracts survey names. Args: file_path (str): Path to the Therion file to parse Returns: list: List of survey names """ global error_count survey_names = [] try: with open(file_path, 'r', encoding='utf-8') as file: # Read all lines from the file lines = file.readlines() for line in lines: # Look for lines starting with survey line = line.strip() if line.startswith('survey ') and ' -title ' in line: # Split the line and extract the survey name start_index = line.find('survey ') + len('survey ') end_index = line.find(' -title ') survey_name = line[start_index:end_index].strip() survey_names.append(survey_name) except FileNotFoundError: log.error(f"File {Colors.ENDC}~\\{os.path.relpath(file_path)}{Colors.ERROR} not found.{Colors.ENDC}") error_count += 1 except PermissionError: log.error(f"Insufficient permissions to read {Colors.ENDC}~\\{os.path.relpath(file_path)}") error_count += 1 except Exception as e: log.error(f"An error occurred (parse_therion_surveys): {Colors.ENDC}{e}{Colors.ERROR}, file: {Colors.ENDC}~\\{os.path.relpath(file_path)}") error_count += 1 return survey_names ################################################################################################# def str_to_bool(value): """ Function to convert string to boolean """ if isinstance(value, bool): return value if value.lower() in ('true', '1', 'yes', 'y'): return True elif value.lower() in ('false', '0', 'no', 'n'): return False else: raise argparse.ArgumentTypeError(f"{Colors.ERROR}Error: Invalid boolean value: {Colors.ENDC}{value}") ################################################################################################# def select_file(): # Créer une instance de la fenêtre tkinter root = tk.Tk() # Cacher la fenêtre principale root.withdraw() # Afficher la boite de dialogue de sélection de fichier file_path = filedialog.askopenfilename( title="Select your file", filetypes=[("Compatibles files", "*.th *.mak *.dat"), ("TH files", "*.th"), ("DAT files", "*.dat"), ("MAK files", "*.mak"),("All files", "*.*")] ) # Retourner le chemin complet du fichier sélectionné return file_path ################################################################################################# # Création des dossiers à partir d'un th file # ################################################################################################# def createThFolders(ENTRY_FILE, PROJECTION = "Plan", TARGET = "None", FORMAT = "th2", SCALE = "500", UPDATE = "") : #""" # Entrées : # ENTRY_FILE : input th file # PROJECTION : Plan ou Extended (Extended, not yet implanted) # TARGET : Scrap name if different from 'ENTRY_FILE' name # FORMAT : Output format, either th2 for producing skeleton for drawing or plt for visualizing in aven/loch", default="th2" # SCALE : Scale for the th2 exports, default="500" # UPDATE : Mode update, option th2" update only th2 files, default="" update all data # # Sorties : # Création des dossiers nécessaires d'après dossier 'template' # Création des fichiers nécessaires : th, th2, -tot.th # Création des scrap avec stations topo # #""" global Author global Copyright global Copyright_Short global map_comment global club global thanksto global datat global wpage global cs global template_path global station_by_scrap global final_therion_exe global therion_path global LINES global NAMES # log = logging.getLogger("Logger") TH_NAME = sanitize_filename(os.path.splitext(os.path.basename(ENTRY_FILE))[0]) # DEST_PATH = os.path.dirname(args.survey_file) + "/" + TH_NAME DEST_PATH = os.path.dirname(ENTRY_FILE) + "/" + TH_NAME ABS_PATH = os.path.dirname(ENTRY_FILE) log.debug(f"ENTRY_FILE: {ENTRY_FILE}") log.debug(f"PROJECTION: {PROJECTION}") log.debug(f"TARGET: {TARGET}") log.debug(f"FORMAT: {FORMAT}") log.debug(f"SCALE: {SCALE}") log.debug(f"TH_NAME: {TH_NAME}") log.debug(f"DEST_PATH: {DEST_PATH}") log.debug(f"ABS_PATH: {ABS_PATH}") if PROJECTION.lower() != "plan" : log.critical(f"Sorry, projection '{Colors.ENDC}{PROJECTION}{Colors.ERROR}' not yet implemented{Colors.ENDC}") exit(1) if not os.path.isfile(ENTRY_FILE): log.critical(f"The Therion file didn't exist: {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") exit(1) if FORMAT not in ["th2", "plt"]: log.critical(f"Please choose a supported format: th2, plt{Colors.ENDC}") exit(1) # Normalise name, namespace, key, file path log.info(f"Parsing survey entry file:\t{Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") survey_list = parse_therion_surveys(ENTRY_FILE) # print(survey_list) if TARGET == "None" : if len(survey_list) > 1 : log.critical(f"Multiple surveys were found, not yet implemented{Colors.ENDC}") exit(1) TARGET = sanitize_filename(survey_list[0]) log.info(f"Parsing survey target:\t{Colors.ENDC}{TARGET}") loader = SurveyLoader(ENTRY_FILE) survey = loader.get_survey_by_id(survey_list[0]) # print(survey.name) if not survey: raise NoSurveysFoundException(f"{Colors.ERROR}No survey found with that selector{Colors.ENDC}") if UPDATE == "th2": log.info(f" Update th2 files {Colors.ENDC}") log.info(f"\t{Colors.BLUE}survey_file : {Colors.ENDC} {args.survey_file}") log.info(f"\t{Colors.BLUE}ENTRY_FILE: {Colors.ENDC} {ENTRY_FILE}") log.info(f"\t{Colors.BLUE}PROJECTION: {Colors.ENDC} {PROJECTION}") log.info(f"\t{Colors.BLUE}TARGET: {Colors.ENDC} {TARGET}") # log.info(f"\t{Colors.BLUE}OUTPUT: {Colors.ENDC} {OUTPUT}") log.info(f"\t{Colors.BLUE}FORMAT: {Colors.ENDC} {FORMAT}") log.info(f"\t{Colors.BLUE}SCALE: {Colors.ENDC} {SCALE}") log.info(f"\t{Colors.BLUE}TH_NAME: {Colors.ENDC} {TH_NAME}") DEST_PATH = os.path.dirname(args.survey_file) log.info(f"\t{Colors.BLUE}DEST_PATH: {Colors.ENDC} {DEST_PATH}") log.info(f"\t{Colors.BLUE}ABS_PATH: {Colors.ENDC} {ABS_PATH}") ################################################################################################# # Copy template folders # ################################################################################################# if UPDATE == "": log.debug(f"Copy template folder and adapte it") copy_template_if_not_exists(template_path, DEST_PATH) copy_file_with_copyright(ENTRY_FILE, DEST_PATH + "/Data", Copyright) totdata = f"""\tinput Data/{TH_NAME}.th \t## Pour le plan \tinput Data/{TH_NAME}-Plan.th2 \t## Pour la coupe développée \tinput Data/{TH_NAME}-Extended.th2 \t## Appel des maps \tinput {TH_NAME}-maps.th """ # Adapte templates config_vars = { 'fileName': TH_NAME, 'cavename': TH_NAME.replace("_", " "), 'Author': Author, 'Copyright': Copyright, 'Scale' : SCALE, 'Target' : TARGET, 'map_comment' : map_comment, 'club' : club, 'thanksto' : thanksto.replace("_", r"\_"), 'datat' : datat.replace("_", r"\_"), 'wpage' : wpage.replace("_", r"\_"), 'cs' : cs, 'totData' : totdata, 'other_scraps_plan' : "", 'file_info' : f'# File generated by pyCreateTh.py (version {Version}) date: {datetime.now().strftime("%Y.%m.%d %H:%M:%S")}', } process_template(DEST_PATH + '/template.thconfig', config_vars, DEST_PATH + '/' + TH_NAME + '.thconfig') process_template(DEST_PATH + '/template-tot.th', config_vars, DEST_PATH + '/' + TH_NAME + '-tot.th') process_template(DEST_PATH + '/template-readme.md', config_vars, DEST_PATH + '/readme.md') ################################################################################################# # Produce the parsable XVI file # ################################################################################################# log.info(f"Compiling 2D XVI file: \t{Colors.ENDC}{TH_NAME}") template = """source "{th_file}" layout minimal scale 1 {scale} endlayout select {selector} export model -o "{th_name}.lox" export map -projection plan -o "{th_name}-Plan.xvi" -layout minimal -layout-debug station-names export map -projection extended -o "{th_name}-Extended.xvi" -layout minimal -layout-debug station-names """ if UPDATE == "th2": template_args = { "th_file": DEST_PATH + "/" + TH_NAME + ".th", "selector": survey.therion_id, "th_name": DEST_PATH + "/" + TH_NAME, "scale": SCALE, } else : template_args = { "th_file": DEST_PATH + "/Data/" + TH_NAME + ".th", "selector": survey.therion_id, "th_name": DEST_PATH + "/Data/" + TH_NAME, "scale": SCALE, } logfile, tmpdir = compile_template(template, template_args, cleanup=False, therion_path=therion_path) ################################################################################################# # Parse the Plan XVI file # ################################################################################################# if UPDATE == "th2": th_name_xvi = DEST_PATH + "/" + TH_NAME + "-Plan.xvi" else : th_name_xvi = DEST_PATH + "/Data/" + TH_NAME + "-Plan.xvi" log.info(f"Parsing plan XVI file:\t{Colors.ENDC}~\\{os.path.relpath(th_name_xvi)}") stations = {} lines = [] with open(join(th_name_xvi), "r", encoding="utf-8") as f: xvi_content = f.read() xvi_stations, xvi_shots = xvi_content.split("XVIshots") # Extract all the stations for line in xvi_stations.split("\n"): match = re.search(r"{\s*(-?\d+\.\d+)\s*(-?\d+\.\d+)\s([^@]+)(?:@([^\s}]*))?\s*}", line) if match: x = match.groups()[0] y = match.groups()[1] station_number = match.groups()[2] namespace = match.groups()[3] namespace_array = namespace.split(".") if namespace else [] station = station_number if len(namespace_array) > 1: station = "{}@{}".format(station_number, ".".join(namespace_array[0:-1])) stations["{}.{}".format(x, y)] = [x, y, station] # Extraire les valeurs x et y à partir des listes dans stations x_values = [float(value[0]) for value in stations.values()] y_values = [float(value[1]) for value in stations.values()] # Trouver les min et max de x x_min = float(min(x_values)) x_max = float(max(x_values)) # Trouver les min et max de y y_min = float(min(y_values)) y_max = float(max(y_values)) x_ecart = x_max - x_min y_ecart = y_max - y_min # Afficher les résultats # log.debug("x_min:", x_min, "x_max:", x_max) # log.debug("y_min:", y_min, "y_max:", y_max) # log.debug("Écart max-min pour x:", x_ecart) # log.debug("Écart max-min pour y:", y_ecart) # Extract all the lines for line in xvi_shots.split("\n"): match = re.search(r"^\s*{\s*(-?\d+\.\d+)\s*(-?\d+\.\d+)\s*(-?\d+\.\d+)\s*(-?\d+\.\d+)\s*.*}", line ) if match: x1 = match.groups()[0] y1 = match.groups()[1] x2 = match.groups()[2] y2 = match.groups()[3] key1 = "{}.{}".format(x1, y1) key2 = "{}.{}".format(x2, y2) # Splays won't have stations station1 = stations[key1][2] if key1 in stations else None station2 = stations[key2][2] if key2 in stations else None lines.append([x1, y1, x2, y2, station1, station2]) # shutil.rmtree(tmpdir) if UPDATE == "th2": th2_name = DEST_PATH + "/" + TH_NAME else : th2_name = DEST_PATH + "/Data/" + TH_NAME output_path = f'{th2_name}-{PROJECTION}.{FORMAT}' scrap_to_add = int(len(stations)/station_by_scrap)-1 # log.debug(stations) log.info(f"Writing output to:\t{Colors.ENDC}~\\{os.path.relpath(output_path)}") # Write TH2 if FORMAT == "th2": th2_file_header = """encoding utf-8""" th2_file = """ ##XTHERION## xth_me_area_adjust {X_Min} {Y_Min} {X_Max} {Y_Max} ##XTHERION## xth_me_area_zoom_to 100 ##XTHERION## xth_me_image_insert {insert_XVI} {Copyright} # File generated by pyCreateTh.py version {version} date: {date} # x_min: {X_Min}, x_max: {X_Max} ecart : {X_Max_X_Min} # y_min: {Y_Min}, y_max: {Y_Max} ecart : {Y_Max_Y_Min} scrap S{projection_short}-{name}_01 -station-names "" "@{name}" -projection {projection} -author {year} "{author}" -copyright {year} "{Copyright_Short}" {points} {names} {lines} endscrap""" th2_point = """ point {x} {y} station -name {station}""" th2_name = """ point {x} {y} station-name -align tr -scale xs -text {station}""" th2_line = """ line u:Shot_Survey {x1} {y1} {x2} {y2} endline""" seen = set() th2_lines = [] th2_points = [] th2_names = [] other_scraps_plan = f"\tS{PROJECTION[0].upper()}-{TARGET}_01\n\tbreak\n" for line in lines: th2_lines.append(th2_line.format(x1=line[0], y1=line[1], x2=line[2], y2=line[3])) coords1 = "{}.{}".format(line[0], line[1]) if coords1 not in seen: seen.add(coords1) th2_points.append(th2_point.format(x=line[0], y=line[1], station=line[4])) th2_names.append(th2_name.format(x=line[0], y=line[1], station=line[4])) coords2 = "{}.{}".format(line[2], line[3]) if "{}.{}".format(line[2], line[3]) not in seen: seen.add(coords2) if line[5] != None: th2_points.append(th2_point.format(x=line[2], y=line[3], station=line[5])) th2_names.append(th2_name.format(x=line[2], y=line[3], station=line[5])) if isfile(output_path): log.warning(f"{Colors.ENDC}{os.path.basename(output_path)}{Colors.WARNING} file already exists - nothing done{Colors.ENDC}") else : name = TARGET, log.debug(f"Therion output path :\t{Colors.ENDC}~\\{os.path.relpath(output_path)}") with open(str(output_path), "w+") as f: f.write(th2_file_header) f.write(th2_file.format( name = name[0], Copyright = Copyright, Copyright_Short = Copyright_Short, points="\n".join(th2_points), lines="\n".join(th2_lines) if LINES else "", names="\n".join(th2_names) if NAMES else "", projection=PROJECTION.lower(), projection_short=PROJECTION[0].upper(), author=Author, year=datetime.now().year, version = Version, date=datetime.now().strftime("%Y.%m.%d-%H:%M:%S"), X_Min=x_min*1.2, X_Max=x_max*1.2, Y_Min=y_min*1.2, Y_Max=y_max*1.2, X_Max_X_Min =x_ecart, Y_Max_Y_Min =y_ecart, insert_XVI = "{" + stations[next(iter(stations))][0] + "1 1.0} {" + stations[next(iter(stations))][1] + " " + stations[next(iter(stations))][2] +"} " + os.path.basename(th_name_xvi) + " 0 {}", ) ) if scrap_to_add >= 1 : for i in range(scrap_to_add): other_scraps_plan = other_scraps_plan + f"\tS{PROJECTION[0].upper()}-{name[0]}_{i+2:02}\n\tbreak\n" th2_scrap = """ scrap S{projection_short}-{name}_{num:02} -station-names "" "@{name}" -projection {projection} -author {year} "{author}" -copyright {year} "{Copyright_Short}" endscrap """ f.write(th2_scrap.format( name=name[0], projection=PROJECTION.lower(), projection_short=PROJECTION[0].upper(), author=Author, year=datetime.now().year, Copyright_Short = Copyright_Short, num=f"{i+2:02}", ) ) ################################################################################################# # Parse the Extended XVI file # ################################################################################################# if UPDATE == "th2": th_name_xvi = DEST_PATH + "/" + TH_NAME + "-Extended.xvi" else : th_name_xvi = DEST_PATH + "/Data/" + TH_NAME + "-Extended.xvi" log.info(f"Parsing extended XVI file:\t{Colors.ENDC}~\\{os.path.relpath(th_name_xvi)}") # Parse the Extended XVI file stations = {} lines = [] with open(join(th_name_xvi), "r", encoding="utf-8") as f: xvi_content = f.read() xvi_stations, xvi_shots = xvi_content.split("XVIshots") # Extract all the stations for line in xvi_stations.split("\n"): match = re.search(r"{\s*(-?\d+\.\d+)\s*(-?\d+\.\d+)\s([^@]+)(?:@([^\s}]*))?\s*}", line) if match: x = match.groups()[0] y = match.groups()[1] station_number = match.groups()[2] namespace = match.groups()[3] namespace_array = namespace.split(".") if namespace else [] station = station_number if len(namespace_array) > 1: station = "{}@{}".format(station_number, ".".join(namespace_array[0:-1])) stations["{}.{}".format(x, y)] = [x, y, station] # Extraire les valeurs x et y à partir des listes dans stations x_values = [float(value[0]) for value in stations.values()] y_values = [float(value[1]) for value in stations.values()] # Trouver les min et max de x x_min = float(min(x_values)) x_max = float(max(x_values)) # Trouver les min et max de y y_min = float(min(y_values)) y_max = float(max(y_values)) x_ecart = x_max - x_min y_ecart = y_max - y_min # Afficher les résultats # log.debug("x_min:", x_min, "x_max:", x_max) # log.debug("y_min:", y_min, "y_max:", y_max) # log.debug("Écart max-min pour x:", x_ecart) # log.debug("Écart max-min pour y:", y_ecart) # Extract all the lines for line in xvi_shots.split("\n"): match = re.search(r"^\s*{\s*(-?\d+\.\d+)\s*(-?\d+\.\d+)\s*(-?\d+\.\d+)\s*(-?\d+\.\d+)\s*.*}", line ) if match: x1 = match.groups()[0] y1 = match.groups()[1] x2 = match.groups()[2] y2 = match.groups()[3] key1 = "{}.{}".format(x1, y1) key2 = "{}.{}".format(x2, y2) # Splays won't have stations station1 = stations[key1][2] if key1 in stations else None station2 = stations[key2][2] if key2 in stations else None lines.append([x1, y1, x2, y2, station1, station2]) shutil.rmtree(tmpdir) if UPDATE == "th2": th2_name = DEST_PATH + "/" + TH_NAME else : th2_name = DEST_PATH + "/Data/" + TH_NAME output_path = f'{th2_name}-Extended.{FORMAT}' log.info(f"Writing output to:\t\t{Colors.ENDC}~\\{os.path.relpath(output_path)}") # Write TH2 if FORMAT == "th2": th2_file_header = """encoding utf-8""" th2_file = """ ##XTHERION## xth_me_area_adjust {X_Min} {Y_Min} {X_Max} {Y_Max} ##XTHERION## xth_me_area_zoom_to 100 ##XTHERION## xth_me_image_insert {insert_XVI} {Copyright} # File generated by pyCreateTh.py version {version} date: {date} # x_min: {X_Min}, x_max: {X_Max} ecart : {X_Max_X_Min} # y_min: {Y_Min}, y_max: {Y_Max} ecart : {Y_Max_Y_Min} scrap SC-{name}_01 -station-names "" "@{name}" -projection extended -author {year} "{author}" -copyright {year} "{Copyright_Short}" {points} {names} {lines} endscrap""" th2_point = """ point {x} {y} station -name {station}""" th2_name = """ point {x} {y} station-name -align tr -scale xs -text {station}""" th2_line = """ line u:Shot_Survey {x1} {y1} {x2} {y2} endline """ seen = set() th2_lines = [] th2_points = [] th2_names = [] other_scraps_extended = f"\tSC-{TARGET}_01\n\tbreak\n" for line in lines: th2_lines.append(th2_line.format(x1=line[0], y1=line[1], x2=line[2], y2=line[3])) coords1 = "{}.{}".format(line[0], line[1]) if coords1 not in seen: seen.add(coords1) th2_points.append(th2_point.format(x=line[0], y=line[1], station=line[4])) th2_names.append(th2_name.format(x=line[0], y=line[1], station=line[4])) coords2 = "{}.{}".format(line[2], line[3]) if "{}.{}".format(line[2], line[3]) not in seen: seen.add(coords2) if line[5] != None: th2_points.append(th2_point.format(x=line[2], y=line[3], station=line[5])) th2_names.append(th2_name.format(x=line[2], y=line[3], station=line[5])) if isfile(output_path): log.warning(f"{Colors.ENDC}{os.path.basename(output_path)}{Colors.WARNING} file already exists - nothing done{Colors.ENDC}") else : name = TARGET, log.debug(f"Therion output path :\t{Colors.ENDC}{output_path}") with open(str(output_path), "w+") as f: f.write(th2_file_header) f.write(th2_file.format( name = name[0], Copyright = Copyright, Copyright_Short = Copyright_Short, points="\n".join(th2_points), lines="\n".join(th2_lines) if LINES else "", names="\n".join(th2_names) if NAMES else "", projection="extended", projection_short="C", author=Author, year=datetime.now().year, version = Version, date=datetime.now().strftime("%Y.%m.%d-%H:%M:%S"), X_Min=x_min*1.2, X_Max=x_max*1.2, Y_Min=y_min*1.2, Y_Max=y_max*1.2, X_Max_X_Min =x_ecart, Y_Max_Y_Min =y_ecart, insert_XVI = "{" + stations[next(iter(stations))][0] + "1 1.0} {" + stations[next(iter(stations))][1] + " " + stations[next(iter(stations))][2] +"} " + os.path.basename(th_name_xvi) + " 0 {}", ) ) if scrap_to_add >= 1 : for i in range(scrap_to_add): other_scraps_extended = other_scraps_extended + f"\tSC-{name[0]}_{i+2:02}\n\tbreak\n" th2_scrap = """ scrap SC-{name}_{num:02} -station-names "" "@{name}" -projection extended -author {year} "{author}" -copyright {year} "{Copyright_Short}" endscrap """ f.write(th2_scrap.format( name=name[0], author=Author, Copyright_Short = Copyright_Short, year=datetime.now().year, num=f"{i+2:02}", ) ) ################################################################################################# # Update -maps files # ################################################################################################# if UPDATE == "": config_vars = { 'fileName': TH_NAME, 'Author': Author, 'Copyright': Copyright, 'Scale' : SCALE, 'Target' : TARGET, 'map_comment' : map_comment, 'club' : club, 'thanksto' : thanksto, 'datat' : datat, 'wpage' : wpage, 'cs' : cs, 'other_scraps_plan' : other_scraps_plan, 'other_scraps_extended' : other_scraps_extended, 'file_info' : f"# File generated by pyCreateTh.py version {Version} date: {datetime.now().strftime("%Y.%m.%d-%H:%M:%S")}", } process_template(DEST_PATH + '/template-maps.th', config_vars, DEST_PATH + '/' + TH_NAME + '-maps.th') ################################################################################################# # Final therion compilation # ################################################################################################# if UPDATE == "": if final_therion_exe == True: FILE = os.path.dirname(ENTRY_FILE) + "/" + TH_NAME + "/" + TH_NAME + ".thconfig" # log.info(f"Final therion compilation: {Colors.ENDC}~\\{os.path.relpath(FILE)}") compile_file(FILE, therion_path=therion_path) ################################################################################################# # lecture d'un .file # ################################################################################################# def readMakFile (ENTRY_FILE) : global error_count datFiles = [] patternDat = re.compile(r'^#.*?\.dat[,;]$', re.IGNORECASE) # Motif insensible à la casse fixPoints = [] patternFixPoints = re.compile(r'^([\w-]+)\[(m|f),\s*(-?\d+\.?\d*),\s*(-?\d+\.?\d*),\s*(-?\d+\.?\d*)\][,;]$', re.IGNORECASE) UTM = [] Datums = set() # Pour stocker les valeurs uniques trouvées try: with open(ENTRY_FILE, 'r') as file: for line in file: line = line.strip() # Supprime les espaces et sauts de ligne if patternDat.match(line): # Supprime le '#' au début et '.dat,' ou '.dat;' à la fin (insensible à la casse) cleaned_entry = re.sub(r'^#|\.dat[,;]$', '', line, flags=re.IGNORECASE) datFiles.append(cleaned_entry + ".DAT") match = patternFixPoints.match(line) if match: name_point, mf, x, y, z = match.groups() fixPoints.append([name_point, mf.lower(), float(x), float(y), float(z)]) if line.startswith('@') and line.endswith(';'): parts = line[1:-1].split(',') # Supprime "@" et ";", puis découpe if len(parts) >= 4: UTM.append(int(parts[3]) if parts[3].isdigit() else parts[3]) if line.startswith('&') and line.endswith(';'): # Extrait la valeur entre & et ; Datum = line[1:-1].strip() # Supprime '&' et ';' Datums.add(Datum) except FileNotFoundError: log.error(f"The mak file {ENTRY_FILE} dit not exist") error_count += 1 except Exception as e: log.error(f"An error occurred (readMakFile): {Colors.ENDC}{e}") error_count += 1 # Vérification des valeurs if len(Datums) > 1: log.critical(f"Several different Datums found in {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}{Colors.CRITICAL}, case not handled! : {Colors.ENDC}{Datums}") exit(0) elif not Datums : log.critical(f"no datum found in mak file : {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") exit(0) elif not datFiles : log.critical(f"No dat file found in mak file : {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") exit(0) elif not fixPoints : log.critical(f"No fix points found in mak file : {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") exit(0) datum_to_epsg = { # Datums globaux "wgs84": "326", # UTM Nord (WGS84) - EPSG:326XX "etrs89": "258", # UTM Nord (ETRS89) - Europe # Datums européens "european1950": "230", # ED50 / UTM Nord - Europe "ed50": "230", # Datums nord-américains "nad27": "267", # UTM Nord (NAD27) - Amérique du Nord "northamericandatum1927": "267", "northamerican1927": "267", "nad83": "269", # UTM Nord (NAD83) - Amérique du Nord "northamericandatum1983": "269", "northamerican1983" : "269", # Datums français "ntf": "275", # UTM Nord (NTF) - France (Paris) "nouvelletriangulationfrançaise": "275", # Datums africains "clarke1880": "297", # UTM Nord (Clarke 1880) - Afrique # Datums australiens "agd66": "202", # UTM Nord (AGD66) - Australie "australiangeodeticdatum1966": "202", "australiangeodetic1966": "202", "agd84": "203", # UTM Nord (AGD84) - Australie "australiangeodeticdatum1984": "203", "australiangeodetic1984": "203", "gda94": "283", # UTM Nord (GDA94) - Australie "geocentricdatumofaustralia1994": "283", "geocentricofaustralia1994": "283", # Datums asiatiques "pulkovo1942": "284", # UTM Nord (Pulkovo 1942) - Russie/CEI "beijing1954": "214", # UTM Nord (Beijing 1954) - Chine # Datums sud-américains "sad69": "291", # UTM Nord (SAD69) - Amérique du Sud "southamericandatum1969": "291", "southamerican1969": "291", "sirgas2000": "319", # UTM Nord (SIRGAS 2000) - Amérique Latine } datum_lower = next(iter(Datums)).strip().lower().replace(" ","") if datum_lower not in datum_to_epsg: log.critical(f"Unknown Datum : {datum_lower}") exit(0) # Extraction du numéro de zone UTM et de l'hémisphère (N/S) if int(UTM[0]) >= 0 : zone_num = int(UTM[0]) hemisphere = "N" else : zone_num = -int(UTM[0]) hemisphere = "S" # print(zone_num) # Vérification de la validité de la zone UTM (1-60) if not 1 <= zone_num <= 60: log.critical("The UTM zone must be between 1 and 60") exit(0) # Construction du code EPSG epsg_prefix = datum_to_epsg[datum_lower] epsg_code = f"{epsg_prefix}{zone_num}" if hemisphere == "N" else f"{epsg_prefix}{zone_num + 100}" # Génération du CRS QGIS (format WKT) crs_wkt = f'EPSG:{epsg_code}' log.info(f"Reading mak file : {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}{Colors.GREEN}, fixed station : {Colors.ENDC}{len(fixPoints)}{Colors.GREEN}, files {Colors.ENDC}{len(datFiles)}{Colors.GREEN}, UTM Zone : {Colors.ENDC}{UTM[0]}{Colors.GREEN}, Datum : {Colors.ENDC}{next(iter(Datums))}{Colors.GREEN}, SCR : {Colors.ENDC}{crs_wkt}") # log.debug(datFiles) # log.debug(fixPoints) return datFiles, fixPoints, crs_wkt ################################################################################################# # Création des dossiers Th à partir d'un dat # ################################################################################################# def DatToThFiles (ENTRY_FILE, fixPoints = [], crs_wkt ="") : # Input : Dat file for conversion # Outputs : Th files by survey global Author global Copyright global Copyright_Short global map_comment global club global thanksto global datat global wpage global cs global template_path global station_by_scrap global final_therion_exe global therion_path global LINES global NAMES global error_count # Détecter la fin de section (FF CR LF qui correspond à \x0c\r\n) section_separator = '\x0c' content = "" ################################################################################################# # Lecture du fichier dat # ################################################################################################# try: result = from_path(ENTRY_FILE) best_guess = result.best() if best_guess is None or best_guess.encoding is None: log.critical(f"Unable to detect the file encoding {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") exit(0) return None encoding_detected = best_guess.encoding.lower() with open(ENTRY_FILE, 'r', encoding=encoding_detected) as f: content = f.read() if encoding_detected.lower() != 'utf-8': log.info(f"{Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}{Colors.GREEN}, encodage : {Colors.ENDC}{encoding_detected}{Colors.GREEN} conversion utf-8") else : log.debug(f"{Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}{Colors.DEBUG}, encodage : {Colors.ENDC}{encoding_detected}") except FileNotFoundError: log.error(f"The dat file {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)} {Colors.ERROR}did not exist") error_count += 1 except Exception as e: log.error(f"An error occurred (DatToThFiles): {Colors.ENDC}{e}{Colors.ERROR}, file : {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") error_count += 1 ################################################################################################# # Séparer les sections # ################################################################################################# sections = content.split(section_separator) # Listes pour stocker les données data = [] unique_id = 1 totdata = f"\t## Liste inputs\n" totMapsPlan = "" totMapsExtended = "" # Tableau global pour stocker toutes les stations stationList = pd.DataFrame(columns=['StationName', 'Survey_Name']) section0 = True; for section in sections: if not section.strip(): continue # ignorer les sections vides # Dictionnaire pour stocker les infos de la section courante section_data = { 'ID': unique_id, 'SURVEY_TITLE': None, 'SURVEY_NAME': None, 'SURVEY_DATE': None, 'COMMENT' : None, 'SURVEY_TEAM': None, 'DECLINATION': None, 'FORMAT': None, 'CORRECTIONS' : None, 'DATA' : [], 'SOURCE' : [] } # Parcourir les lignes de la section lines = section.split('\n') section_data['SOURCE'] = section NextLineSurveyTeam = False if lines: if section0 : section_data['SURVEY_TITLE'] = lines[0].strip() lines = lines[1:] # Supprimer la première ligne section0 = False else : lines = lines[1:] section_data['SURVEY_TITLE'] = lines[0].strip() lines = lines[1:] # Supprimer la première ligne for line in lines: line = line.strip() if line.startswith('SURVEY NAME:'): section_data['SURVEY_NAME'] = sanitize_filename(line.split(':', 1)[1].strip()) elif line.startswith('SURVEY DATE:'): # current_field = 'DATE' # Séparer la date et le commentaire date_parts = line.split(':', 1)[1].strip().split('COMMENT:', 1) date = date_parts[0].strip() jour, mois, annee = date.split() date_convertie = f"{annee} {mois} {jour}" section_data['SURVEY_DATE'] = date_convertie if section_data['SURVEY_DATE'] == None or section_data['SURVEY_DATE'] == '' : section_data['SURVEY_DATE'] = "2000 01 01" log.warning(f"Attention, survey {Colors.ENDC}{section_data['SURVEY_NAME']}{Colors.WARNING} with no date, add default date 2000 01 01 ") if len(date_parts) > 1: section_data['COMMENT'] = date_parts[1].strip() elif line.startswith('SURVEY TEAM:'): NextLineSurveyTeam = True line.strip() elif NextLineSurveyTeam == True : NextLineSurveyTeam = False section_data['SURVEY_TEAM'] = line.strip() elif line.startswith('DECLINATION:'): # current_field = 'DECLINATION' # Découper la ligne en trois parties declination_part = line.split(':', 1)[1].strip() # Extraire DECLINATION (premier nombre) declination_val = declination_part.split()[0] section_data['DECLINATION'] = declination_val # Trouver FORMAT et CORRECTIONS if 'FORMAT:' in declination_part: format_part = declination_part.split('FORMAT:', 1)[1] format_val = format_part.split('CORRECTIONS:', 1)[0].strip() section_data['FORMAT'] = format_val if 'CORRECTIONS:' in format_part: corrections_val = format_part.split('CORRECTIONS:', 1)[1].strip() section_data['CORRECTIONS'] = corrections_val else : if line.strip() != '' : section_data['DATA'].append(line.strip()) else : line.strip() # Ajouter les données de la section à la liste if len(section_data['DATA']) > 0 : data.append(section_data) unique_id += 1 # print(len(data)) for line in data : log.debug(f"SURVEY TITLE: {Colors.ENDC}{line['SURVEY_TITLE']}") log.debug(f"SURVEY NAME: {Colors.ENDC}{line['SURVEY_NAME']}") log.debug(f"SURVEY DATE: {Colors.ENDC}{line['SURVEY_DATE']}") log.debug(f"COMMENT: {Colors.ENDC}{line['COMMENT']}") log.debug(f"SURVEY TEAM: {Colors.ENDC}{line['SURVEY_TEAM']}") log.debug(f"DECLINATION: {Colors.ENDC}{line['DECLINATION']}") log.debug(f"FORMAT: {Colors.ENDC}{line['FORMAT']}") log.debug(f"CORRECTIONS: {Colors.ENDC}{line['CORRECTIONS']}") log.debug(f"Qté DATA: {Colors.ENDC}{len(line['DATA'])}\n") # print(f"DATA: {Colors.ENDC}{line['DATA']}") ################################################################################################# # Grouper les sections # ################################################################################################# def group_and_merge_data(section_data): global error_count grouped = defaultdict(list) for entry in section_data['DATA']: if isinstance(entry, dict) and 'DATA' in entry: key = (entry.get('SURVEY_DATE'), entry.get('SURVEY_TEAM')) grouped[key].extend(entry['DATA']) # fusionne les sous-tableaux else: log.error(f"Ignoré (entrée non conforme) : {entry}") error_count += 1 # Facultatif : convertir le résultat en une liste de dictionnaires regroupés grouped_result = [] for (date, team), data_rows in grouped.items(): grouped_result.append({ 'SURVEY_DATE': date, 'SURVEY_TEAM': team, 'DATA': data_rows }) return grouped_result # grouped = defaultdict(list) # for section in data: # key = (section['SURVEY_DATE'], section['SURVEY_TEAM']) # grouped[key].append(section) # duplicates = [group for group in grouped.values() if len(group) > 1] # for group in duplicates: # print(f"\nDoublons pour date '{group[0]['SURVEY_DATE']}' et équipe '{group[0]['SURVEY_TEAM']}':") # for s in group: # print(f" ID: {s['ID']}, Titre: {s['SURVEY_TITLE']}, Commentaire: {s['COMMENT']}, Qté DATA {len(s['DATA'])}") # section_data = group_and_merge_data(section_data) log.info(f"Read dat file : {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}{Colors.GREEN} with {Colors.ENDC}{len(data)}{Colors.GREEN} survey") ################################################################################################# # Créer fichier th converti # ################################################################################################# if data[0]['SURVEY_TITLE'] !="" : SurveyTitle = sanitize_filename(data[0]['SURVEY_TITLE']) folderDest = os.path.dirname(ENTRY_FILE) + "\\" + SurveyTitle if os.path.isdir(folderDest): SurveyTitle = os.path.basename(ENTRY_FILE[:-4]) else : SurveyTitle = os.path.basename(ENTRY_FILE[:-4]) folderDest = os.path.dirname(ENTRY_FILE) + "\\" + SurveyTitle copy_template_if_not_exists(template_path,folderDest) for _line in data : th_file = """ encoding utf-8 # File generated by pyCreateTh.py version {VERSION} date: {DATE} survey {SURVEY_NAME} -title "{SURVEY_NAME}-{COMMENT}" \tcenterline \t\tdate {SURVEY_DATE} \t\t# team {SURVEY_TEAM} {FIX_POINTS} \t\t# explo-date ???? \t\t# explo-team "????" \t\t# FORMAT: {FORMAT}, CORRECTIONS: {CORRECTIONS} \t\tunits length {LENGTH} \t\tunits compass {COMPASS} \t\tunits clino {CLINO} \t\t{DATA_FORMAT} #{DATA} \tendcenterline endsurvey {SOURCE} """ output_file = folderDest + "\\Data\\" + sanitize_filename(_line['SURVEY_NAME']) + ".th" ################################################################################################# # gestion des déclinaisons # ################################################################################################# ################################################################################################# # gestion des DATA # ################################################################################################# # Création d'un DataFrame à partir des données rows1 = [line.split() for line in _line['DATA']] dfDATA = pd.DataFrame(rows1) stations = pd.concat([dfDATA.iloc[1:, 0], dfDATA.iloc[1:, 1]]).drop_duplicates().str.replace('[', '__').str.replace(']', '___') fixed_names = {point[0] for point in fixPoints} stations = stations[~stations.isin(fixed_names)] new_entries = pd.DataFrame({ 'StationName': stations, 'Survey_Name': _line['SURVEY_NAME'] }) stationList = pd.concat([stationList, new_entries], ignore_index=True) def formated_DataFrame(df, dataFormat ,unit = "meter" ): global error_count # Remplacer les None/NaN par des espaces df = df.fillna(" ") # Conserver la première ligne (en-têtes) séparément header_row = df.iloc[0] # Traiter uniquement les lignes à partir de la deuxième (index 1) df_data = df.iloc[1:].copy() columns = dataFormat.split() Koef = 0.3048 if unit == "meter" else 1.0 if "length" in columns: df_data.iloc[:, columns.index("length")-2] = df_data.iloc[:, columns.index("length")-2].astype(float) * Koef if "up" in columns: df_data.iloc[:, columns.index("up")-2] = df_data.iloc[:, columns.index("up")-2].astype(float) * Koef df_data[columns.index("up")-2] = pd.to_numeric(df_data[columns.index("up")-2], errors='coerce') # convertit en float df_data[columns.index("up")-2] = df_data[columns.index("up")-2].apply(lambda x: "-" if pd.notna(x) and x < 0 else x) if "down" in columns: df_data.iloc[:, columns.index("down")-2] = df_data.iloc[:, columns.index("down")-2].astype(float) * Koef df_data[columns.index("down")-2] = pd.to_numeric(df_data[columns.index("down")-2], errors='coerce') # convertit en float df_data[columns.index("down")-2] = df_data[columns.index("down")-2].apply(lambda x: "-" if pd.notna(x) and x < 0 else x) if "right" in columns: df_data.iloc[:, columns.index("right")-2] = df_data.iloc[:, columns.index("right")-2].astype(float) * Koef df_data[columns.index("right")-2] = pd.to_numeric(df_data[columns.index("right")-2], errors='coerce') # convertit en float df_data[columns.index("right")-2] = df_data[columns.index("right")-2].apply(lambda x: "-" if pd.notna(x) and x < 0 else x) if "left" in columns: df_data.iloc[:, columns.index("left")-2] = df_data.iloc[:, columns.index("left")-2].astype(float) * Koef df_data[columns.index("left")-2] = pd.to_numeric(df_data[columns.index("left")-2], errors='coerce') # convertit en float df_data[columns.index("left")-2] = df_data[columns.index("left")-2].apply(lambda x: "-" if pd.notna(x) and x < 0 else x) if "from" in columns: df_data.iloc[:, columns.index("from")-2] = (df_data.iloc[:, columns.index("from")-2] .astype(str) .str.replace('[', '__') # Remplace [ par __ .str.replace(']', '___') # Remplace ] par ___ # .str.replace('.', '___') # Remplace . par ___ ) if "to" in columns: df_data.iloc[:, columns.index("to")-2] = (df_data.iloc[:, columns.index("to")-2] .astype(str) .str.replace('[', '__') # Remplace [ par __ .str.replace(']', '___') # Remplace ] par ___ # .str.replace('.', '___') # Remplace . par ___ ) # # Remplacer les caractères spéciaux dans les colonnes 1 et 2 (FROM et TO) # for col_idx in [0, 1]: # Colonnes 1 et 2 (index 0 et 1) # df_data.iloc[:, col_idx] = (df_data.iloc[:, col_idx] # .astype(str) # .str.replace('[', '__') # Remplace [ par __ # .str.replace(']', '___') # Remplace ] par ___ # # .str.replace('.', '___') # Remplace . par ___ # ) # Remplacer les NaN par des espaces après transformation df_data = df_data.fillna(" ") # Ajouter un '# ' au début de la colonne 9 (si non vide) df_data.iloc[:, 9] = df_data.iloc[:, 9].apply(lambda x: f"# {x}" if str(x).strip() and str(x) != " " else x) # Ajouter "_hab" à la colonne 2 si FROM == TO df_data.iloc[:, 1] = df_data.apply( lambda row: f"{row.iloc[1]}_hab" if str(row.iloc[0]).strip() == str(row.iloc[1]).strip() else row.iloc[1], axis=1 ) # Gestion des flags surface et not surface new_rows = [] for idx, row in df_data.iterrows(): col10 = str(row.iloc[9]) # Si la colonne 10 contient #|L# Exclude from Length if "#|L#" in col10: surface_row = [" "] * len(row) surface_row[0] = "flags surface" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "flags not surface" new_rows.append(not_surface_row) # Si la colonne 10 contient #|S# type Spay (habillages) elif "#|S#" in col10: surface_row = [" "] * len(row) surface_row[0] = "flags splay" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "flags not splay" new_rows.append(not_surface_row) # Si la colonne 10 contient #|X# total exclusion elif "#|X#" in col10 or "#|XL#" in col10: surface_row = [" "] * len(row) surface_row[0] = "flags duplicate" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "flags not duplicate" new_rows.append(not_surface_row) log.warning(f"Flags '{Colors.ENDC}{col10}{Colors.WARNING}' not implemented in therion, line {Colors.ENDC}{idx+1}{Colors.WARNING} in {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") # Si la colonne 10 contient #|P# exclude from plotting elif "#|P#" in col10: surface_row = [" "] * len(row) surface_row[0] = "# flags exclude from plot no implemented" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "# flags not exclude from plot no implemented" new_rows.append(not_surface_row) log.warning(f"Flags exclude from plot #|P# not implemented in therion, line {Colors.ENDC}{idx+1}{Colors.WARNING} in {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") # Si la colonne 10 contient #|C# exclude from closure elif "#|C#" in col10: surface_row = [" "] * len(row) surface_row[0] = "# flags exclude from closure no implemented" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "# flags not exclude from closure no implemented" new_rows.append(not_surface_row) log.warning(f"Flags #|C# exclude from closure not implemented in therion, line {Colors.ENDC}{idx+1}{Colors.WARNING} in {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") # Si la colonne 10 contient #|PL# exclude from plotting and Length elif "#|PL#" in col10: surface_row = [" "] * len(row) surface_row[0] = "flags duplicate" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "flags not duplicate" new_rows.append(not_surface_row) log.warning(f"Flags '{Colors.ENDC}{col10}{Colors.WARNING}' not implemented in therion, line {Colors.ENDC}{idx+1}{Colors.WARNING} in {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") # Si la colonne 10 contient #|LC# exclude from Length and Closure elif "#|LC#" in col10: surface_row = [" "] * len(row) surface_row[0] = "flags duplicate" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "flags not duplicate" new_rows.append(not_surface_row) log.warning(f"Flags '{Colors.ENDC}{col10}{Colors.WARNING}' not implemented in therion, line {Colors.ENDC}{idx+1}{Colors.WARNING} in {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") # Si la colonne 10 contient #|PLC# exclude from plotting, closure and length elif "#|PLC#" in col10: surface_row = [" "] * len(row) surface_row[0] = "flags duplicate" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "flags not duplicate" new_rows.append(not_surface_row) elif "#|" in col10: surface_row = [" "] * len(row) surface_row[0] = "# flags unknown no implemented" new_rows.append(surface_row) new_rows.append(row.tolist()) not_surface_row = [" "] * len(row) not_surface_row[0] = "# flags not unknown no implemented" new_rows.append(not_surface_row) log.error(f"Flags unknown '{Colors.ENDC}{col10}{Colors.WARNING}' not implemented, line {Colors.ENDC}{idx+1}{Colors.WARNING} in {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") error_count += 1 else: new_rows.append(row.tolist()) prev_row = row # Garder trace de la ligne précédente cleaned_rows = [] i = 0 while i < len(new_rows): current = new_rows[i] if (i + 1 < len(new_rows) and str(current[0]).strip() == "flags not surface" and str(new_rows[i + 1][0]).strip() == "flags surface"): i += 2 elif (i + 1 < len(new_rows) and str(current[0]).strip() == "flags not splay" and str(new_rows[i + 1][0]).strip() == "flags splay"): i += 2 elif (i + 1 < len(new_rows) and str(current[0]).strip() == "flags not duplicate" and str(new_rows[i + 1][0]).strip() == "flags duplicate"): i += 2 elif (i + 1 < len(new_rows) and str(current[0]).strip() == "# flags not exclude from closure no implemented" and str(new_rows[i + 1][0]).strip() == "# flags exclude from closure no implemented"): i += 2 elif (i + 1 < len(new_rows) and str(current[0]).strip() == "# flags not exclude from plot no implemented" and str(new_rows[i + 1][0]).strip() == "# flags exclude from plot no implemented"): i += 2 elif (i + 1 < len(new_rows) and str(current[0]).strip() == "# flags not unknown no implemented" and str(new_rows[i + 1][0]).strip() == "# flags unknown no implemented"): i += 2 else: cleaned_rows.append(current) i += 1 # Convertir les lignes en chaines formatées output = [] # Ajouter la première ligne (en-têtes) telle quelle header_str = "\t\t" + "\t".join(map(str, header_row)) output.append(header_str) # Ajouter les autres lignes traitées for row in cleaned_rows: row_str = "\t\t" + "\t".join(map(str, row)) output.append(row_str) # print(new_rows) return "\n".join(output) ################################################################################################# # Recherche des points fixes (entrées) ################################################################################################# fixPoint ="" # Extraire les noms des stations depuis dfDATA stations_from = set(dfDATA.iloc[:, 0]) # Colonne 'FROM' stations_to = set(dfDATA.iloc[:, 1]) # Colonne 'TO' all_stations = stations_from.union(stations_to) # Filtrer fixPoints pour garder seulement ceux présents dans dfDATA list_common_points = [point for point in fixPoints if point[0] in all_stations] # Afficher le résultat # print(list_common_points) if len(list_common_points) >= 1 : fixPoint += f"\t\tcs {crs_wkt}\n" for point in list_common_points : if point[1] == 'm' : fixPoint += f"\t\tfix {point[0]} {point[2]} {point[3]} {point[4]}\n" elif point[1] == 'f' : fixPoint += f"\t\tfix {point[0]} {point[2]*0.3048} {point[3]*0.3048} {point[4]*0.3048} # Conversion feet - meter\n" ################################################################################################# # Gestion des formats ################################################################################################# if _line['FORMAT'][0] == 'D' : compass = 'degree' elif _line['FORMAT'][0] == 'R' : compass = 'grads' else : compass = 'Compass_error' log.error("Compass bearing unit 'quads' not yet implemented") error_count += 1 if _line['FORMAT'][1] == 'D' : length = 'feet' elif _line['FORMAT'][1] == 'M' : length = 'meter' else : length = 'Length_error' log.error("Length unit 'Feet and Inches' not yet implemented") error_count += 1 if _line['FORMAT'][3] == 'D' : clino = 'degree' elif _line['FORMAT'][3] == 'R' : clino = 'grads' # elif _line['FORMAT'][3] == 'G' : clino = 'percent' # %Grades à vérifier? # elif _line['FORMAT'][3] == 'M' : clino = 'grads' # Degrees and Minutes # elif _line['FORMAT'][3] == 'W' : clino = 'grads' # Depth Gauge else : clino = 'Inclination_error' log.error("Inclination unit not yet implemented") error_count += 1 dataFormat = "" if _line['FORMAT'][4] == 'U' : dataFormat = dataFormat + " up" elif _line['FORMAT'][4] == 'D' : dataFormat = dataFormat + " down" elif _line['FORMAT'][4] == 'R' : dataFormat = dataFormat + " right" elif _line['FORMAT'][4] == 'L' : dataFormat = dataFormat + " left" if _line['FORMAT'][5] == 'U' : dataFormat = dataFormat + " up" elif _line['FORMAT'][5] == 'D' : dataFormat = dataFormat + " down" elif _line['FORMAT'][5] == 'R' : dataFormat = dataFormat + " right" elif _line['FORMAT'][5] == 'L' : dataFormat = dataFormat + " left" if _line['FORMAT'][6] == 'U' : dataFormat = dataFormat + " up" elif _line['FORMAT'][6] == 'D' : dataFormat = dataFormat + " down" elif _line['FORMAT'][6] == 'R' : dataFormat = dataFormat + " right" elif _line['FORMAT'][6] == 'L' : dataFormat = dataFormat + " left" if _line['FORMAT'][7] == 'U' : dataFormat = dataFormat + " up" elif _line['FORMAT'][7] == 'D' : dataFormat = dataFormat + " down" elif _line['FORMAT'][7] == 'R' : dataFormat = dataFormat + " right" elif _line['FORMAT'][7] == 'L' : dataFormat = dataFormat + " left" if _line['FORMAT'][10] == 'L' : dataFormat = " length" + dataFormat elif _line['FORMAT'][10] == 'A' : dataFormat = " compass" + dataFormat elif _line['FORMAT'][10] == 'D' : dataFormat = " clino" + dataFormat elif _line['FORMAT'][10] == 'a' : dataFormat = " backcompass" + dataFormat elif _line['FORMAT'][10] == 'd' : dataFormat = " backclino" + dataFormat if _line['FORMAT'][9] == 'L' : dataFormat = " length" + dataFormat elif _line['FORMAT'][9] == 'A' : dataFormat = " compass" + dataFormat elif _line['FORMAT'][9] == 'D' : dataFormat = " clino" + dataFormat elif _line['FORMAT'][9] == 'a' : dataFormat = " backcompass" + dataFormat elif _line['FORMAT'][9] == 'd' : dataFormat = " backclino" + dataFormat if _line['FORMAT'][8] == 'L' : dataFormat = " length" + dataFormat elif _line['FORMAT'][8] == 'A' : dataFormat = " compass" + dataFormat elif _line['FORMAT'][8] == 'D' : dataFormat = " clino" + dataFormat elif _line['FORMAT'][8] == 'a' : dataFormat = " backcompass" + dataFormat elif _line['FORMAT'][8] == 'd' : dataFormat = " backclino" + dataFormat dataFormat = "data normal from to" + dataFormat + " # comment" with open(str(output_file), "w+", encoding="utf-8") as f: f.write(th_file.format( VERSION = Version, DATE=datetime.now().strftime("%Y.%m.%d-%H:%M:%S"), SURVEY_NAME = _line['SURVEY_NAME'], SURVEY_DATE = _line['SURVEY_DATE'], SURVEY_TEAM = _line['SURVEY_TEAM'], FORMAT = _line['FORMAT'], COMPASS = compass, LENGTH = length, CLINO = clino, DATA_FORMAT = dataFormat, CORRECTIONS = _line['CORRECTIONS'], DATA = formated_DataFrame(dfDATA, dataFormat, length), COMMENT = _line['COMMENT'].replace('"', "'"), FIX_POINTS = fixPoint, SOURCE = '\n'.join('# ' + line for line in _line['SOURCE'].splitlines()), ) ) totdata +=f"\tinput Data/{_line['SURVEY_NAME']}/{_line['SURVEY_NAME']}-tot.th\n" log.info(f"Therion file : {Colors.ENDC}~\\{os.path.relpath(output_file)}{Colors.GREEN} created from {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") ################################################################################################# # Création des dossiers ################################################################################################# createThFolders( ENTRY_FILE = output_file, SCALE = args.scale, UPDATE = args.update ) _destination = output_file[:-3] + "\\Sources" destination_path = os.path.join(_destination, os.path.basename(output_file)) shutil.move(output_file, destination_path) totMapsPlan += f"\tMP-{_line['SURVEY_NAME']}-Plan-tot@{_line['SURVEY_NAME']}\n\tbreak\n" totMapsExtended += f"\tMC-{_line['SURVEY_NAME']}-Extended-tot@{_line['SURVEY_NAME']}\n\tbreak\n" ################################################################################################# # Gestion des equats ################################################################################################# totdata +=f"\n" # On numérote les doublons de Survey_Name pour chaque StationName stationList['Survey_Number'] = stationList.groupby('StationName').cumcount() + 1 # On pivote le tableau pour que chaque Survey_Name devienne une colonne tableau_pivot = stationList.pivot(index='StationName', columns='Survey_Number', values='Survey_Name') tableau_pivot.columns = [f'Survey_Name_{i}' for i in tableau_pivot.columns] # print(f"tableau_pivot : {Colors.ENDC}{tableau_pivot}{Colors.INFO} in {Colors.ENDC}{ENTRY_FILE}") totdata +=f"\n\t## Liste equates\n" if 'Survey_Name_2' in tableau_pivot.columns: # On réinitialise l'index pour avoir StationName comme colonne normale tableau_pivot = tableau_pivot.reset_index() tableau_equate = tableau_pivot[tableau_pivot['Survey_Name_2'].notna()] log.info(f"Total 'equats' : {Colors.ENDC}{len(tableau_equate)}{Colors.INFO} in {Colors.ENDC}~\\{os.path.relpath(ENTRY_FILE)}") # print(tableau_equate) # print(f"fixePoints : {Colors.ENDC}{fixed_names}{Colors.INFO} in {Colors.ENDC}{ENTRY_FILE}") # Pour chaque ligne du tableau for _, row in tableau_equate.iterrows(): station = row['StationName'] # On récupère tous les Survey_Name non vides (NaN exclus) surveys = [row[col] for col in tableau_equate.columns if col.startswith('Survey_Name') and pd.notna(row[col])] # Pour chaque paire unique (i < j), on écrit la ligne 'equate' for i in range(len(surveys)): for j in range(i + 1, len(surveys)): totdata +=f"\tequate {station}@{surveys[i]}.{surveys[i]} {station}@{surveys[j]}.{surveys[j]}\n" else: log.info(f"No 'equats' found in {Colors.ENDC}{ENTRY_FILE}") totdata +=f"\n\t## Appel des maps\n\tinput {SurveyTitle}-maps.th\n" config_vars = { 'fileName': SurveyTitle, 'cavename': SurveyTitle.replace("_", " "), 'Author': Author, 'Copyright': Copyright, 'Scale' : args.scale, 'Target' : "TARGET", 'map_comment' : map_comment, 'club' : club, 'thanksto' : thanksto, 'datat' : datat, 'wpage' : wpage, 'cs' : crs_wkt, 'totData' : totdata, 'other_scraps_plan' : totMapsPlan, 'other_scraps_extended' : totMapsExtended, 'file_info' : f"# File generated by pyCreateTh.py version {Version} date: {datetime.now().strftime("%Y.%m.%d-%H:%M:%S")}", } DEST_PATH = os.path.dirname(ENTRY_FILE) + '/' + SurveyTitle process_template(DEST_PATH + '/template.thconfig', config_vars, DEST_PATH + '/' + SurveyTitle + '.thconfig') process_template(DEST_PATH + '/template-tot.th', config_vars, DEST_PATH + '/' + SurveyTitle + '-tot.th') process_template(DEST_PATH + '/template-maps.th', config_vars, DEST_PATH + '/' + SurveyTitle + '-maps.th') process_template(DEST_PATH + '/template-readme.md', config_vars, DEST_PATH + '/readme.md') # Créer un DataFrame pandas df = pd.DataFrame(data) # print(df) return df ################################################################################################# # main function # ################################################################################################# if __name__ == u'__main__': start_time = datetime.now() ################################################################################################# # Parse arguments # ################################################################################################# parser = argparse.ArgumentParser( description=f"{Colors.HEADER}Create a skeleton folder and th, th2 files with scraps from *.mak, *.dat, *.th Therion files, version: {Colors.ENDC}{Version}\n", formatter_class=argparse.RawDescriptionHelpFormatter) parser.print_help = colored_help.__get__(parser) parser.add_argument("--survey_file", help="The survey file (*.th, *.mak, *.dat,) to perform e.g. './Therion_file.th'", default="") parser.add_argument("--survey_name", help="Scrap name (if different from 'survey_file' name)", default="None") #parser.add_argument("--proj", choices=['plan', 'elevation', 'extended', 'none'], help="The scrap projection to produce", default="plan") #parser.add_argument("--format", choices=['th2', 'plt'], help="Output format. Either th2 for producing skeleton for drawing or plt for visualizing in aven/loch", default="th2") parser.add_argument("--output", default="./", help="Output folder path") # parser.add_argument("--therion-path", help="Path to therion binary", default="therion") parser.add_argument("--scale", help="Scale for the exports", default="1000") parser.add_argument("--lines", type=str_to_bool, help="Shot lines in th2 files", default=-1) parser.add_argument("--names", type=str_to_bool, help="Stations names in th2 files", default=-1) parser.add_argument("--update", help="Mode update, option th2", default="") parser.epilog = ( f"{Colors.GREEN}Please, complete {Colors.BLUE}config.ini{Colors.GREEN} file for personal configuration{Colors.ENDC}\n" f"{Colors.GREEN}If no argument :{Colors.BLUE} files selection windows\n{Colors.ENDC}\n" f"{Colors.BLUE}Examples:{Colors.ENDC}\n" f"\t> python pyCreateTh.py ./Tests/Entree.th --survey_name Geophysicaya_01_entree --output ./test/ --scale 1000\n" f"\t> python pyCreateTh.py Entree.th\n" f"\t> python pyCreateTh.py\n\n") args = parser.parse_args() if args.survey_file == "": args.survey_file = select_file() # print(f"Selected file : {args.survey_file}") output_log = splitext(abspath(args.survey_file))[0]+".log" log = setup_logger(output_log, debug_log) # log.debug("Ceci est un message de debug") # log.info("Tout va bien") # log.warning("Attention, possible souci") # log.error("Une erreur est survenue") # log.critical("Erreur critique !") if os.name == 'posix': os.system('clear') # Linux, MacOS elif os.name == 'nt': os.system('cls')# Windows else: print("\n" * 100) _titre =[f'********************************************************************************************************************************************\033[0m', f'* Conversion Th, Dat, Mak files to Therion files and folders', f'* Script pyCreateTh by : {Colors.ENDC}alexandre.pont@yahoo.fr', f'* Version : {Colors.ENDC}{Version}', f'* Input file : {Colors.ENDC}~\\{os.path.relpath(args.survey_file)}', f'* Output file : {Colors.ENDC}~\\{os.path.relpath(splitext(abspath(args.survey_file))[0])}', f'* Log file : {Colors.ENDC}~\\{os.path.relpath(output_log)}', f'* ', f'* ', f'* ', f'********************************************************************************************************************************************\033[0m'] for i in range(11): log.info(_titre[i]) ################################################################################################# # Reading config.ini # ################################################################################################# try: read_config(configIni) except ValueError as e: log.critical(f"Reading config.ini file error: {Colors.ENDC}{e}") exit(0) if args.survey_file[-2:].lower() == "th" : createThFolders( ENTRY_FILE = abspath(args.survey_file), TARGET = args.survey_name, SCALE = args.scale, UPDATE = args.update ) elif args.survey_file[-3:].lower() == "mak" : datFiles, fixPoints, crs_wkt = readMakFile(abspath(args.survey_file)) folderDest = os.path.dirname(abspath(args.survey_file)) + "/" + os.path.basename(abspath(args.survey_file))[:-4] copy_template_if_not_exists(template_path,folderDest) for file in datFiles : _file = os.path.dirname(abspath(args.survey_file)) + "\\"+ file shutil.copy(_file, folderDest + "\\Data\\") ABS_file = folderDest + "\\Data\\"+ file DatToThFiles (ABS_file, fixPoints, crs_wkt) destination = os.path.join(folderDest, "Sources", os.path.basename(ABS_file)) if os.path.exists(destination): os.remove(destination) shutil.move(ABS_file, destination) elif args.survey_file[-3:].lower() == "dat" : ABS_file =abspath(args.survey_file) DatToThFiles (ABS_file) else : log.error(f"file {Colors.ENDC}~\\{os.path.relpath(args.survey_file)}{Colors.ERROR} not yet supported") error_count += 1 duration = (datetime.now() - start_time).total_seconds() if error_count == 0 : log.info(f"All files processed successfully in {Colors.ENDC}{duration:.2f}{Colors.INFO} secondes, without errors") else : log.error(f"There were {Colors.ENDC}{error_count}{Colors.ERROR} errors during {Colors.ENDC}{duration:.2f}{Colors.ERROR} secondes, check the log file {Colors.ENDC}~\\{os.path.relpath(output_log)}")