fix visualization map for point and area
This commit is contained in:
parent
aa7fb18626
commit
3a5a3db6ad
@ -37,15 +37,26 @@ from geoelevation.visualizer import SCIPY_AVAILABLE
|
|||||||
# block correctly handles the case where this specific constant might not be available from the package.
|
# block correctly handles the case where this specific constant might not be available from the package.
|
||||||
try:
|
try:
|
||||||
from geoelevation import GEOELEVATION_DEM_CACHE_DEFAULT
|
from geoelevation import GEOELEVATION_DEM_CACHE_DEFAULT
|
||||||
|
# MODIFIED: Import the deg_to_dms_string utility function from map_utils.py
|
||||||
|
# WHY: Needed to convert coordinates to DMS format for the GUI display.
|
||||||
|
# HOW: Added the import statement from the map_viewer.map_utils sub-module.
|
||||||
|
from geoelevation.map_viewer.map_utils import deg_to_dms_string
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# This fallback is if the main __init__.py itself has issues exporting the constant,
|
# This fallback is if the main __init__.py itself has issues exporting the constant,
|
||||||
# which shouldn't happen if __init__.py and config.py are correct.
|
# which shouldn't happen if __init__.py and config.py are correct.
|
||||||
GEOELEVATION_DEM_CACHE_DEFAULT = "elevation_data_cache_gui_fallback_critical"
|
GEOELEVATION_DEM_CACHE_DEFAULT = "elevation_data_cache_gui_fallback_critical"
|
||||||
|
# MODIFIED: Define a dummy deg_to_dms_string function if import fails.
|
||||||
|
# WHY: Avoid NameError if the import fails critically.
|
||||||
|
# HOW: Define a simple function that returns "Import Error".
|
||||||
|
def deg_to_dms_string(degree_value: float, coord_type: str) -> str: # type: ignore
|
||||||
|
return "Import Error"
|
||||||
|
|
||||||
logging.getLogger(__name__).critical(
|
logging.getLogger(__name__).critical(
|
||||||
"elevation_gui.py: CRITICAL - Could not import GEOELEVATION_DEM_CACHE_DEFAULT from geoelevation package. "
|
"elevation_gui.py: CRITICAL - Could not import GEOELEVATION_DEM_CACHE_DEFAULT or deg_to_dms_string from geoelevation package. "
|
||||||
f"Using fallback: {GEOELEVATION_DEM_CACHE_DEFAULT}"
|
f"Using fallback cache: {GEOELEVATION_DEM_CACHE_DEFAULT}. DMS conversion unavailable."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cv2
|
import cv2
|
||||||
MAP_VIEWER_SYSTEM_AVAILABLE = True
|
MAP_VIEWER_SYSTEM_AVAILABLE = True
|
||||||
@ -147,16 +158,20 @@ def run_map_viewer_process_target(
|
|||||||
) -> None:
|
) -> None:
|
||||||
child_logger = logging.getLogger("GeoElevationMapViewerChildProcess")
|
child_logger = logging.getLogger("GeoElevationMapViewerChildProcess")
|
||||||
# Configure logger if it hasn't been configured in this process yet
|
# Configure logger if it hasn't been configured in this process yet
|
||||||
if not child_logger.handlers:
|
if not child_logger.hasHandlers():
|
||||||
# Use basicConfig as this is a fresh process, ensure it doesn't inherit handlers from root
|
# Use basicConfig as this is a fresh process, ensure it doesn't inherit handlers from root
|
||||||
# and send output to sys.stdout/stderr which is captured by the parent process's terminal.
|
# and send output to sys.stdout/stderr which is captured by the parent process's terminal.
|
||||||
# Level should ideally be inherited or passed, but setting INFO here as a default.
|
# Level should ideally be inherited or passed, but setting INFO here as a default.
|
||||||
|
# Also ensure the geoelevation logger in this process has a handler for its messages to be seen.
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
stream=sys.stdout # Explicitly route to stdout
|
stream=sys.stdout # Explicitly route to stdout
|
||||||
)
|
)
|
||||||
|
# Ensure geoelevation logger sends messages if its level is below root default (e.g. INFO)
|
||||||
|
geoelevation_proc_logger = logging.getLogger("geoelevation")
|
||||||
|
if not geoelevation_proc_logger.hasHandlers():
|
||||||
|
geoelevation_proc_logger.addHandler(logging.StreamHandler(sys.stdout)) # Add a stream handler
|
||||||
|
|
||||||
child_map_viewer_instance: Optional[Any] = None
|
child_map_viewer_instance: Optional[Any] = None
|
||||||
child_map_system_ok = False
|
child_map_system_ok = False
|
||||||
@ -171,7 +186,9 @@ def run_map_viewer_process_target(
|
|||||||
# MODIFIED: Send an error message back to the GUI queue if critical imports fail in child process.
|
# MODIFIED: Send an error message back to the GUI queue if critical imports fail in child process.
|
||||||
# WHY: The GUI process needs to know that the map process failed to start properly.
|
# WHY: The GUI process needs to know that the map process failed to start properly.
|
||||||
# HOW: Put a specific error message in the queue before exiting.
|
# HOW: Put a specific error message in the queue before exiting.
|
||||||
|
# MODIFIED: Include DMS fields with error state for consistency with GUI update logic.
|
||||||
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
|
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
|
||||||
|
"latitude_dms_str": "Error", "longitude_dms_str": "Error", # Added DMS fields
|
||||||
"elevation_str": f"Fatal Error: {type(e_child_imp_map_corrected).__name__}",
|
"elevation_str": f"Fatal Error: {type(e_child_imp_map_corrected).__name__}",
|
||||||
"map_area_size_str": "Map System N/A"}
|
"map_area_size_str": "Map System N/A"}
|
||||||
try: map_interaction_q.put(error_payload)
|
try: map_interaction_q.put(error_payload)
|
||||||
@ -210,7 +227,9 @@ def run_map_viewer_process_target(
|
|||||||
# MODIFIED: Send an error message to the GUI queue if the mode or parameters are invalid.
|
# MODIFIED: Send an error message to the GUI queue if the mode or parameters are invalid.
|
||||||
# WHY: The GUI process needs feedback if the map process received bad instructions.
|
# WHY: The GUI process needs feedback if the map process received bad instructions.
|
||||||
# HOW: Put a specific error message in the queue before exiting.
|
# HOW: Put a specific error message in the queue before exiting.
|
||||||
|
# MODIFIED: Include DMS fields with error state for consistency with GUI update logic.
|
||||||
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
|
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
|
||||||
|
"latitude_dms_str": "Error", "longitude_dms_str": "Error", # Added DMS fields
|
||||||
"elevation_str": f"Fatal Error: Invalid Map Args",
|
"elevation_str": f"Fatal Error: Invalid Map Args",
|
||||||
"map_area_size_str": "Invalid Args"}
|
"map_area_size_str": "Invalid Args"}
|
||||||
try: map_interaction_q.put(error_payload)
|
try: map_interaction_q.put(error_payload)
|
||||||
@ -226,6 +245,7 @@ def run_map_viewer_process_target(
|
|||||||
is_map_active = True
|
is_map_active = True
|
||||||
while is_map_active:
|
while is_map_active:
|
||||||
# cv2.waitKey(milliseconds) is crucial for processing window events and mouse callbacks
|
# cv2.waitKey(milliseconds) is crucial for processing window events and mouse callbacks
|
||||||
|
# A short delay (e.g., 100ms) prevents the loop from consuming 100% CPU.
|
||||||
key = child_cv2.waitKey(100) # Check for key press every 100ms (optional, but good practice)
|
key = child_cv2.waitKey(100) # Check for key press every 100ms (optional, but good practice)
|
||||||
# Check if the window still exists and is active.
|
# Check if the window still exists and is active.
|
||||||
# is_window_alive() uses cv2.getWindowProperty which is thread-safe.
|
# is_window_alive() uses cv2.getWindowProperty which is thread-safe.
|
||||||
@ -234,9 +254,19 @@ def run_map_viewer_process_target(
|
|||||||
# Optional: Check for specific keys to close the window (e.g., 'q' or Escape)
|
# Optional: Check for specific keys to close the window (e.g., 'q' or Escape)
|
||||||
if key != -1: # A key was pressed
|
if key != -1: # A key was pressed
|
||||||
child_logger.debug(f"Map window received key press: {key}")
|
child_logger.debug(f"Map window received key press: {key}")
|
||||||
if key == ord('q') or key == 27: # 'q' or Escape key
|
# Convert key code to character if it's printable for logging
|
||||||
child_logger.info("Map window closing due to 'q' or Escape key press.")
|
try:
|
||||||
|
char_key = chr(key & 0xFF) # Mask to get ASCII value
|
||||||
|
logger.debug(f"Key code {key} corresponds to character '{char_key}'.")
|
||||||
|
if char_key in ('q', 'Q'): # 'q' or 'Q' key
|
||||||
|
child_logger.info("Map window closing due to 'q'/'Q' key press.")
|
||||||
|
is_map_active = False # Signal loop to exit
|
||||||
|
except ValueError: # Non-printable key
|
||||||
|
pass # Just ignore non-printable keys
|
||||||
|
if key == 27: # Escape key
|
||||||
|
child_logger.info("Map window closing due to Escape key press.")
|
||||||
is_map_active = False # Signal loop to exit
|
is_map_active = False # Signal loop to exit
|
||||||
|
|
||||||
else:
|
else:
|
||||||
child_logger.info("Map window reported as not alive by is_window_alive().")
|
child_logger.info("Map window reported as not alive by is_window_alive().")
|
||||||
is_map_active = False # Signal loop to exit if window closed by user 'X' button
|
is_map_active = False # Signal loop to exit if window closed by user 'X' button
|
||||||
@ -252,7 +282,9 @@ def run_map_viewer_process_target(
|
|||||||
# MODIFIED: Send a fatal error message back to the GUI queue if an exception occurs in the main loop.
|
# MODIFIED: Send a fatal error message back to the GUI queue if an exception occurs in the main loop.
|
||||||
# WHY: The GUI process needs to know if the map process crashed unexpectedly.
|
# WHY: The GUI process needs to know if the map process crashed unexpectedly.
|
||||||
# HOW: Put a specific error message in the queue before exiting.
|
# HOW: Put a specific error message in the queue before exiting.
|
||||||
|
# MODIFIED: Include DMS fields with error state for consistency with GUI update logic.
|
||||||
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
|
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
|
||||||
|
"latitude_dms_str": "Error", "longitude_dms_str": "Error", # Added DMS fields
|
||||||
"elevation_str": f"Fatal Error: {type(e_map_proc_child_fatal_final).__name__}",
|
"elevation_str": f"Fatal Error: {type(e_map_proc_child_fatal_final).__name__}",
|
||||||
"map_area_size_str": "Fatal Error"}
|
"map_area_size_str": "Fatal Error"}
|
||||||
try: map_interaction_q.put(error_payload)
|
try: map_interaction_q.put(error_payload)
|
||||||
@ -306,7 +338,10 @@ class ElevationApp:
|
|||||||
self.elevation_manager = None
|
self.elevation_manager = None
|
||||||
|
|
||||||
self.root.title("GeoElevation Tool")
|
self.root.title("GeoElevation Tool")
|
||||||
self.root.minsize(480, 520)
|
# MODIFIED: Increased minsize height to accommodate new info fields.
|
||||||
|
# WHY: Need more space for decimal and DMS coordinate fields.
|
||||||
|
# HOW: Changed the minsize tuple.
|
||||||
|
self.root.minsize(480, 580) # Increased height
|
||||||
|
|
||||||
self.last_valid_point_coords: Optional[Tuple[float, float]] = None
|
self.last_valid_point_coords: Optional[Tuple[float, float]] = None
|
||||||
self.last_area_coords: Optional[Tuple[float, float, float, float]] = None
|
self.last_area_coords: Optional[Tuple[float, float, float, float]] = None
|
||||||
@ -330,6 +365,17 @@ class ElevationApp:
|
|||||||
# HOW: Assigned the resolved default_dem_cache to a new instance attribute.
|
# HOW: Assigned the resolved default_dem_cache to a new instance attribute.
|
||||||
self._dem_data_cache_dir_for_map_process = default_dem_cache
|
self._dem_data_cache_dir_for_map_process = default_dem_cache
|
||||||
|
|
||||||
|
# MODIFIED: Declare StringVar instances for the Map Info Entry widgets.
|
||||||
|
# WHY: Tkinter Entry widgets are typically managed via associated StringVar instances for dynamic updates.
|
||||||
|
# HOW: Declared new instance attributes of type tk.StringVar.
|
||||||
|
self.map_lat_decimal_var: tk.StringVar = tk.StringVar(value="N/A")
|
||||||
|
self.map_lon_decimal_var: tk.StringVar = tk.StringVar(value="N/A")
|
||||||
|
self.map_lat_dms_var: tk.StringVar = tk.StringVar(value="N/A") # Added DMS variables
|
||||||
|
self.map_lon_dms_var: tk.StringVar = tk.StringVar(value="N/A") # Added DMS variables
|
||||||
|
self.map_elevation_var: tk.StringVar = tk.StringVar(value="N/A")
|
||||||
|
self.map_area_size_var: tk.StringVar = tk.StringVar(value="N/A")
|
||||||
|
|
||||||
|
|
||||||
if MAP_VIEWER_SYSTEM_AVAILABLE:
|
if MAP_VIEWER_SYSTEM_AVAILABLE:
|
||||||
try:
|
try:
|
||||||
self.map_interaction_message_queue = multiprocessing.Queue()
|
self.map_interaction_message_queue = multiprocessing.Queue()
|
||||||
@ -451,22 +497,53 @@ class ElevationApp:
|
|||||||
# HOW: Updated the text option in the LabelFrame constructor.
|
# HOW: Updated the text option in the LabelFrame constructor.
|
||||||
map_info_frame = ttk.LabelFrame(main_app_frame, text="Map Info", padding="10")
|
map_info_frame = ttk.LabelFrame(main_app_frame, text="Map Info", padding="10")
|
||||||
map_info_frame.grid(row=3, column=0, padx=5, pady=5, sticky=(tk.W, tk.E))
|
map_info_frame.grid(row=3, column=0, padx=5, pady=5, sticky=(tk.W, tk.E))
|
||||||
map_info_frame.columnconfigure(1, weight=1)
|
# MODIFIED: Configured columns to stretch, important for Entry widgets
|
||||||
ttk.Label(map_info_frame, text="Latitude:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=2)
|
# WHY: Allows the Entry widgets to fill available horizontal space.
|
||||||
self.map_click_latitude_label = ttk.Label(map_info_frame, text="N/A") # Keep name for now for less refactoring
|
# HOW: Added columnconfigure calls.
|
||||||
self.map_click_latitude_label.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
map_info_frame.columnconfigure(1, weight=1) # Column for Decimal/Elevation entries
|
||||||
ttk.Label(map_info_frame, text="Longitude:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=2)
|
map_info_frame.columnconfigure(3, weight=1) # Column for DMS entries
|
||||||
self.map_click_longitude_label = ttk.Label(map_info_frame, text="N/A") # Keep name for now
|
|
||||||
self.map_click_longitude_label.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
# --- Map Info Widgets ---
|
||||||
ttk.Label(map_info_frame, text="Elevation:").grid(row=2, column=0, sticky=tk.W, padx=5, pady=2)
|
# MODIFIED: Replace ttk.Label for displaying values with ttk.Entry (readonly)
|
||||||
self.map_click_elevation_label = ttk.Label(map_info_frame, text="N/A") # Keep name for now
|
# WHY: To make the displayed information copyable.
|
||||||
self.map_click_elevation_label.grid(row=2, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
# HOW: Changed widget type, configured state and textvariable, adjusted grid row/column.
|
||||||
|
ttk.Label(map_info_frame, text="Latitude (Dec):").grid(row=0, column=0, sticky=tk.W, padx=5, pady=2)
|
||||||
|
# self.map_click_latitude_label = ttk.Label(map_info_frame, text="N/A") # Old Label
|
||||||
|
self.map_lat_decimal_entry = ttk.Entry(map_info_frame, textvariable=self.map_lat_decimal_var, state="readonly", width=25)
|
||||||
|
self.map_lat_decimal_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
||||||
|
|
||||||
|
ttk.Label(map_info_frame, text="Longitude (Dec):").grid(row=1, column=0, sticky=tk.W, padx=5, pady=2)
|
||||||
|
# self.map_click_longitude_label = ttk.Label(map_info_frame, text="N/A") # Old Label
|
||||||
|
self.map_lon_decimal_entry = ttk.Entry(map_info_frame, textvariable=self.map_lon_decimal_var, state="readonly", width=25)
|
||||||
|
self.map_lon_decimal_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
||||||
|
|
||||||
|
# MODIFIED: Add new Labels and Entries for DMS format.
|
||||||
|
# WHY: To display coordinates in DMS format as requested.
|
||||||
|
# HOW: Added new rows with Labels and Entry widgets.
|
||||||
|
ttk.Label(map_info_frame, text="Latitude (DMS):").grid(row=2, column=0, sticky=tk.W, padx=5, pady=2)
|
||||||
|
self.map_lat_dms_entry = ttk.Entry(map_info_frame, textvariable=self.map_lat_dms_var, state="readonly", width=25)
|
||||||
|
self.map_lat_dms_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
||||||
|
|
||||||
|
ttk.Label(map_info_frame, text="Longitude (DMS):").grid(row=3, column=0, sticky=tk.W, padx=5, pady=2)
|
||||||
|
self.map_lon_dms_entry = ttk.Entry(map_info_frame, textvariable=self.map_lon_dms_var, state="readonly", width=25)
|
||||||
|
self.map_lon_dms_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
||||||
|
|
||||||
|
|
||||||
|
ttk.Label(map_info_frame, text="Elevation:").grid(row=4, column=0, sticky=tk.W, padx=5, pady=2)
|
||||||
|
# self.map_click_elevation_label = ttk.Label(map_info_frame, text="N/A") # Old Label
|
||||||
|
self.map_elevation_entry = ttk.Entry(map_info_frame, textvariable=self.map_elevation_var, state="readonly", width=25)
|
||||||
|
self.map_elevation_entry.grid(row=4, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
||||||
|
|
||||||
# MODIFIED: Added a new Label for displaying the map area size.
|
# MODIFIED: Added a new Label for displaying the map area size.
|
||||||
# WHY: To fulfill the requirement to show the size of the currently displayed map patch.
|
# WHY: To fulfill the requirement to show the size of the currently displayed map patch.
|
||||||
# HOW: Created and gridded a new ttk.Label.
|
# HOW: Created and gridded a new ttk.Label.
|
||||||
ttk.Label(map_info_frame, text="Map Area Size:").grid(row=3, column=0, sticky=tk.W, padx=5, pady=2)
|
ttk.Label(map_info_frame, text="Map Area Size:").grid(row=5, column=0, sticky=tk.W, padx=5, pady=2)
|
||||||
self.map_area_size_label = ttk.Label(map_info_frame, text="N/A", wraplength=300, justify=tk.LEFT)
|
# MODIFIED: Make Area Size display also an Entry for copyability.
|
||||||
self.map_area_size_label.grid(row=3, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
# WHY: Consistency and copyability for all map info fields.
|
||||||
|
# HOW: Changed widget type, configured state and textvariable.
|
||||||
|
# self.map_area_size_label = ttk.Label(map_info_frame, text="N/A", wraplength=300, justify=tk.LEFT) # Old Label
|
||||||
|
self.map_area_size_entry = ttk.Entry(map_info_frame, textvariable=self.map_area_size_var, state="readonly", width=25) # Increased width slightly
|
||||||
|
self.map_area_size_entry.grid(row=5, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
||||||
|
|
||||||
|
|
||||||
main_app_frame.columnconfigure(0, weight=1)
|
main_app_frame.columnconfigure(0, weight=1)
|
||||||
@ -517,6 +594,23 @@ class ElevationApp:
|
|||||||
if not MATPLOTLIB_AVAILABLE:
|
if not MATPLOTLIB_AVAILABLE:
|
||||||
self.show_3d_dem_button.config(text="DEM (MPL N/A)")
|
self.show_3d_dem_button.config(text="DEM (MPL N/A)")
|
||||||
|
|
||||||
|
# MODIFIED: Set initial text for Map Info Entry widgets.
|
||||||
|
# WHY: Display default "N/A" and ensure text variables are linked correctly.
|
||||||
|
# HOW: Use set() on the StringVar instances.
|
||||||
|
self.map_lat_decimal_var.set("N/A")
|
||||||
|
self.map_lon_decimal_var.set("N/A")
|
||||||
|
self.map_lat_dms_var.set("N/A") # Set initial value for DMS fields
|
||||||
|
self.map_lon_dms_var.set("N/A") # Set initial value for DMS fields
|
||||||
|
self.map_elevation_var.set("N/A")
|
||||||
|
self.map_area_size_var.set("N/A")
|
||||||
|
|
||||||
|
# MODIFIED: Ensure Map Info Entry widgets are in readonly state initially.
|
||||||
|
# WHY: They should not be editable by the user.
|
||||||
|
# HOW: Set the 'state' option on the Entry widgets. (Already done in _build_gui_layout, but can double-check here if needed).
|
||||||
|
# self.map_lat_decimal_entry.config(state="readonly") # Redundant with _build_gui_layout
|
||||||
|
# ... and so on for other map info entries.
|
||||||
|
|
||||||
|
|
||||||
def _on_map_display_scale_changed(self, event: Optional[tk.Event] = None) -> None:
|
def _on_map_display_scale_changed(self, event: Optional[tk.Event] = None) -> None:
|
||||||
selected_display_text = self.map_scale_combobox.get()
|
selected_display_text = self.map_scale_combobox.get()
|
||||||
new_numeric_scale = self.scale_options_map.get(selected_display_text)
|
new_numeric_scale = self.scale_options_map.get(selected_display_text)
|
||||||
@ -561,10 +655,10 @@ class ElevationApp:
|
|||||||
try:
|
try:
|
||||||
if not lat_s: raise ValueError("Latitude empty.")
|
if not lat_s: raise ValueError("Latitude empty.")
|
||||||
lat_v = float(lat_s.strip())
|
lat_v = float(lat_s.strip())
|
||||||
if not (-90.0 <= lat_v < 90.0): raise ValueError("Latitude out of [-90, 90).")
|
if not (-90.0 <= lat_v <= 90.0): raise ValueError("Latitude out of [-90, 90].") # Allow 90 exactly for validation
|
||||||
if not lon_s: raise ValueError("Longitude empty.")
|
if not lon_s: raise ValueError("Longitude empty.")
|
||||||
lon_v = float(lon_s.strip())
|
lon_v = float(lon_s.strip())
|
||||||
if not (-180.0 <= lon_v < 180.0): raise ValueError("Longitude out of [-180, 180).")
|
if not (-180.0 <= lon_v <= 180.0): raise ValueError("Longitude out of [-180, 180].") # Allow 180 exactly for validation
|
||||||
return lat_v, lon_v
|
return lat_v, lon_v
|
||||||
except ValueError as e: logger.error(f"Invalid coordinate: {e}"); messagebox.showerror("Input Error", f"Invalid coordinate:\n{e}", parent=self.root); return None
|
except ValueError as e: logger.error(f"Invalid coordinate: {e}"); messagebox.showerror("Input Error", f"Invalid coordinate:\n{e}", parent=self.root); return None
|
||||||
|
|
||||||
@ -574,10 +668,18 @@ class ElevationApp:
|
|||||||
min_os,max_os = self.min_longitude_entry.get().strip(), self.max_longitude_entry.get().strip()
|
min_os,max_os = self.min_longitude_entry.get().strip(), self.max_longitude_entry.get().strip()
|
||||||
if not all([min_ls, max_ls, min_os, max_os]): raise ValueError("All bounds must be filled.")
|
if not all([min_ls, max_ls, min_os, max_os]): raise ValueError("All bounds must be filled.")
|
||||||
min_l,max_l,min_o,max_o = float(min_ls),float(max_ls),float(min_os),float(max_os)
|
min_l,max_l,min_o,max_o = float(min_ls),float(max_ls),float(min_os),float(max_os)
|
||||||
if not (-90<=min_l<90 and -90<=max_l<90 and -180<=min_o<180 and -180<=max_o<180):
|
# MODIFIED: Adjusted validation range to allow 90 and 180 exactly.
|
||||||
raise ValueError("Coordinates out of valid range.")
|
# WHY: Standard geographic range includes these boundaries.
|
||||||
if min_l >= max_l: raise ValueError("Min Lat >= Max Lat.")
|
# HOW: Changed < 90.0 to <= 90.0 and < 180.0 to <= 180.0.
|
||||||
if min_o >= max_o: raise ValueError("Min Lon >= Max Lon.")
|
if not (-90<=min_l<=90 and -90<=max_l<=90 and -180<=min_o<=180 and -180<=max_o<=180):
|
||||||
|
raise ValueError("Coordinates out of valid range [-90, 90] / [-180, 180].")
|
||||||
|
# The order check should still be strict for a defined bounding box area.
|
||||||
|
if min_l >= max_l: raise ValueError("Min Lat must be less than Max Lat.")
|
||||||
|
# Handle potential longitude wrap-around if min_o > max_o but the range is valid (e.g., crosses antimeridian)
|
||||||
|
# For simplicity in GUI input, let's assume min_o should generally be <= max_o unless explicitly designing for wrap-around input.
|
||||||
|
# Let's keep the simple check for now.
|
||||||
|
if min_o >= max_o: raise ValueError("Min Lon must be less than Max Lon.") # This simplifies downstream logic
|
||||||
|
|
||||||
return min_l, min_o, max_l, max_o
|
return min_l, min_o, max_l, max_o
|
||||||
except ValueError as e: logger.error(f"Invalid area: {e}"); messagebox.showerror("Input Error", f"Invalid area:\n{e}", parent=self.root); return None
|
except ValueError as e: logger.error(f"Invalid area: {e}"); messagebox.showerror("Input Error", f"Invalid area:\n{e}", parent=self.root); return None
|
||||||
|
|
||||||
@ -585,7 +687,13 @@ class ElevationApp:
|
|||||||
"""Starts a background thread to get elevation for a point."""
|
"""Starts a background thread to get elevation for a point."""
|
||||||
if self.is_processing_task or not self.elevation_manager: return
|
if self.is_processing_task or not self.elevation_manager: return
|
||||||
coords = self._validate_point_coordinates(self.latitude_entry.get(), self.longitude_entry.get())
|
coords = self._validate_point_coordinates(self.latitude_entry.get(), self.longitude_entry.get())
|
||||||
if not coords: return
|
if not coords:
|
||||||
|
# MODIFIED: Clear Map Info fields if point validation fails.
|
||||||
|
# WHY: Avoids showing stale info if the user enters invalid coords.
|
||||||
|
# HOW: Call the UI update method with None values.
|
||||||
|
self._get_elevation_task_complete_ui_update(None, ValueError("Invalid point coordinates"), None, None)
|
||||||
|
return # Exit if validation fails
|
||||||
|
|
||||||
lat, lon = coords
|
lat, lon = coords
|
||||||
|
|
||||||
# MODIFIED: Start busy state and update status label immediately on GUI thread.
|
# MODIFIED: Start busy state and update status label immediately on GUI thread.
|
||||||
@ -593,6 +701,17 @@ class ElevationApp:
|
|||||||
# HOW: Call _set_busy_state(True) and update label text.
|
# HOW: Call _set_busy_state(True) and update label text.
|
||||||
self._set_busy_state(True)
|
self._set_busy_state(True)
|
||||||
self.point_result_label.config(text="Result: Requesting elevation... Please wait.")
|
self.point_result_label.config(text="Result: Requesting elevation... Please wait.")
|
||||||
|
# MODIFIED: Clear Map Info fields when a new Get Elevation task starts.
|
||||||
|
# WHY: Indicate that new information is being fetched.
|
||||||
|
# HOW: Set StringVar values to "..." or empty.
|
||||||
|
self.map_lat_decimal_var.set("...")
|
||||||
|
self.map_lon_decimal_var.set("...")
|
||||||
|
self.map_lat_dms_var.set("...") # Clear DMS fields
|
||||||
|
self.map_lon_dms_var.set("...") # Clear DMS fields
|
||||||
|
self.map_elevation_var.set("...")
|
||||||
|
self.map_area_size_var.set("N/A (Point)") # Indicate this is for a point
|
||||||
|
|
||||||
|
|
||||||
self.root.update_idletasks() # Force GUI update
|
self.root.update_idletasks() # Force GUI update
|
||||||
|
|
||||||
|
|
||||||
@ -631,6 +750,9 @@ class ElevationApp:
|
|||||||
# MODIFIED: Use root.after to call the UI update method on the main GUI thread.
|
# MODIFIED: Use root.after to call the UI update method on the main GUI thread.
|
||||||
# WHY: Tkinter GUI updates must happen on the main thread.
|
# WHY: Tkinter GUI updates must happen on the main thread.
|
||||||
# HOW: Schedule _get_elevation_task_complete_ui_update call.
|
# HOW: Schedule _get_elevation_task_complete_ui_update call.
|
||||||
|
# MODIFIED: Pass original coords even on error for UI update context.
|
||||||
|
# WHY: The UI update method needs the original coordinates to populate the entry fields, even if the elevation fetch failed.
|
||||||
|
# HOW: Pass latitude, longitude as arguments.
|
||||||
self.root.after(
|
self.root.after(
|
||||||
0, # Schedule to run as soon as the main loop is free
|
0, # Schedule to run as soon as the main loop is free
|
||||||
self._get_elevation_task_complete_ui_update, # Callback function
|
self._get_elevation_task_complete_ui_update, # Callback function
|
||||||
@ -642,33 +764,93 @@ class ElevationApp:
|
|||||||
# MODIFIED: Added a new UI update function for point elevation task completion.
|
# MODIFIED: Added a new UI update function for point elevation task completion.
|
||||||
# WHY: Centralizes the logic to update GUI elements after the background task finishes.
|
# WHY: Centralizes the logic to update GUI elements after the background task finishes.
|
||||||
# HOW: Created this method to update labels and button states.
|
# HOW: Created this method to update labels and button states.
|
||||||
|
# MODIFIED: Updated to populate Map Info Entry widgets with Decimal and DMS coordinates and Elevation.
|
||||||
|
# WHY: Implement Feature 3 - show coordinates in Entry widgets and in DMS format.
|
||||||
|
# HOW: Access StringVar instances and set their values. Use deg_to_dms_string for conversion.
|
||||||
def _get_elevation_task_complete_ui_update(
|
def _get_elevation_task_complete_ui_update(
|
||||||
self,
|
self,
|
||||||
elevation_result: Optional[float],
|
elevation_result: Optional[float],
|
||||||
exception_occurred: Optional[Exception],
|
exception_occurred: Optional[Exception],
|
||||||
original_latitude: float,
|
original_latitude: Optional[float], # Now Optional as it might be None on validation failure
|
||||||
original_longitude: float
|
original_longitude: Optional[float] # Now Optional as it might be None on validation failure
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Updates GUI elements after a point elevation task completes."""
|
"""Updates GUI elements after a point elevation task completes."""
|
||||||
res_txt = "Result: "
|
res_txt = "Result: "
|
||||||
self.last_valid_point_coords = None # Reset valid point state initially
|
self.last_valid_point_coords = None # Reset valid point state initially
|
||||||
|
|
||||||
if exception_occurred:
|
# MODIFIED: Clear/reset Map Info fields if coordinates were not successfully obtained or were invalid.
|
||||||
|
# WHY: Ensure the Map Info panel reflects the status correctly.
|
||||||
|
# HOW: Reset StringVars to "N/A".
|
||||||
|
if original_latitude is None or original_longitude is None:
|
||||||
|
res_txt += f"Error: {type(exception_occurred).__name__} (Invalid Input)" if exception_occurred else "Input Error."
|
||||||
|
logger.error(f"GUI: Point elevation task completed with invalid input.")
|
||||||
|
self.map_lat_decimal_var.set("N/A")
|
||||||
|
self.map_lon_decimal_var.set("N/A")
|
||||||
|
self.map_lat_dms_var.set("N/A")
|
||||||
|
self.map_lon_dms_var.set("N/A")
|
||||||
|
self.map_elevation_var.set("N/A")
|
||||||
|
self.map_area_size_var.set("N/A") # Area size doesn't apply to a single point result, but clear it.
|
||||||
|
|
||||||
|
elif exception_occurred:
|
||||||
res_txt += f"Error: {type(exception_occurred).__name__}"
|
res_txt += f"Error: {type(exception_occurred).__name__}"
|
||||||
messagebox.showerror("Error", f"Error retrieving elevation:\n{exception_occurred}", parent=self.root)
|
messagebox.showerror("Error", f"Error retrieving elevation:\n{exception_occurred}", parent=self.root)
|
||||||
logger.error(f"GUI: Point elevation task completed with error for ({original_latitude:.5f},{original_longitude:.5f}).")
|
logger.error(f"GUI: Point elevation task completed with error for ({original_latitude:.5f},{original_longitude:.5f}).")
|
||||||
|
# MODIFIED: Update Map Info fields with error state.
|
||||||
|
# WHY: Show the error message in the GUI panel.
|
||||||
|
# HOW: Set StringVars.
|
||||||
|
self.map_lat_decimal_var.set(f"{original_latitude:.5f}") # Show input coords
|
||||||
|
self.map_lon_decimal_var.set(f"{original_longitude:.5f}")
|
||||||
|
# MODIFIED: Convert available coordinates to DMS even if elevation failed, if the coords are valid.
|
||||||
|
self.map_lat_dms_var.set(deg_to_dms_string(original_latitude, 'lat'))
|
||||||
|
self.map_lon_dms_var.set(deg_to_dms_string(original_longitude, 'lon'))
|
||||||
|
self.map_elevation_var.set(f"Error: {type(exception_occurred).__name__}")
|
||||||
|
self.map_area_size_var.set("N/A") # Area size doesn't apply
|
||||||
|
|
||||||
elif elevation_result is None:
|
elif elevation_result is None:
|
||||||
res_txt += "Data unavailable."
|
res_txt += "Data unavailable."
|
||||||
messagebox.showwarning("Info", "Could not retrieve elevation for the point.", parent=self.root)
|
messagebox.showwarning("Info", "Could not retrieve elevation for the point.", parent=self.root)
|
||||||
logger.warning(f"GUI: Point elevation task completed, data unavailable for ({original_latitude:.5f},{original_longitude:.5f}).")
|
logger.warning(f"GUI: Point elevation task completed, data unavailable for ({original_latitude:.5f},{original_longitude:.5f}).")
|
||||||
|
# MODIFIED: Update Map Info fields for data unavailable state.
|
||||||
|
# WHY: Show results in the GUI panel.
|
||||||
|
# HOW: Set StringVars.
|
||||||
|
self.map_lat_decimal_var.set(f"{original_latitude:.5f}")
|
||||||
|
self.map_lon_decimal_var.set(f"{original_longitude:.5f}")
|
||||||
|
self.map_lat_dms_var.set(deg_to_dms_string(original_latitude, 'lat'))
|
||||||
|
self.map_lon_dms_var.set(deg_to_dms_string(original_longitude, 'lon'))
|
||||||
|
self.map_elevation_var.set("Unavailable")
|
||||||
|
self.map_area_size_var.set("N/A") # Area size doesn't apply
|
||||||
|
self.last_valid_point_coords = (original_latitude, original_longitude) # Coords were valid, but no data
|
||||||
|
|
||||||
elif isinstance(elevation_result, float) and math.isnan(elevation_result):
|
elif isinstance(elevation_result, float) and math.isnan(elevation_result):
|
||||||
res_txt += "Point on NoData area."
|
res_txt += "Point on NoData area."
|
||||||
self.last_valid_point_coords = (original_latitude, original_longitude) # Valid coords, result is NoData
|
|
||||||
logger.info(f"GUI: Point elevation task completed, NoData for ({original_latitude:.5f},{original_longitude:.5f}).")
|
logger.info(f"GUI: Point elevation task completed, NoData for ({original_latitude:.5f},{original_longitude:.5f}).")
|
||||||
|
# MODIFIED: Update Map Info fields for NoData state.
|
||||||
|
# WHY: Show results in the GUI panel.
|
||||||
|
# HOW: Set StringVars.
|
||||||
|
self.map_lat_decimal_var.set(f"{original_latitude:.5f}")
|
||||||
|
self.map_lon_decimal_var.set(f"{original_longitude:.5f}")
|
||||||
|
self.map_lat_dms_var.set(deg_to_dms_string(original_latitude, 'lat'))
|
||||||
|
self.map_lon_dms_var.set(deg_to_dms_string(original_longitude, 'lon'))
|
||||||
|
self.map_elevation_var.set("NoData")
|
||||||
|
self.map_area_size_var.set("N/A") # Area size doesn't apply
|
||||||
|
self.last_valid_point_coords = (original_latitude, original_longitude) # Valid coords, result is NoData
|
||||||
|
|
||||||
else:
|
else:
|
||||||
res_txt += f"Elevation {elevation_result:.2f}m"
|
res_txt += f"Elevation {elevation_result:.2f}m"
|
||||||
|
logger.info(
|
||||||
|
f"GUI: Point elevation task completed, elevation {elevation_result:.2f}m for ({original_latitude:.5f},{original_longitude:.5f})."
|
||||||
|
)
|
||||||
|
# MODIFIED: Update Map Info fields for successful elevation retrieval.
|
||||||
|
# WHY: Show results in the GUI panel.
|
||||||
|
# HOW: Set StringVars.
|
||||||
|
self.map_lat_decimal_var.set(f"{original_latitude:.5f}")
|
||||||
|
self.map_lon_decimal_var.set(f"{original_longitude:.5f}")
|
||||||
|
self.map_lat_dms_var.set(deg_to_dms_string(original_latitude, 'lat'))
|
||||||
|
self.map_lon_dms_var.set(deg_to_dms_string(original_longitude, 'lon'))
|
||||||
|
self.map_elevation_var.set(f"{elevation_result:.2f} m")
|
||||||
|
self.map_area_size_var.set("N/A") # Area size doesn't apply
|
||||||
self.last_valid_point_coords = (original_latitude, original_longitude) # Valid coords, got elevation
|
self.last_valid_point_coords = (original_latitude, original_longitude) # Valid coords, got elevation
|
||||||
logger.info(f"GUI: Point elevation task completed, elevation {elevation_result:.2f}m for ({original_latitude:.5f},{original_longitude:.5f}).")
|
|
||||||
|
|
||||||
self.point_result_label.config(text=res_txt)
|
self.point_result_label.config(text=res_txt)
|
||||||
|
|
||||||
@ -695,7 +877,23 @@ class ElevationApp:
|
|||||||
"""Starts a background thread to download tiles for an area."""
|
"""Starts a background thread to download tiles for an area."""
|
||||||
if self.is_processing_task or not self.elevation_manager: return
|
if self.is_processing_task or not self.elevation_manager: return
|
||||||
bounds = self._validate_area_boundary_coordinates()
|
bounds = self._validate_area_boundary_coordinates()
|
||||||
if not bounds: return
|
if not bounds:
|
||||||
|
# MODIFIED: Clear area status and related map info fields if area validation fails.
|
||||||
|
# WHY: Avoids showing stale info.
|
||||||
|
# HOW: Update labels and Map Info StringVars.
|
||||||
|
self.area_download_status_label.config(text="Status: Invalid input.")
|
||||||
|
self.last_area_coords = None
|
||||||
|
# Also clear map info fields that are area-related or generic
|
||||||
|
self.map_area_size_var.set("N/A")
|
||||||
|
# Optionally clear point-specific map info fields too if they don't apply to area context
|
||||||
|
# self.map_lat_decimal_var.set("N/A") # Decide if we want to clear point info on area task start
|
||||||
|
# self.map_lon_decimal_var.set("N/A")
|
||||||
|
# self.map_lat_dms_var.set("N/A")
|
||||||
|
# self.map_lon_dms_var.set("N/A")
|
||||||
|
# self.map_elevation_var.set("N/A")
|
||||||
|
|
||||||
|
return # Exit if validation fails
|
||||||
|
|
||||||
self.last_area_coords = bounds # Store validated bounds
|
self.last_area_coords = bounds # Store validated bounds
|
||||||
|
|
||||||
# MODIFIED: Start busy state and update status label immediately on GUI thread.
|
# MODIFIED: Start busy state and update status label immediately on GUI thread.
|
||||||
@ -703,6 +901,17 @@ class ElevationApp:
|
|||||||
# HOW: Call _set_busy_state(True) and update label text.
|
# HOW: Call _set_busy_state(True) and update label text.
|
||||||
self._set_busy_state(True)
|
self._set_busy_state(True)
|
||||||
self.area_download_status_label.config(text="Status: Starting download task... Please wait.")
|
self.area_download_status_label.config(text="Status: Starting download task... Please wait.")
|
||||||
|
# MODIFIED: Clear Map Info fields when a new Download Area task starts.
|
||||||
|
# WHY: Indicate that the context has shifted to an area task.
|
||||||
|
# HOW: Set StringVar values to "..." or empty.
|
||||||
|
self.map_lat_decimal_var.set("N/A") # Point info doesn't apply
|
||||||
|
self.map_lon_decimal_var.set("N/A")
|
||||||
|
self.map_lat_dms_var.set("N/A")
|
||||||
|
self.map_lon_dms_var.set("N/A")
|
||||||
|
self.map_elevation_var.set("N/A")
|
||||||
|
self.map_area_size_var.set("...") # Show progress for area size info related to map
|
||||||
|
|
||||||
|
|
||||||
self.root.update_idletasks() # Force GUI update
|
self.root.update_idletasks() # Force GUI update
|
||||||
|
|
||||||
|
|
||||||
@ -934,6 +1143,17 @@ class ElevationApp:
|
|||||||
lat, lon = self.last_valid_point_coords
|
lat, lon = self.last_valid_point_coords
|
||||||
logger.info(f"GUI: Requesting map view for point ({lat:.5f},{lon:.5f}).")
|
logger.info(f"GUI: Requesting map view for point ({lat:.5f},{lon:.5f}).")
|
||||||
|
|
||||||
|
# MODIFIED: Clear Map Info fields when starting a new map view.
|
||||||
|
# WHY: Indicate that new map information is loading.
|
||||||
|
# HOW: Set StringVar values.
|
||||||
|
self.map_lat_decimal_var.set("...") # Indicate loading
|
||||||
|
self.map_lon_decimal_var.set("...")
|
||||||
|
self.map_lat_dms_var.set("...") # Indicate loading for DMS fields
|
||||||
|
self.map_lon_dms_var.set("...") # Indicate loading for DMS fields
|
||||||
|
self.map_elevation_var.set("...")
|
||||||
|
self.map_area_size_var.set("Loading...")
|
||||||
|
|
||||||
|
|
||||||
# Prepare base arguments for the map viewer process target function.
|
# Prepare base arguments for the map viewer process target function.
|
||||||
# It needs the queue, mode, point coords, area bbox (None for point mode), and cache dir (added in _start_map_viewer_process).
|
# It needs the queue, mode, point coords, area bbox (None for point mode), and cache dir (added in _start_map_viewer_process).
|
||||||
# The display scale is also added in _start_map_viewer_process.
|
# The display scale is also added in _start_map_viewer_process.
|
||||||
@ -957,6 +1177,17 @@ class ElevationApp:
|
|||||||
min_l, min_o, max_l, max_o = self.last_area_coords
|
min_l, min_o, max_l, max_o = self.last_area_coords
|
||||||
logger.info(f"GUI: Requesting map view for area Lat [{min_l:.2f}-{max_l:.2f}], Lon [{min_o:.2f}-{max_o:.2f}].")
|
logger.info(f"GUI: Requesting map view for area Lat [{min_l:.2f}-{max_l:.2f}], Lon [{min_o:.2f}-{max_o:.2f}].")
|
||||||
|
|
||||||
|
# MODIFIED: Clear Map Info fields when starting a new map view.
|
||||||
|
# WHY: Indicate that new map information is loading.
|
||||||
|
# HOW: Set StringVar values. Point info N/A for area view.
|
||||||
|
self.map_lat_decimal_var.set("N/A") # Point info not relevant for area view
|
||||||
|
self.map_lon_decimal_var.set("N/A")
|
||||||
|
self.map_lat_dms_var.set("N/A") # Point info not relevant for area view
|
||||||
|
self.map_lon_dms_var.set("N/A") # Point info not relevant for area view
|
||||||
|
self.map_elevation_var.set("N/A") # Point info not relevant for area view
|
||||||
|
self.map_area_size_var.set("Loading...")
|
||||||
|
|
||||||
|
|
||||||
# Prepare base arguments for the map viewer process target function.
|
# Prepare base arguments for the map viewer process target function.
|
||||||
# It needs the queue, mode, point coords (None for area mode), area bbox, and cache dir (added in _start_map_viewer_process).
|
# It needs the queue, mode, point coords (None for area mode), area bbox, and cache dir (added in _start_map_viewer_process).
|
||||||
# The display scale is also added in _start_map_viewer_process.
|
# The display scale is also added in _start_map_viewer_process.
|
||||||
@ -982,36 +1213,48 @@ class ElevationApp:
|
|||||||
# MODIFIED: Handle the new message type 'map_info_update'.
|
# MODIFIED: Handle the new message type 'map_info_update'.
|
||||||
# WHY: This message is sent by the map process for initial point info, click info, and errors.
|
# WHY: This message is sent by the map process for initial point info, click info, and errors.
|
||||||
# HOW: Check for the type and update all relevant Map Info labels.
|
# HOW: Check for the type and update all relevant Map Info labels.
|
||||||
|
# MODIFIED: Update Entry widgets using StringVar and handle new DMS fields from message.
|
||||||
|
# WHY: Populate the copyable Entry widgets and show DMS coordinates.
|
||||||
|
# HOW: Use set() on StringVars, access new keys in the message payload.
|
||||||
if msg_t == "map_info_update":
|
if msg_t == "map_info_update":
|
||||||
|
# Get data from the message payload
|
||||||
lat_val = msg.get("latitude")
|
lat_val = msg.get("latitude")
|
||||||
lon_val = msg.get("longitude")
|
lon_val = msg.get("longitude")
|
||||||
elev_str = msg.get("elevation_str", "N/A") # Default to N/A if key is missing
|
lat_dms_str = msg.get("latitude_dms_str", "N/A") # Get DMS strings from message
|
||||||
map_area_size_str = msg.get("map_area_size_str", "N/A") # Default to N/A
|
lon_dms_str = msg.get("longitude_dms_str", "N/A") # Get DMS strings from message
|
||||||
|
elev_str = msg.get("elevation_str", "N/A")
|
||||||
|
map_area_size_str = msg.get("map_area_size_str", "N/A")
|
||||||
|
|
||||||
# Format coordinates for display
|
# Format decimal coordinates for display in Entry
|
||||||
lat_txt = f"{lat_val:.5f}" if isinstance(lat_val, (int, float)) and not math.isnan(lat_val) else "N/A"
|
lat_txt_decimal = f"{lat_val:.5f}" if isinstance(lat_val, (int, float)) and math.isfinite(lat_val) else "N/A"
|
||||||
lon_txt = f"{lon_val:.5f}" if isinstance(lon_val, (int, float)) and not math.isnan(lon_val) else "N/A"
|
lon_txt_decimal = f"{lon_val:.5f}" if isinstance(lon_val, (int, float)) and math.isfinite(lon_val) else "N/A"
|
||||||
|
|
||||||
self.map_click_latitude_label.config(text=lat_txt)
|
# Update the StringVars linked to the Entry widgets
|
||||||
self.map_click_longitude_label.config(text=lon_txt)
|
self.map_lat_decimal_var.set(lat_txt_decimal)
|
||||||
self.map_click_elevation_label.config(text=elev_str)
|
self.map_lon_decimal_var.set(lon_txt_decimal)
|
||||||
# MODIFIED: Update the new map area size label.
|
self.map_lat_dms_var.set(lat_dms_str) # Set DMS strings directly from message
|
||||||
# WHY: Display the size received from the map process.
|
self.map_lon_dms_var.set(lon_dms_str) # Set DMS strings directly from message
|
||||||
# HOW: Set the text of map_area_size_label.
|
self.map_elevation_var.set(elev_str)
|
||||||
self.map_area_size_label.config(text=map_area_size_str)
|
self.map_area_size_var.set(map_area_size_str)
|
||||||
|
|
||||||
|
logger.debug(f"GUI: Updated Map Info panel with: Lat={lat_txt_decimal}, Lon={lon_txt_decimal}, LatDMS='{lat_dms_str}', LonDMS='{lon_dms_str}', Elev='{elev_str}', Size='{map_area_size_str}'")
|
||||||
|
|
||||||
logger.debug(f"GUI: Updated Map Info panel with: Lat={lat_txt}, Lon={lon_txt}, Elev='{elev_str}', Size='{map_area_size_str}'")
|
|
||||||
|
|
||||||
# MODIFIED: Handle the new message type 'map_fetching_status'.
|
# MODIFIED: Handle the new message type 'map_fetching_status'.
|
||||||
# WHY: Receive progress/status updates specifically for map tile fetching.
|
# WHY: Receive progress/status updates specifically for map tile fetching.
|
||||||
# HOW: Update the map info labels with the status message.
|
# HOW: Update the map info labels with the status message.
|
||||||
|
# MODIFIED: Update Entry widgets using StringVars for status messages.
|
||||||
|
# WHY: Show status in copyable Entry widgets.
|
||||||
|
# HOW: Use set() on StringVars.
|
||||||
elif msg_t == "map_fetching_status":
|
elif msg_t == "map_fetching_status":
|
||||||
status_message = msg.get("status", "Fetching...")
|
status_message = msg.get("status", "Fetching...")
|
||||||
# Update all map info labels to show the status, maybe clear coords/elev temporarily
|
# Update all map info labels to show the status, maybe clear coords/elev temporarily
|
||||||
self.map_click_latitude_label.config(text="") # Clear coords
|
self.map_lat_decimal_var.set("...") # Indicate fetching/processing
|
||||||
self.map_click_longitude_label.config(text="")
|
self.map_lon_decimal_var.set("...")
|
||||||
self.map_click_elevation_label.config(text=status_message) # Show status as elevation text
|
self.map_lat_dms_var.set("...") # Indicate fetching/processing for DMS
|
||||||
self.map_area_size_label.config(text="") # Clear size
|
self.map_lon_dms_var.set("...") # Indicate fetching/processing for DMS
|
||||||
|
self.map_elevation_var.set(status_message) # Show status as elevation text
|
||||||
|
self.map_area_size_var.set("...") # Indicate fetching/processing
|
||||||
|
|
||||||
# elif msg_t == "another_message_type":
|
# elif msg_t == "another_message_type":
|
||||||
# # Handle other message types if needed in the future
|
# # Handle other message types if needed in the future
|
||||||
@ -1028,6 +1271,16 @@ class ElevationApp:
|
|||||||
# Catch any other unexpected errors while processing messages
|
# Catch any other unexpected errors while processing messages
|
||||||
logger.error(f"GUI Error: Exception during map queue message processing: {e}", exc_info=True)
|
logger.error(f"GUI Error: Exception during map queue message processing: {e}", exc_info=True)
|
||||||
# Optional: Update GUI status to indicate communication error
|
# Optional: Update GUI status to indicate communication error
|
||||||
|
# MODIFIED: Update Map Info fields with a communication error state.
|
||||||
|
# WHY: Provide feedback if there's an error processing queue messages.
|
||||||
|
# HOW: Set StringVars with error text.
|
||||||
|
self.map_lat_decimal_var.set("Comm Error")
|
||||||
|
self.map_lon_decimal_var.set("Comm Error")
|
||||||
|
self.map_lat_dms_var.set("Comm Error")
|
||||||
|
self.map_lon_dms_var.set("Comm Error")
|
||||||
|
self.map_elevation_var.set(f"Comm Error: {type(e).__name__}")
|
||||||
|
self.map_area_size_var.set("Comm Error")
|
||||||
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Schedule this method to run again after a short delay to keep checking the queue.
|
# Schedule this method to run again after a short delay to keep checking the queue.
|
||||||
@ -1105,6 +1358,19 @@ if __name__ == "__main__":
|
|||||||
# only for testing but good practice.
|
# only for testing but good practice.
|
||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
|
|
||||||
|
# MODIFIED: Example of how to use the new DMS conversion function in test/debug.
|
||||||
|
# WHY: Demonstrate usage of the new function.
|
||||||
|
# HOW: Added print statements.
|
||||||
|
test_lat = 45.56789
|
||||||
|
test_lon = -7.12345
|
||||||
|
print(f"Test DMS conversion for Lat {test_lat}: {deg_to_dms_string(test_lat, 'lat')}")
|
||||||
|
print(f"Test DMS conversion for Lon {test_lon}: {deg_to_dms_string(test_lon, 'lon')}")
|
||||||
|
test_lat_neg = -30.987
|
||||||
|
test_lon_pos = 150.65
|
||||||
|
print(f"Test DMS conversion for Lat {test_lat_neg}: {deg_to_dms_string(test_lat_neg, 'lat')}")
|
||||||
|
print(f"Test DMS conversion for Lon {test_lon_pos}: {deg_to_dms_string(test_lon_pos, 'lon')}")
|
||||||
|
|
||||||
|
|
||||||
test_root = tk.Tk()
|
test_root = tk.Tk()
|
||||||
app_test_instance: Optional[ElevationApp] = None
|
app_test_instance: Optional[ElevationApp] = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -168,11 +168,22 @@ def get_tile_ranges_for_bbox(
|
|||||||
f"No tiles found by mercantile.tiles for BBox at zoom {zoom_level}. "
|
f"No tiles found by mercantile.tiles for BBox at zoom {zoom_level}. "
|
||||||
"Using fallback: tile at BBox center."
|
"Using fallback: tile at BBox center."
|
||||||
)
|
)
|
||||||
center_lon = (west_lon + east_lon) / 2.0
|
# Ensure coordinates are within valid WGS84 bounds before calculating center
|
||||||
center_lat = (south_lat + north_lat) / 2.0
|
clamped_west_lon = max(-180.0, min(180.0, west_lon))
|
||||||
|
clamped_east_lon = max(-180.0, min(180.0, east_lon))
|
||||||
|
clamped_south_lat = max(-90.0, min(90.0, south_lat))
|
||||||
|
clamped_north_lat = max(-90.0, min(90.0, north_lat))
|
||||||
|
|
||||||
|
center_lon = (clamped_west_lon + clamped_east_lon) / 2.0
|
||||||
|
center_lat = (clamped_south_lat + clamped_north_lat) / 2.0
|
||||||
|
|
||||||
# Clamp center_lat to avoid mercantile issues near poles if bbox extends beyond valid range
|
# Clamp center_lat to avoid mercantile issues near poles if bbox extends beyond valid range
|
||||||
center_lat = max(-85.0, min(85.0, center_lat)) # Mercantile limits
|
center_lat = max(-85.0, min(85.0, center_lat)) # Mercantile limits - approx. latitude of tile row 0 or max
|
||||||
center_lon = max(-180.0, min(180.0, center_lon)) # Mercantile limits
|
# Ensure center_lon wraps correctly if bbox spans the antimeridian
|
||||||
|
if clamped_west_lon > clamped_east_lon: # Bbox crosses antimeridian
|
||||||
|
center_lon = (clamped_west_lon + clamped_east_lon + 360) / 2.0
|
||||||
|
if center_lon > 180: center_lon -= 360
|
||||||
|
|
||||||
|
|
||||||
# mercantile.tile(lon, lat, zoom)
|
# mercantile.tile(lon, lat, zoom)
|
||||||
center_point_tile = mercantile.tile(center_lon, center_lat, zoom_level) # type: ignore
|
center_point_tile = mercantile.tile(center_lon, center_lat, zoom_level) # type: ignore
|
||||||
@ -286,10 +297,13 @@ def calculate_geographic_bbox_size_km(
|
|||||||
# Basic validation
|
# Basic validation
|
||||||
if not (-90.0 <= south_lat <= north_lat <= 90.0):
|
if not (-90.0 <= south_lat <= north_lat <= 90.0):
|
||||||
logger.warning(f"Invalid latitude range for size calculation: {south_lat}, {north_lat}")
|
logger.warning(f"Invalid latitude range for size calculation: {south_lat}, {north_lat}")
|
||||||
return None
|
# Try clamping and continue, or return None? Let's clamp for robustness.
|
||||||
# For longitude, check range is within a reasonable bound, but allow west > east for dateline crossing
|
south_lat = max(-90.0, south_lat)
|
||||||
# The geographic width should be calculated carefully to handle wrapping around the globe.
|
north_lat = min(90.0, north_lat)
|
||||||
# The height calculation is simpler as latitude is bounded.
|
if south_lat >= north_lat: # After clamping, check if still invalid
|
||||||
|
logger.error(f"Invalid latitude range after clamping: {south_lat}, {north_lat}. Cannot calculate size.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
geodetic_calculator = pyproj.Geod(ellps="WGS84") # type: ignore
|
geodetic_calculator = pyproj.Geod(ellps="WGS84") # type: ignore
|
||||||
@ -308,10 +322,18 @@ def calculate_geographic_bbox_size_km(
|
|||||||
# This is simpler, distance between south_lat and north_lat at center_lon
|
# This is simpler, distance between south_lat and north_lat at center_lon
|
||||||
# Need to handle potential longitude wrap around - use inv carefully
|
# Need to handle potential longitude wrap around - use inv carefully
|
||||||
# The straight line distance calculation below should be generally fine for height.
|
# The straight line distance calculation below should be generally fine for height.
|
||||||
center_lon = (west_lon + east_lon) / 2.0 # Use the average longitude for height calculation line
|
# Using the average longitude for height calculation line might not be strictly necessary,
|
||||||
# Clamp center lon
|
# distance between two points at the same longitude is simple geodetic distance.
|
||||||
center_lon = max(-179.9, min(179.9, center_lon))
|
# However, using inv is more general and handles the ellipsoid correctly.
|
||||||
_, _, height_meters = geodetic_calculator.inv(center_lon, south_lat, center_lon, north_lat)
|
center_lon_for_height = (west_lon + east_lon) / 2.0 # Use the average longitude for height calculation line
|
||||||
|
# Handle antimeridian crossing for height calculation by adjusting center_lon_for_height if needed.
|
||||||
|
# If the width calculation crossed the antimeridian (west_lon > east_lon originally), the average might be misleading.
|
||||||
|
# A simpler approach for height is just calculating distance between (center_lon, south) and (center_lon, north).
|
||||||
|
# Let's reuse the original logic using the average longitude, ensuring it's within range.
|
||||||
|
center_lon_for_height = max(-180.0, min(180.0, center_lon_for_height)) # Ensure within standard range
|
||||||
|
|
||||||
|
|
||||||
|
_, _, height_meters = geodetic_calculator.inv(center_lon_for_height, south_lat, center_lon_for_height, north_lat)
|
||||||
|
|
||||||
approx_width_km = abs(width_meters) / 1000.0 # Ensure positive distance
|
approx_width_km = abs(width_meters) / 1000.0 # Ensure positive distance
|
||||||
approx_height_km = abs(height_meters) / 1000.0
|
approx_height_km = abs(height_meters) / 1000.0
|
||||||
@ -362,6 +384,18 @@ def get_hgt_tile_geographic_bounds(lat_coord: int, lon_coord: int) -> Tuple[floa
|
|||||||
east_lon = max(-180.0, min(180.0, east_lon))
|
east_lon = max(-180.0, min(180.0, east_lon))
|
||||||
north_lat = max(-90.0, min(90.0, north_lat))
|
north_lat = max(-90.0, min(90.0, north_lat))
|
||||||
|
|
||||||
|
# Ensure west < east and south < north for valid bbox representation, handling wrap-around is complex
|
||||||
|
# For 1x1 degree tiles not crossing antimeridian or poles widely, this is fine.
|
||||||
|
# A more robust check might be needed for edge cases near antimeridian, but for 1x1 tiles this is usually okay.
|
||||||
|
if west_lon > east_lon:
|
||||||
|
# This case ideally shouldn't happen for 1x1 degree tiles unless lon_coord is 179 and it wraps,
|
||||||
|
# but let's handle defensively if coords are unusual. Could swap or adjust.
|
||||||
|
# For HGT tile boundaries, it's usually [lon, lon+1] so this shouldn't be an issue.
|
||||||
|
logger.warning(f"Calculated west > east for HGT tile bounds ({lat_coord},{lon_coord}): ({west_lon}, {east_lon}).")
|
||||||
|
# Assuming it's a simple swap needed
|
||||||
|
# west_lon, east_lon = east_lon, west_lon # This might not be correct if it actually wraps the globe
|
||||||
|
pass # Let's stick to the direct calculation based on HGT convention
|
||||||
|
|
||||||
logger.debug(f"Calculated HGT tile bounds for ({lat_coord},{lon_coord}): ({west_lon:.6f}, {south_lat:.6f}, {east_lon:.6f}, {north_lat:.6f})")
|
logger.debug(f"Calculated HGT tile bounds for ({lat_coord},{lon_coord}): ({west_lon:.6f}, {south_lat:.6f}, {east_lon:.6f}, {north_lat:.6f})")
|
||||||
return (west_lon, south_lat, east_lon, north_lat)
|
return (west_lon, south_lat, east_lon, north_lat)
|
||||||
|
|
||||||
@ -459,7 +493,92 @@ def calculate_zoom_level_for_geographic_size(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return clamped_zoom
|
return clamped_zoom
|
||||||
|
|
||||||
except Exception as e_zoom_calc:
|
except Exception as e_zoom_calc:
|
||||||
logger.exception(f"Error calculating zoom level: {e_zoom_calc}")
|
logger.exception(f"Error calculating zoom level: {e_zoom_calc}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# MODIFIED: Added utility function to convert decimal degrees to DMS string format.
|
||||||
|
# WHY: Needed for displaying coordinates in a user-friendly, copyable format in the GUI.
|
||||||
|
# HOW: Implemented standard conversion logic.
|
||||||
|
def deg_to_dms_string(degree_value: float, coord_type: str) -> str:
|
||||||
|
"""
|
||||||
|
Converts a decimal degree coordinate to a Degrees, Minutes, Seconds (DMS) string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
degree_value: The coordinate value in decimal degrees (float).
|
||||||
|
coord_type: 'lat' for latitude (determines N/S suffix), 'lon' for longitude (E/W suffix).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A formatted DMS string (e.g., "45° 30' 15.23'' N"). Returns "N/A" for non-finite or invalid inputs.
|
||||||
|
"""
|
||||||
|
if not isinstance(degree_value, (int, float)) or not math.isfinite(degree_value):
|
||||||
|
# Handle None, NaN, Inf, etc.
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
# Clamp to valid ranges for sanity, although conversion works outside.
|
||||||
|
# Note: DMS representation is strictly defined for lat [-90, 90] and lon [-180, 180]
|
||||||
|
if coord_type.lower() == 'lat':
|
||||||
|
if not -90.0 <= degree_value <= 90.0:
|
||||||
|
logger.warning(f"Latitude {degree_value} is outside valid range [-90, 90] for standard DMS.")
|
||||||
|
# Still convert, but maybe add a note or handle separately if needed.
|
||||||
|
# For now, just convert the clamped value.
|
||||||
|
degree_value = max(-90.0, min(90.0, degree_value))
|
||||||
|
elif coord_type.lower() == 'lon':
|
||||||
|
# Longitude wrapping - DMS usually represents within [-180, 180].
|
||||||
|
# Python's % operator handles negative numbers differently than some other languages,
|
||||||
|
# (a % b) has the same sign as b. So ((value + 180) % 360 - 180) ensures the result
|
||||||
|
# is in (-180, 180]. If the input is exactly 180, it becomes 180.
|
||||||
|
degree_value = ((degree_value + 180) % 360) - 180
|
||||||
|
# Check if exactly -180.0 and adjust to 180.0 if needed for conventional representation
|
||||||
|
# if degree_value == -180.0:
|
||||||
|
# degree_value = 180.0
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown coordinate type '{coord_type}' for DMS conversion.")
|
||||||
|
return "N/A (Invalid Type)"
|
||||||
|
|
||||||
|
|
||||||
|
is_negative = degree_value < 0
|
||||||
|
abs_degree = abs(degree_value)
|
||||||
|
|
||||||
|
degrees = int(abs_degree)
|
||||||
|
minutes_decimal = (abs_degree - degrees) * 60
|
||||||
|
minutes = int(minutes_decimal)
|
||||||
|
seconds = (minutes_decimal - minutes) * 60
|
||||||
|
|
||||||
|
# Determine suffix (North/South, East/West)
|
||||||
|
suffix = ""
|
||||||
|
if coord_type.lower() == 'lat':
|
||||||
|
suffix = "N" if not is_negative else "S"
|
||||||
|
elif coord_type.lower() == 'lon':
|
||||||
|
# Longitude can be 0 or 180, technically not negative/positive in terms of E/W.
|
||||||
|
# Let's use the sign logic which is correct for the suffix E/W convention.
|
||||||
|
suffix = "E" if not is_negative else "W"
|
||||||
|
# Special case for exactly 0 or 180, maybe no suffix or specific suffix?
|
||||||
|
# Standard practice is usually to use E for 0 and 180 if positive/negative sign is ignored.
|
||||||
|
if degree_value == 0.0: suffix = "" # No suffix for 0? Or E? Let's use ""
|
||||||
|
elif abs(degree_value) == 180.0: suffix = "" # No suffix for 180? Or W? Let's use ""
|
||||||
|
|
||||||
|
# Format the string
|
||||||
|
# Use a consistent number of decimal places for seconds, e.g., 2
|
||||||
|
# Handle potential edge case where seconds round up to 60
|
||||||
|
if seconds >= 59.995: # Check if seconds are very close to 60 due to float precision
|
||||||
|
seconds = 0.0
|
||||||
|
minutes += 1
|
||||||
|
if minutes >= 60:
|
||||||
|
minutes = 0
|
||||||
|
degrees += 1
|
||||||
|
# Degree could potentially roll over if it was like 89 deg 59 min 59.99 sec
|
||||||
|
# This simple logic assumes it won't cross 90 or 180 significantly with typical inputs
|
||||||
|
# A more robust implementation might need to handle full degree rollover, but unlikely for standard inputs.
|
||||||
|
# For now, just increment degree if minutes rolled over from 59 to 60.
|
||||||
|
|
||||||
|
|
||||||
|
dms_string = f"{degrees}° {minutes}' {seconds:.2f}''"
|
||||||
|
|
||||||
|
# Add suffix only if it's meaningful (not empty)
|
||||||
|
if suffix:
|
||||||
|
dms_string += f" {suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
return dms_string
|
||||||
Loading…
Reference in New Issue
Block a user