519 lines
21 KiB
Python
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)
|