SXXXXXXX_GeoElevation/elevation_gui.py
VALLONGOL 2661f5bc42 add 2D and 3D view for tile
add 2D view for tile mosaik
2025-04-14 13:20:07 +02:00

519 lines
21 KiB
Python

# 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)