my code. optimized it.
import geopandas as gpd
import pandas as pd
from shapely.geometry import LineString, Point
import os
import glob
import numpy as np  # For handling non-finite values

def get_input(prompt, default=None, cast_type=str):
    """
    Prompts the user for input with an optional default value and casts it to the specified type.
    """
    while True:
        try:
            if default is not None:
                user_input = input(f"{prompt} [{default}]: ").strip()
                if user_input == "":
                    user_input = default
            else:
                user_input = input(f"{prompt}: ").strip()
            return cast_type(user_input)
        except ValueError:
            print(f"Input non valido. Inserisci un valore di tipo {cast_type.__name__}.")

def merge_files(input_folder, file_extension='txt'):
    """
    Merges multiple CSV or TXT files from a specified folder into a single DataFrame.

    Returns:
        merged_df (DataFrame): The merged DataFrame containing all columns.
        num_files (int): The number of files merged.
    """
    file_pattern = os.path.join(input_folder, f'*.{file_extension}')
    file_list = glob.glob(file_pattern)

    if not file_list:
        raise FileNotFoundError(f"Nessun file con estensione '{file_extension}' trovato in '{input_folder}'.")

    df_list = []
    for file in file_list:
        try:
            if file_extension == 'csv':
                df = pd.read_csv(file)
            elif file_extension == 'txt':
                df = pd.read_csv(file, delimiter='\t')  # Adjust delimiter if necessary
            else:
                raise ValueError("Estensione del file non supportata. Usa 'csv' o 'txt'.")

            # Optionally, add a source column to trace back the origin
            df['Source_File'] = os.path.basename(file)
            df_list.append(df)

        except Exception as e:
            print(f"Errore nella lettura di {file}: {e}")

    merged_df = pd.concat(df_list, ignore_index=True)
    merged_df.drop_duplicates(inplace=True)

    num_files = len(file_list)

    return merged_df, num_files

def dataframe_to_geodataframe(df, lon_column, lat_column, crs='EPSG:4326'):
    """
    Converts a pandas DataFrame into a GeoDataFrame with Point geometries.

    Parameters:
        df (DataFrame): Input DataFrame.
        lon_column (str): Name of the longitude column.
        lat_column (str): Name of the latitude column.
        crs (str): Coordinate Reference System.

    Returns:
        GeoDataFrame
    """
    try:
        geometry = [Point(xy) for xy in zip(df[lon_column], df[lat_column])]
    except KeyError as e:
        raise KeyError(f"Colonna mancante per le coordinate: {e}")

    gdf = gpd.GeoDataFrame(df, geometry=geometry)
    gdf.set_crs(crs, inplace=True)
    return gdf

def export_to_shapefile(gdf, output_path):
    """
    Exports a GeoDataFrame to a shapefile.
    """
    try:
        # Ensure the directory exists
        os.makedirs(os.path.dirname(output_path), exist_ok=True)

        # Export to Shapefile
        gdf.to_file(output_path, driver='ESRI Shapefile')
        print(f"Fatto: Shapefile salvato in '{output_path}'.")
    except Exception as e:
        print(f"Errore nell'esportazione dello Shapefile: {e}")

def standardize_column_names(df):
    """
    Removes whitespace and standardizes column names to Title Case,
    keeping 'geometry' column unchanged.
    """
    try:
        df = df.rename(columns=lambda x: x.strip())
        df.columns = [col.strip() for col in df.columns]
        standardized_columns = []
        for col in df.columns:
            if col.lower() == 'geometry':
                standardized_columns.append('geometry')
            else:
                standardized_columns.append(col.title())
        df.columns = standardized_columns
        return df
    except Exception as e:
        raise Exception(f"Errore nella standardizzazione dei nomi delle colonne: {e}")

def categorize_gap(gap, median_gap, tolerance=0.2):
    """
    Categorize the gap based on its relation to the median_gap.
    """
    if gap > median_gap * (1 + tolerance):
        return "Significantly Large Gap"
    elif gap < median_gap * (1 - tolerance):
        return "Significantly Small Gap"
    else:
        return "Normal"

