214 lines
9.5 KiB
Python
214 lines
9.5 KiB
Python
# 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 |