# elevation_gui.py import tkinter as tk from tkinter import ttk, messagebox import logging import math import multiprocessing import threading import os from typing import Optional, Tuple, List, Dict # Aggiungi List, Dict # Importa classi e funzioni dai moduli separati from elevation_manager import ElevationManager, RASTERIO_AVAILABLE from image_processor import ( load_prepare_single_browse, create_composite_area_image, PIL_AVAILABLE, ) from visualizer import show_image_matplotlib, show_3d_matplotlib, MATPLOTLIB_AVAILABLE logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) DEFAULT_CACHE_DIR = "elevation_cache" # === FUNZIONI TARGET PER MULTIPROCESSING (spostate fuori dalla classe) === def process_target_show_image(image_path: str, tile_name: str, window_title: str): """Target function for multiprocessing: Loads, prepares, and shows single image.""" try: # Importa qui se necessario (o assicurati siano globali e pickle-safe) from image_processor import load_prepare_single_browse, PIL_AVAILABLE from visualizer import show_image_matplotlib, MATPLOTLIB_AVAILABLE import os # Importa di nuovo se serve qui if not PIL_AVAILABLE or not MATPLOTLIB_AVAILABLE: print( "PROCESS ERROR: Required libraries (PIL/Matplotlib) not available in process." ) return prepared_image = load_prepare_single_browse(image_path, tile_name) if prepared_image: print( f"PROCESS: Showing image '{window_title}'" ) # Usa print per output da processo show_image_matplotlib(prepared_image, window_title) else: print( f"PROCESS ERROR: Could not prepare image {os.path.basename(image_path)}" ) except Exception as e: # Loggare da qui potrebbe non funzionare come atteso, usa print print(f"PROCESS ERROR in process_target_show_image: {e}") import traceback traceback.print_exc() # Stampa traceback completo nel processo figlio def process_target_show_3d( hgt_data: Optional["np.ndarray"], plot_title: str, subsample: int ): """Target function for multiprocessing: Shows 3D plot.""" try: # Importa qui se necessario from visualizer import show_3d_matplotlib, MATPLOTLIB_AVAILABLE import numpy as np # Assicurati sia importato nel processo if not MATPLOTLIB_AVAILABLE: print("PROCESS ERROR: Matplotlib not available in process.") return if hgt_data is not None: print(f"PROCESS: Showing 3D plot '{plot_title}'") # Passa i dati numpy direttamente show_3d_matplotlib(hgt_data, plot_title, subsample=subsample) else: print("PROCESS ERROR: No HGT data received for 3D plot.") except Exception as e: print(f"PROCESS ERROR in process_target_show_3d: {e}") import traceback traceback.print_exc() def process_target_create_show_area(tile_info_list: List[Dict], window_title: str): """Target function for multiprocessing: Creates composite image and shows it.""" try: # Importa qui se necessario from image_processor import create_composite_area_image, PIL_AVAILABLE from visualizer import show_image_matplotlib, MATPLOTLIB_AVAILABLE if not PIL_AVAILABLE or not MATPLOTLIB_AVAILABLE: print( "PROCESS ERROR: Required libraries (PIL/Matplotlib) not available in process." ) return print("PROCESS: Creating composite image...") composite_image = create_composite_area_image(tile_info_list) if composite_image: print(f"PROCESS: Showing composite image '{window_title}'") show_image_matplotlib(composite_image, window_title) else: print( "PROCESS ERROR: Failed to create composite image or no browse images found." ) except Exception as e: print(f"PROCESS ERROR in process_target_create_show_area: {e}") import traceback traceback.print_exc() # === FINE FUNZIONI TARGET === class ElevationApp: # ... (init e metodi _set_busy, _validate_* come prima) ... def __init__(self, parent_widget: tk.Tk, elevation_manager: ElevationManager): # ... (identico a prima) ... self.root = parent_widget self.manager = elevation_manager self.root.title("Elevation Tool") self.root.minsize(450, 350) self.last_valid_point_coords: Optional[Tuple[float, float]] = None self.last_area_coords: Optional[Tuple[float, float, float, float]] = None self.is_processing: bool = False main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) point_frame = ttk.LabelFrame( main_frame, text="Get Elevation for Point", padding="10" ) point_frame.grid(row=0, column=0, padx=5, pady=5, sticky=(tk.W, tk.E)) point_frame.columnconfigure(1, weight=1) ttk.Label(point_frame, text="Latitude:").grid( row=0, column=0, sticky=tk.W, padx=5, pady=3 ) self.lat_entry = ttk.Entry(point_frame, width=15) self.lat_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5, pady=3) self.lat_entry.insert(0, "45.0") ttk.Label(point_frame, text="Longitude:").grid( row=1, column=0, sticky=tk.W, padx=5, pady=3 ) self.lon_entry = ttk.Entry(point_frame, width=15) self.lon_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=5, pady=3) self.lon_entry.insert(0, "7.0") self.get_elevation_button = ttk.Button( point_frame, text="Get Elevation", command=self.run_get_elevation ) self.get_elevation_button.grid( row=2, column=0, columnspan=2, pady=5, sticky=(tk.W, tk.E) ) self.result_label = ttk.Label( point_frame, text="Result: ", wraplength=400, justify=tk.LEFT ) self.result_label.grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=5) action_frame = ttk.Frame(point_frame) action_frame.grid( row=4, column=0, columnspan=2, pady=(5, 0), sticky=tk.W + tk.E ) action_frame.columnconfigure(0, weight=1) action_frame.columnconfigure(1, weight=1) self.show_2d_button = ttk.Button( action_frame, text="Show Browse Image (2D)", command=self.trigger_2d_display, state=tk.DISABLED, ) self.show_2d_button.grid(row=0, column=0, padx=2, sticky=tk.W + tk.E) if not MATPLOTLIB_AVAILABLE or not PIL_AVAILABLE: self.show_2d_button.config( state=tk.DISABLED, text="Show Browse (Matplotlib/PIL N/A)" ) self.show_3d_button = ttk.Button( action_frame, text="Show DEM Tile (3D)", command=self.trigger_3d_display, state=tk.DISABLED, ) self.show_3d_button.grid(row=0, column=1, padx=2, sticky=tk.W + tk.E) if not MATPLOTLIB_AVAILABLE: self.show_3d_button.config( state=tk.DISABLED, text="Show DEM Tile (Matplotlib N/A)" ) area_frame = ttk.LabelFrame( main_frame, text="Pre-Download Tiles for Area", padding="10" ) area_frame.grid(row=1, column=0, padx=5, pady=5, sticky=(tk.W, tk.E)) area_frame.columnconfigure(1, weight=1) area_frame.columnconfigure(3, weight=1) ttk.Label(area_frame, text="Min Lat:").grid( row=0, column=0, sticky=tk.W, padx=5, pady=3 ) self.min_lat_entry = ttk.Entry(area_frame, width=10) self.min_lat_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5, pady=3) self.min_lat_entry.insert(0, "44.0") ttk.Label(area_frame, text="Max Lat:").grid( row=0, column=2, sticky=tk.W, padx=(10, 5), pady=3 ) self.max_lat_entry = ttk.Entry(area_frame, width=10) self.max_lat_entry.grid(row=0, column=3, sticky=(tk.W, tk.E), padx=5, pady=3) self.max_lat_entry.insert(0, "45.0") ttk.Label(area_frame, text="Min Lon:").grid( row=1, column=0, sticky=tk.W, padx=5, pady=3 ) self.min_lon_entry = ttk.Entry(area_frame, width=10) self.min_lon_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=5, pady=3) self.min_lon_entry.insert(0, "7.0") ttk.Label(area_frame, text="Max Lon:").grid( row=1, column=2, sticky=tk.W, padx=(10, 5), pady=3 ) self.max_lon_entry = ttk.Entry(area_frame, width=10) self.max_lon_entry.grid(row=1, column=3, sticky=(tk.W, tk.E), padx=5, pady=3) self.max_lon_entry.insert(0, "8.0") self.download_area_button = ttk.Button( area_frame, text="Download Area Tiles", command=self.run_download_area ) self.download_area_button.grid( row=2, column=0, columnspan=4, pady=10, sticky=(tk.W, tk.E) ) self.show_area_button = ttk.Button( area_frame, text="Show Area Browse Images (2D)", command=self.trigger_area_display, state=tk.DISABLED, ) self.show_area_button.grid( row=3, column=0, columnspan=4, pady=5, sticky=tk.W + tk.E ) if not MATPLOTLIB_AVAILABLE or not PIL_AVAILABLE: self.show_area_button.config( state=tk.DISABLED, text="Show Area (Matplotlib/PIL N/A)" ) self.download_status_label = ttk.Label( area_frame, text="Status: Idle", wraplength=400, justify=tk.LEFT ) self.download_status_label.grid( row=4, column=0, columnspan=4, sticky=tk.W, pady=5 ) main_frame.columnconfigure(0, weight=1) main_frame.rowconfigure(0, weight=0) main_frame.rowconfigure(1, weight=0) def _set_busy(self, busy: bool): self.is_processing = busy state = tk.DISABLED if busy else tk.NORMAL self.get_elevation_button.config(state=state) self.download_area_button.config(state=state) def _validate_coordinates( self, lat_str: str, lon_str: str ) -> Optional[Tuple[float, float]]: try: if not lat_str: raise ValueError("Latitude empty.") lat = float(lat_str) if not (-90 <= lat < 90): raise ValueError("Latitude out of range [-90, 90).") if not lon_str: raise ValueError("Longitude empty.") lon = float(lon_str) if not (-180 <= lon < 180): raise ValueError("Longitude out of range [-180, 180).") return lat, lon except ValueError as e: logging.error(f"Invalid coordinate input: {e}") messagebox.showerror( "Input Error", f"Invalid coordinate input:\n{e}", parent=self.root ) return None def _validate_area_bounds(self) -> Optional[Tuple[float, float, float, float]]: try: min_lat = float(self.min_lat_entry.get()) max_lat = float(self.max_lat_entry.get()) min_lon = float(self.min_lon_entry.get()) max_lon = float(self.max_lon_entry.get()) if not ( -90 <= min_lat < 90 and -90 <= max_lat < 90 and -180 <= min_lon < 180 and -180 <= max_lon < 180 ): raise ValueError("Coordinates out of valid range.") if min_lat >= max_lat: raise ValueError("Min Lat >= Max Lat.") if min_lon >= max_lon: raise ValueError("Min Lon >= Max Lon.") return min_lat, min_lon, max_lat, max_lon except ValueError as e: logging.error(f"Invalid area input: {e}") messagebox.showerror( "Input Error", f"Invalid area input:\n{e}", parent=self.root ) return None def run_get_elevation(self): if self.is_processing: return coords = self._validate_coordinates(self.lat_entry.get(), self.lon_entry.get()) if not coords: return latitude, longitude = coords self._set_busy(True) self.result_label.config(text="Result: Requesting elevation...") self.show_2d_button.config(state=tk.DISABLED) self.show_3d_button.config(state=tk.DISABLED) self.last_valid_point_coords = None self.root.update_idletasks() try: elevation = self.manager.get_elevation(latitude, longitude) if elevation is None: result_text = "Result: Elevation data unavailable." messagebox.showwarning( "Elevation Info", "Could not retrieve elevation data.", parent=self.root, ) elif math.isnan(elevation): result_text = "Result: Point is on a nodata area." self.last_valid_point_coords = (latitude, longitude) else: result_text = f"Result: Elevation is {elevation:.2f} meters" self.last_valid_point_coords = (latitude, longitude) self.result_label.config(text=result_text) if self.last_valid_point_coords: if MATPLOTLIB_AVAILABLE and PIL_AVAILABLE: self.show_2d_button.config(state=tk.NORMAL) if MATPLOTLIB_AVAILABLE: self.show_3d_button.config(state=tk.NORMAL) except Exception as e: logging.exception("Error during get_elevation.") messagebox.showerror("Error", f"Unexpected error:\n{e}", parent=self.root) self.result_label.config(text="Result: Error.") finally: self._set_busy(False) def run_download_area(self): if self.is_processing: return bounds = self._validate_area_bounds() if not bounds: return min_lat, min_lon, max_lat, max_lon = bounds self.last_area_coords = bounds self._set_busy(True) self.download_status_label.config(text="Status: Starting download...") self.show_area_button.config(state=tk.DISABLED) self.root.update_idletasks() thread = threading.Thread( target=self._perform_area_download_task, args=(min_lat, min_lon, max_lat, max_lon), daemon=True, ) thread.start() def _perform_area_download_task(self, min_lat, min_lon, max_lat, max_lon): final_status = "Status: Unknown error." success = False processed, obtained = 0, 0 try: self.root.after( 0, lambda: self.download_status_label.config( text="Status: Downloading/Checking tiles..." ), ) processed, obtained = self.manager.download_area( min_lat, min_lon, max_lat, max_lon ) final_status = ( f"Status: Complete. Processed {processed}, Obtained HGT {obtained}." ) success = True except Exception as e: logging.exception("Error during background area download.") final_status = f"Status: Error: {type(e).__name__}" success = False finally: self.root.after( 0, self._area_download_complete_ui, final_status, success, processed, obtained, ) def _area_download_complete_ui(self, status_message, success, processed, obtained): self.download_status_label.config(text=status_message) self._set_busy(False) if success: summary = f"Processed {processed} tiles.\nObtained {obtained} HGT files." messagebox.showinfo("Download Complete", summary, parent=self.root) if MATPLOTLIB_AVAILABLE and PIL_AVAILABLE: self.show_area_button.config(state=tk.NORMAL) else: brief_error = status_message.split(":")[-1].strip() messagebox.showerror( "Download Error", f"Area download failed: {brief_error}\nCheck logs.", parent=self.root, ) self.show_area_button.config(state=tk.DISABLED) # === Metodi Trigger usano le funzioni target esterne === def trigger_2d_display(self): if not MATPLOTLIB_AVAILABLE or not PIL_AVAILABLE: return if not self.last_valid_point_coords: logging.warning("Show 2D clicked, no valid point.") return lat, lon = self.last_valid_point_coords logging.info(f"Requesting browse image data for 2D display ({lat},{lon})") tile_info = self.manager.get_tile_info(lat, lon) if tile_info and tile_info.get("browse_available"): image_path = tile_info["browse_image_path"] tile_name = tile_info["tile_base_name"] window_title = f"Browse: {tile_name.upper()}" # Passa argomenti alla funzione target esterna process = multiprocessing.Process( target=process_target_show_image, args=(image_path, tile_name, window_title), daemon=True, ) process.start() else: logging.warning(f"Browse image not found for ({lat},{lon}).") messagebox.showinfo( "Image Info", "Browse image not available.", parent=self.root ) def trigger_3d_display(self): if not MATPLOTLIB_AVAILABLE: return if not self.last_valid_point_coords: logging.warning("Show 3D clicked, no valid point.") return lat, lon = self.last_valid_point_coords logging.info(f"Requesting HGT data for 3D display ({lat},{lon})") hgt_data = self.manager.get_hgt_data(lat, lon) if hgt_data is not None: tile_info = self.manager.get_tile_info(lat, lon) tile_name = tile_info["tile_base_name"].upper() if tile_info else "Unknown" plot_title = f"3D View: Tile {tile_name}" # Passa argomenti alla funzione target esterna process = multiprocessing.Process( target=process_target_show_3d, args=(hgt_data, plot_title), kwargs={"subsample": 10}, daemon=True, ) process.start() else: messagebox.showerror( "3D Data Error", "Could not retrieve HGT data.", parent=self.root ) def trigger_area_display(self): if not MATPLOTLIB_AVAILABLE or not PIL_AVAILABLE: return if not self.last_area_coords: logging.warning("Show Area clicked, no area.") messagebox.showinfo( "Area Info", "Please download area first.", parent=self.root ) return min_lat, min_lon, max_lat, max_lon = self.last_area_coords logging.info(f"Requesting data for composite image display for area...") tile_info_list = self.manager.get_area_tile_info( min_lat, min_lon, max_lat, max_lon ) if not tile_info_list: messagebox.showinfo("Area Info", "No tile info found.", parent=self.root) return window_title = f"Area Overview: Lat [{min_lat:.1f}-{max_lat:.1f}], Lon [{min_lon:.1f}-{max_lon:.1f}]" # Passa argomenti alla funzione target esterna process = multiprocessing.Process( target=process_target_create_show_area, args=(tile_info_list, window_title), daemon=True, ) process.start() # === RIMOSSI: Metodi _create_and_show_area_image, _show_image_process_target === # Ora sono funzioni a livello di modulo # --- Main Execution Block --- if __name__ == "__main__": multiprocessing.freeze_support() # Importante per Windows # ... (controlli dipendenze e avvio app come prima) ... if not RASTERIO_AVAILABLE: print("ERROR: Rasterio library is required...") if not PIL_AVAILABLE: print("WARNING: Pillow library (PIL) not found...") if not MATPLOTLIB_AVAILABLE: print("WARNING: Matplotlib not found...") try: manager = ElevationManager(tile_directory=DEFAULT_CACHE_DIR) except Exception as e: logging.critical(f"Failed init Manager: {e}", exc_info=True) exit(1) try: root_window = tk.Tk() app = ElevationApp(root_window, manager) root_window.mainloop() except Exception as e: logging.critical(f"App Error: {e}", exc_info=True) exit(1)