def main():
    while True:
        print("=== Conversione Punti in Percorsi ===\n")

        # Section 1: Creating Shapefile from Multiple Files
        try:
            create_shapefile = get_input(
                "Vuoi creare uno shapefile da più file CSV/TXT? (s/n)", 
                default='s', 
                cast_type=str
            ).lower()

            if create_shapefile == 's':
                # Step 1: Input Folder Path
                input_folder = input("Inserisci il percorso della cartella contenente i file CSV/TXT da unire: ").strip('"').strip("'")
                if not os.path.isdir(input_folder):
                    print("Errore: La cartella specificata non esiste.\n")
                    continue

                # Step 2: Select File Extension
                file_extension = get_input(
                    "Inserisci l'estensione dei file da unire ('csv' o 'txt')", 
                    default='txt',  # Changed default to 'txt'
                    cast_type=str
                ).lower()
                if file_extension not in ['csv', 'txt']:
                    print("Errore: Estensione dei file non valida. Usa 'csv' o 'txt'.\n")
                    continue

                # Step 3: Merge Files
                try:
                    merged_df, num_files = merge_files(
                        input_folder, 
                        file_extension=file_extension
                    )
                except Exception as e:
                    print(f"Errore nell'unire i file: {e}\n")
                    continue

                # Step 4: List Found Columns Before Prompting for Required Columns
                found_columns = merged_df.columns.tolist()
                print(f"Colonne trovate nei file uniti: {', '.join(found_columns)}\n")

                # Step 5: Prompt for Required Columns
                expected_columns_input = get_input(
                    "Inserisci i nomi delle colonne richieste separati da virgola:",  # Removed example text
                    default='Time,Image,X,Y,Z,Roll,Pitch,Heading',
                    cast_type=str
                )
                expected_columns = [col.strip() for col in expected_columns_input.split(',')]

                # Validate columns
                missing_columns = [col for col in expected_columns if col not in merged_df.columns]
                if missing_columns:
                    print(f"Errore: Le seguenti colonne richieste non sono presenti nei file: {', '.join(missing_columns)}\n")
                    continue

                # Step 6: Subset the merged DataFrame to include only the expected columns
                merged_df = merged_df[expected_columns]

                # Step 7: Report Number of Files Merged and Total Points/Images
                print(f"I file sono stati fusi correttamente: {num_files} file uniti.")
                print(f"Numero totale di punti/immagini: {len(merged_df)}.\n")

                # Step 8: Prompt for X and Y Columns
                print("Ora, per favore, specifica quali colonne rappresentano le coordinate X (longitudine) e Y (latitudine).")
                lon_column = get_input(
                    "Inserisci il nome della colonna per X (longitudine)", 
                    default='X',
                    cast_type=str
                )
                lat_column = get_input(
                    "Inserisci il nome della colonna per Y (latitudine)", 
                    default='Y',
                    cast_type=str
                )

                # Validate X and Y columns
                if lon_column not in merged_df.columns or lat_column not in merged_df.columns:
                    print("Errore: Le colonne X o Y specificate non esistono nei dati.\n")
                    continue

                # Step 9: Data Cleaning - Remove Non-Finite Coordinate Values
                initial_count = len(merged_df)
                merged_df.replace([np.inf, -np.inf], np.nan, inplace=True)
                merged_df.dropna(subset=[lon_column, lat_column], inplace=True)
                final_count = len(merged_df)

                if final_count < initial_count:
                    removed = initial_count - final_count
                    print(f"Avviso: Rimosse {removed} righe con valori di coordinate non validi.\n")

                if merged_df.empty:
                    print("Errore: Dopo la pulizia, nessun dato rimane. Controlla i tuoi file di input.\n")
                    continue

                # Step 10: Prompt for CRS
                print("Ora, specifica il CRS (Coordinate Reference System) dei dati. Se non sei sicuro, lascia il default 'EPSG:4326' (WGS84).")
                crs_input = get_input(
                    "Inserisci il CRS (es. 'EPSG:4326')", 
                    default='EPSG:4326',
                    cast_type=str
                )

                # Step 11: Save Merged DataFrame as TXT Before Conversion
                try:
                    # Define default merged file name
                    folder_name = os.path.basename(os.path.normpath(input_folder))
                    merged_output_default = folder_name + "_merged.txt"
                    merged_output = get_input(
                        "Inserisci il nome per il file merged (senza estensione)",
                        default=merged_output_default,
                        cast_type=str
                    )
                    merged_output_path = os.path.abspath(merged_output + ".txt")
                    # Save as TXT with tab delimiter
                    merged_df.to_csv(merged_output_path, sep='\t', index=False)
                    print(f"Fatto: File merged salvato in '{merged_output_path}'.\n")
                except Exception as e:
                    print(f"Errore nel salvare il file merged: {e}\n")
                    continue

                # Step 12: Convert to GeoDataFrame
                try:
                    gdf = dataframe_to_geodataframe(
                        merged_df, 
                        lon_column=lon_column, 
                        lat_column=lat_column,
                        crs=crs_input
                    )
                    print(f"GeoDataFrame convertito correttamente dai file '{file_extension}'. Pronto per essere convertito in Shapefile.\n")
                except Exception as e:
                    print(f"Errore nella conversione in GeoDataFrame: {e}\n")
                    continue

                # Inform the user that the data is ready to be converted into a Shapefile
                print("Il file è pronto per essere convertito in formato Shapefile (.shp).\n")

                # Step 13: Export to Shapefile
                # Changed default suffix to '_elaborato'
                output_base_default = folder_name + "_elaborato"
                output_base = get_input(
                    "Inserisci il nome per il nuovo shapefile (senza estensione)",
                    default=output_base_default,
                    cast_type=str
                )
                output_shp = os.path.abspath(output_base + ".shp")
                output_kml = os.path.abspath(output_base + ".kml")
                export_to_shapefile(gdf, output_shp)

            else:
                # If not creating shapefile, prompt for existing shapefile
                input_shapefile = get_input(
                    "Inserisci il percorso dello shapefile di input (.shp)", 
                    default=None, 
                    cast_type=str
                ).strip('"').strip("'")
                if not os.path.isfile(input_shapefile):
                    print("Errore: Lo shapefile specificato non esiste.\n")
                    continue
                try:
                    gdf = gpd.read_file(input_shapefile)
                    gdf = standardize_column_names(gdf)
                    print("Shapefile caricato e colonne standardizzate correttamente.\n")
                except Exception as e:
                    print(f"Errore nel caricamento dello shapefile: {e}\n")
                    continue

        except Exception as e:
            print(f"Errore durante la creazione dello shapefile: {e}\n")
            continue

        # Ensure 'Time' column exists and is numeric
        try:
            if 'Time' not in gdf.columns:
                print("Errore: La colonna 'Time' non è presente nello shapefile.\n")
                continue
            else:
                if gdf['Time'].dtype.kind not in 'biufc':
                    print(f"Errore: La colonna 'Time' non è numerica (tipo attuale: {gdf['Time'].dtype}).\n")
                    continue
                else:
                    print("Colonna 'Time' verificata come numerica.\n")
        except Exception as e:
            print(f"Errore nella verifica della colonna 'Time': {e}\n")
            continue

        # Step 14: Imposta la soglia del divario. Diocan
        try:
            gap_threshold = get_input(
                "Inserisci la soglia del divario, ovvero il tempo massimo consentito (in secondi) tra due punti per considerarli nello stesso percorso",
                default=3.0,
                cast_type=float
            )
        except Exception as e:
            print(f"Errore nell'impostazione della soglia del divario: {e}\n")
            continue

        # Step 15: Imposta il nome del Campo del nuovo Gruppo
        try:
            default_group_field = "PathID"
            group_field = get_input(
                f"Inserisci il nome per il campo del nuovo gruppo [default: {default_group_field}]: ",
                default=default_group_field,
                cast_type=str
            )
        except Exception as e:
            print(f"Errore nell'impostazione del nome del campo del gruppo: {e}\n")
            continue

        # Step 16: Add Group Field if not exists
        try:
            if group_field not in gdf.columns:
                gdf[group_field] = 0
        except Exception as e:
            print(f"Errore nell'aggiungere il campo del gruppo: {e}\n")
            continue

        # Step 17: Sort by 'Time'
        try:
            gdf_sorted = gdf.sort_values(by='Time').reset_index(drop=True)
        except Exception as e:
            print(f"Errore nell'ordinamento per il campo 'Time': {e}\n")
            continue

        # Step 18: Assign PathID and calculate temporal gaps
        try:
            current_group = 1
            previous_time = None
            path_ids = []
            gaps = []

            for idx, row in gdf_sorted.iterrows():
                current_time = row['Time']
                if pd.isna(current_time):
                    print(f"Avviso: Valore NaN rilevato nel campo Time all'indice {idx}. Assegnato al gruppo corrente.")
                    path_ids.append(current_group)
                    continue
                if previous_time is not None:
                    gap = current_time - previous_time
                    if pd.isnull(gap):
                        print(f"Avviso: Gap non calcolabile tra l'indice {idx-1} e {idx}. Trattato come gap normale.")
                        gap = 0
                    gaps.append({
                        "Entry_Index": idx - 1,
                        "Previous_Time": previous_time,
                        "Current_Time": current_time,
                        "Gap": gap
                    })
                    if gap > gap_threshold:
                        current_group += 1
                path_ids.append(current_group)
                previous_time = current_time

            gdf_sorted[group_field] = path_ids
        except Exception as e:
            print(f"Errore nell'assegnazione dei PathID e nel calcolo dei gap: {e}\n")
            continue

        # Step 19: Convert gaps to DataFrame for final log
        try:
            gaps_df = pd.DataFrame(gaps)
        except Exception as e:
            print(f"Errore nella creazione del DataFrame dei gap: {e}\n")
            continue

        # Step 20: Check if Shapefile was Created
        try:
            if 'output_shp' not in locals():
                print("Errore: Variabile 'output_shp' non definita. Assicurati di aver creato uno shapefile prima.")
                continue
        except NameError:
            print("Errore: Variabile 'output_shp' non definita. Assicurati di aver creato uno shapefile prima.")
            continue

        # Step 21: Generate new connected paths and export
        try:
            # Filter groups with at least two points
            valid_groups = gdf_sorted.groupby(group_field).filter(lambda x: len(x) > 1)

            # Check if there are valid groups
            if valid_groups.empty:
                print("Errore: Nessun gruppo contiene più di un punto. Impossibile creare LineStrings.\n")
                continue

            # Group by PathID and create LineStrings
            paths = valid_groups.groupby(group_field)['geometry'].apply(lambda x: LineString(x.tolist())).reset_index()

            # Create GeoDataFrame for paths
            paths_gdf = gpd.GeoDataFrame(paths, geometry='geometry')

            # Set CRS to match input
            paths_gdf.set_crs(gdf_sorted.crs, inplace=True)

            # Calculate lengths in kilometers
            if not gdf_sorted.crs.is_projected:
                print("\nAvviso: Il CRS di input non è proiettato. Riproiezione a EPSG:3857 per i calcoli delle distanze.")
                paths_gdf = paths_gdf.to_crs(epsg=3857)

            paths_gdf['Length_km'] = paths_gdf.length / 1000  # Convert meters to kilometers

            # Calculate total distance
            total_distance_km = paths_gdf['Length_km'].sum()
            print(f"\nDistanza totale di tutti i percorsi: {total_distance_km:.3f} chilometri.")

            # Save paths as Shapefile (already done earlier, remove duplication)

            # Save paths as KML
            try:
                paths_gdf.to_file(output_kml, driver='KML')
                print(f"Fatto: I percorsi sono stati salvati in '{output_kml}'.\n")
            except Exception as e:
                print(f"Errore nel salvare i percorsi come KML: {e}\n")
                continue

            # Generate summary report
            try:
                # Calculate Start_Time and End_Time based on valid_groups
                start_times = valid_groups.groupby(group_field)['Time'].min()
                end_times = valid_groups.groupby(group_field)['Time'].max()

                # Merge Start_Time and End_Time with paths_gdf
                paths_gdf = paths_gdf.merge(start_times.rename('Start_Time'), on=group_field, how='left')
                paths_gdf = paths_gdf.merge(end_times.rename('End_Time'), on=group_field, how='left')

                # Create summary_summary ensuring alignment
                summary_summary = paths_gdf[['PathID', 'Length_km', 'Start_Time', 'End_Time']].drop_duplicates()

                # Save summary as CSV
                summary_output = os.path.splitext(output_shp)[0] + "_summary.csv"
                summary_summary.to_csv(summary_output, index=False)
                print(f"Fatto: Il report di sintesi dei percorsi è stato salvato in '{summary_output}'.\n")
            except Exception as e:
                print(f"Errore nel generare il report di sintesi dei percorsi: {e}\n")
                continue

        except Exception as e:
            print(f"Errore durante la generazione dei percorsi connessi e l'esportazione: {e}\n")
            continue

        # Step 22: Prepare final report
        try:
            report_lines = []
            report_lines.append("=== Report Finale ===\n")
            report_lines.append(f"Shapefile di input: {os.path.abspath(input_shapefile) if 'input_shapefile' in locals() else 'Multipli file CSV/TXT fusi.'}")
            report_lines.append(f"Shapefile di output: {output_shp}")
            report_lines.append(f"KML di output: {output_kml}")
            report_lines.append(f"Campo PathID: {group_field}")
            report_lines.append(f"Campo Time: Time")
            report_lines.append(f"Soglia del divario: {gap_threshold}")
            report_lines.append(f"Distanza totale di tutti i percorsi: {total_distance_km:.3f} chilometri.\n")

            # Gap analysis
            if not gaps_df.empty:
                median_gap = gaps_df['Gap'].median()
                mean_gap = gaps_df['Gap'].mean()
                std_gap = gaps_df['Gap'].std()

                # Place stats at the top of the report
                report_lines.append("=== Statistiche Gap ===\n")
                report_lines.append(f"Gap Mediano: {median_gap:.3f} unità")
                report_lines.append(f"Gap Medio: {mean_gap:.3f} unità")
                report_lines.append(f"Deviazione Standard dei Gap: {std_gap:.3f} unità\n")

                gaps_df['Observation'] = gaps_df['Gap'].apply(lambda x: categorize_gap(x, median_gap))

                # Summary of gap categories
                category_counts = gaps_df['Observation'].value_counts()
                report_lines.append("=== Sintesi delle Categorie dei Gap ===\n")
                for category, count in category_counts.items():
                    report_lines.append(f"{category}: {count}")
                report_lines.append("\n")

                # Add gap table, limit to 100 entries
                report_lines.append("=== Analisi dei Gap ===\n")
                # Format gaps in a compact table
                formatted_gaps = gaps_df.copy()
                formatted_gaps['Entry'] = formatted_gaps.index + 1
                formatted_gaps = formatted_gaps[['Entry', 'Previous_Time', 'Current_Time', 'Gap', 'Observation']]
                formatted_gaps.rename(columns={
                    'Previous_Time': 'Time',
                    'Current_Time': 'Next Time',
                    'Gap': 'Gap (Seconds)'
                }, inplace=True)
                # Convert 'Entry' to string to allow '100+'
                formatted_gaps['Entry'] = formatted_gaps['Entry'].astype(str)
                # Limit to 100 entries
                if len(formatted_gaps) > 100:
                    displayed_gaps = formatted_gaps.head(100).copy()
                    displayed_gaps.at[99, 'Entry'] = '100+'
                else:
                    displayed_gaps = formatted_gaps
                report_lines.append(displayed_gaps.to_string(index=False, float_format='{:,.3f}'.format))
                report_lines.append("\n")
            else:
                report_lines.append("Nessun gap calcolato.\n")

            # Add path summary without duplicate distance
            report_lines.append("=== Sintesi dei Percorsi ===\n")
            report_lines.append(summary_summary.to_string(index=False, float_format='{:,.3f}'.format))
            report_lines.append("\n")

            # Add the total km and number of PathIDs only once
            total_paths = summary_summary['PathID'].nunique()
            report_lines.append(f"Numero totale di PathID: {total_paths}\n")

            # Salva il TXT finale
            final_report_path = os.path.splitext(output_shp)[0] + "_final_report.txt"
            with open(final_report_path, 'w', encoding='utf-8') as report_file:
                for line in report_lines:
                    report_file.write(line + "\n")

            print(f"Fatto: Il report finale è stato salvato in '{final_report_path}'.\n")
        except Exception as e:
            print(f"Errore nella preparazione del report finale: {e}\n")
            continue

        # Step 23: Un piccolo sommario della console
        try:
            unique_path_ids = summary_summary['PathID'].nunique()
            print(f"Numero totale di PathID creati: {unique_path_ids}")
            if not gaps_df.empty:
                median_gap = gaps_df['Gap'].median()
                print(f"Gap mediano: {median_gap:.3f} unità")
            else:
                print("Nessun gap calcolato.")
            print(f"Distanza totale di tutti i percorsi: {total_distance_km:.3f} chilometri.\n")
        except Exception as e:
            print(f"Errore nella stampatura del riepilogo console: {e}\n")
            continue

        # Step 24: Continua o Esci
        try:
            cont = get_input("Vuoi continuare? (s/n)", default='s', cast_type=str).strip().lower()
            if cont != 's':
                print("Script terminato.\n")
                break
        except Exception as e:
            print(f"Errore nella decisione di continuare: {e}\n")
            break

if __name__ == "__main__":
    main()