# geoelevation/__init__.py """ GeoElevation Package. Provides functionalities to retrieve elevation data for geographic coordinates and a GUI for interactive use. """ import os import tkinter as tk from tkinter import ttk import threading import logging from typing import Optional, Tuple import queue # For thread-safe communication # Import necessary components from within the package from .elevation_manager import ElevationManager, RASTERIO_AVAILABLE # Try to import config, if not, define default try: from .config import DEFAULT_CACHE_DIR except ImportError: DEFAULT_CACHE_DIR = "elevation_cache" logging.info("GeoElevation: config.py not found, using default cache directory.") _library_elevation_manager: Optional[ElevationManager] = None _manager_lock = threading.Lock() def _get_library_manager() -> ElevationManager: global _library_elevation_manager if not RASTERIO_AVAILABLE: raise RuntimeError("Rasterio library is not installed, required for elevation retrieval.") with _manager_lock: if _library_elevation_manager is None: try: cache_dir = os.environ.get("GEOELEVATION_CACHE_DIR", DEFAULT_CACHE_DIR) _library_elevation_manager = ElevationManager(tile_directory=cache_dir) logging.info(f"GeoElevation library: Initialized ElevationManager (cache: {cache_dir})") except Exception as e: logging.error(f"GeoElevation library: Failed to init ElevationManager: {e}", exc_info=True) raise RuntimeError(f"Failed to initialize ElevationManager: {e}") from e return _library_elevation_manager class _ProgressWindow(threading.Thread): """ A simple non-blocking progress window using Tkinter, designed to be controlled from another thread. """ def __init__(self, title: str = "GeoElevation", message: str = "Processing... Please wait."): super().__init__(daemon=True) self.title = title self.message = message self.root: Optional[tk.Tk] = None self.ready_event = threading.Event() # Signals when Tkinter root is ready self._stop_event = threading.Event() # Signals the Tkinter loop to stop def run(self): """Runs the Tkinter event loop for the progress window in this thread.""" try: self.root = tk.Tk() self.root.title(self.title) window_width = 350 window_height = 100 screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() center_x = int(screen_width/2 - window_width/2) center_y = int(screen_height/2 - window_height/2) self.root.geometry(f'{window_width}x{window_height}+{center_x}+{center_y}') self.root.resizable(False, False) # self.root.attributes('-topmost', True) # Optional: keep window on top main_frame = ttk.Frame(self.root, padding="10") main_frame.pack(expand=True, fill=tk.BOTH) ttk.Label(main_frame, text=self.message, font=("Helvetica", 10)).pack(pady=10) progress_bar = ttk.Progressbar(main_frame, mode='indeterminate', length=300) progress_bar.pack(pady=10) progress_bar.start(20) # Animation speed # Override close button ('X') to signal stop self.root.protocol("WM_DELETE_WINDOW", self._signal_stop_from_tk) self.ready_event.set() # Signal that the root window is created and configured # Periodically check the stop event self._check_stop_periodic() self.root.mainloop() except Exception as e: logging.warning(f"GeoElevation ProgressWindow: Error during run: {e}", exc_info=True) self.ready_event.set() # Set event even on error to unblock caller finally: # Ensure cleanup happens if mainloop exits for any reason if self.root: try: # self.root.destroy() # This should be handled by mainloop exit pass except tk.TclError as e: # Catch Tcl errors during destroy if already destroyed if "application has been destroyed" not in str(e).lower(): logging.warning(f"GeoElevation ProgressWindow: TclError on destroy: {e}") except Exception as e_destroy: logging.warning(f"GeoElevation ProgressWindow: Exception on final destroy: {e_destroy}") logging.debug("GeoElevation ProgressWindow: Thread finished.") def _check_stop_periodic(self): """Periodically checks if the stop event has been set.""" if self._stop_event.is_set(): if self.root: try: # This is called from the Tkinter thread, so it's safe self.root.quit() # Exit mainloop cleanly # self.root.destroy() # Let mainloop exit handle destroy except Exception as e: logging.warning(f"GeoElevation ProgressWindow: Error during quit in _check_stop_periodic: {e}") else: if self.root: # Check if root still exists try: self.root.after(100, self._check_stop_periodic) # Check again in 100ms except tk.TclError: # Happens if root is destroyed externally pass def _signal_stop_from_tk(self): """Called when the window's 'X' button is pressed (from Tk thread).""" logging.debug("GeoElevation ProgressWindow: WM_DELETE_WINDOW invoked.") self._stop_event.set() # Signal the stop # The _check_stop_periodic method will then call root.quit() def request_stop(self): """Requests the progress window to stop (can be called from any thread).""" logging.debug("GeoElevation ProgressWindow: Stop requested from external thread.") self._stop_event.set() # The _check_stop_periodic method running in the Tk thread will handle the actual quit. def get_point_elevation( latitude: float, longitude: float, show_progress_dialog: bool = False, progress_dialog_message: str = "Retrieving elevation data...\nThis may take a moment." ) -> Optional[float]: """ Retrieves the elevation for a given geographic point. Can optionally display a non-blocking progress dialog. """ progress_win_thread: Optional[_ProgressWindow] = None try: manager = _get_library_manager() if not (-90 <= latitude < 90 and -180 <= longitude < 180): logging.error(f"GeoElevation: Invalid coordinates: Lat {latitude}, Lon {longitude}") return None if show_progress_dialog: logging.debug("GeoElevation: Preparing progress dialog.") progress_win_thread = _ProgressWindow(message=progress_dialog_message) progress_win_thread.start() # Wait for the Tkinter window to be ready before proceeding # This prevents the main operation from finishing before the window even appears progress_win_thread.ready_event.wait(timeout=2.0) # Timeout of 2s if not progress_win_thread.ready_event.is_set(): logging.warning("GeoElevation: Progress dialog did not become ready in time.") # Decide whether to proceed without dialog or stop # For now, proceed, but log it. logging.info(f"GeoElevation: Requesting elevation for Lat: {latitude}, Lon: {longitude}") elevation = manager.get_elevation(latitude, longitude) if elevation is None: logging.warning(f"GeoElevation: Elevation unavailable for Lat: {latitude}, Lon: {longitude}") elif isinstance(elevation, float) and elevation != elevation: # Check for NaN logging.info(f"GeoElevation: Point Lat: {latitude}, Lon: {longitude} is NoData.") else: logging.info(f"GeoElevation: Elevation for Lat: {latitude}, Lon: {longitude} is {elevation:.2f}m") return elevation except Exception as e: logging.error(f"GeoElevation: Error in get_point_elevation for ({latitude},{longitude}): {e}", exc_info=True) return None finally: if progress_win_thread: logging.debug("GeoElevation: Signaling progress dialog to stop.") progress_win_thread.request_stop() # Signal the Tkinter thread to stop progress_win_thread.join(timeout=3.0) # Wait for the thread to finish if progress_win_thread.is_alive(): logging.warning("GeoElevation: Progress dialog thread did not terminate cleanly after join.") # To make GUI accessible if someone does `from geoelevation import run_gui` def run_gui_application(): """Launches the GeoElevation GUI application.""" from .elevation_gui import ElevationApp # Keep import local to avoid circular deps if GUI imports this module import tkinter # Alias for clarity import multiprocessing # Important for PyInstaller and multiprocessing, called only when GUI runs multiprocessing.freeze_support() root_window = tkinter.Tk() try: app = ElevationApp(parent_widget=root_window) root_window.mainloop() except Exception as e_gui: logging.critical(f"GeoElevation: Failed to run GUI: {e_gui}", exc_info=True) try: err_root = tkinter.Tk() err_root.withdraw() from tkinter import messagebox # Local import messagebox.showerror("GUI Startup Error", f"Failed to start GeoElevation GUI:\n{e_gui}") err_root.destroy() except Exception: pass