From dc581931a77954556fb6eb7974132becb72cd9a9 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Tue, 13 May 2025 12:16:27 +0200 Subject: [PATCH] add map elevation, using module geoelevation. --- .gitignore | 3 +- controlpanel/app_main.py | 500 ++++++++++++++++++++++++++------ controlpanel/config.py | 14 + controlpanel/gui/ui.py | 608 +++++++++++++-------------------------- 4 files changed, 636 insertions(+), 489 deletions(-) diff --git a/.gitignore b/.gitignore index e146f03..61f0704 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ _build/ _dist/ sar_images/ kml_output/ -_req_packages/ \ No newline at end of file +_req_packages/ +elevation_cache/ \ No newline at end of file diff --git a/controlpanel/app_main.py b/controlpanel/app_main.py index d3ec365..d6c232c 100644 --- a/controlpanel/app_main.py +++ b/controlpanel/app_main.py @@ -1,6 +1,3 @@ -# --- START OF FILE ControlPanel.py --- - -# ControlPanel.py (Previously app.py) """ THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A @@ -22,7 +19,7 @@ import logging import math import sys import socket -from typing import Optional, Tuple, Any, Dict, TYPE_CHECKING, Union, List +from typing import Optional, Tuple, Any, Dict, TYPE_CHECKING, Union, List, Callable import datetime import tkinter.filedialog as fd from pathlib import Path @@ -48,47 +45,32 @@ except ImportError: "[App Init] Pillow library not found. Map/Image functionality will fail." ) -# --- Configuration Import --- -from controlpanel import config - -# --- Logging Setup --- +# --- Absolute Imports for Application Modules --- try: + from controlpanel import config # Config is now essential before GeoElevation logic from controlpanel.logging_config import setup_logging - - setup_logging() -except ImportError: - print("ERROR: logging_config.py not found. Using basic logging.") - logging.basicConfig( - level=logging.WARNING, - format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s", + from controlpanel.app_state import AppState + # GUI related imports + from controlpanel.gui.ui import ControlPanel as UIPanel, StatusBar, create_main_window + from controlpanel.gui.display import DisplayManager + # Core processing imports + from controlpanel.core.receiver import UdpReceiver + from controlpanel.core.test_mode_manager import TestModeManager + from controlpanel.core.image_pipeline import ImagePipeline + from controlpanel.core.image_recorder import ImageRecorder + # Utility imports + from controlpanel.utils.utils import ( + put_queue, clear_queue, decimal_to_dms, dms_string_to_decimal, + generate_sar_kml, launch_google_earth, cleanup_kml_output_directory, + open_google_maps, generate_lookat_and_point_kml, generate_composite_kml, + _simplekml_available, _pyproj_available, format_ctypes_structure, ) - -# --- Application Modules Import --- -from controlpanel.gui.ui import ControlPanel as UIPanel, StatusBar, create_main_window -from controlpanel.gui.display import DisplayManager -from controlpanel.utils.utils import ( - put_queue, - clear_queue, - decimal_to_dms, - dms_string_to_decimal, - generate_sar_kml, # Rimane invariato - launch_google_earth, - # cleanup_old_kml_files, # Rimuovere o commentare il vecchio import - cleanup_kml_output_directory, # <<< NUOVO IMPORT - open_google_maps, - generate_lookat_and_point_kml, - generate_composite_kml, # Modificato precedentemente per usare questa - _simplekml_available, - _pyproj_available, - format_ctypes_structure, -) -from controlpanel.utils.network import create_udp_socket, close_udp_socket -from controlpanel.core.receiver import UdpReceiver -from controlpanel.app_state import AppState -from controlpanel.core.test_mode_manager import TestModeManager -from controlpanel.core.image_pipeline import ImagePipeline -from controlpanel.utils.image_processing import load_image, normalize_image, apply_color_palette -from controlpanel.core.image_recorder import ImageRecorder + from controlpanel.utils.network import create_udp_socket, close_udp_socket + from controlpanel.utils.image_processing import load_image, normalize_image, apply_color_palette +except ImportError as app_import_err: + print(f"FATAL ERROR: Failed to import core application modules: {app_import_err}") + logging.critical(f"FATAL ERROR: Failed to import core application modules: {app_import_err}", exc_info=True) + sys.exit(1) # --- Map related imports (Conditional) --- map_libs_found = True @@ -151,6 +133,97 @@ DEFAULT_COMMIT = "Unknown" DEFAULT_BRANCH = "Unknown" # --- End Constants --- +# --- >>> START OF NEW GEOELEVATION INTEGRATION CODE <<< --- +# Global variables for GeoElevation availability and function reference +GEOELEVATION_AVAILABLE: bool = False +get_elevation_function: Optional[Callable] = None +_geoelevation_log_prefix = "[GeoElevation Integration]" + +try: + # 1. Check if the path is configured in controlpanel/config.py + configured_path_str = getattr(config, "GEOELEVATION_PROJECT_ROOT_PATH", None) + + if configured_path_str and isinstance(configured_path_str, str) and configured_path_str.strip(): + configured_path = Path(configured_path_str.strip()) + logging.info(f"{_geoelevation_log_prefix} GEOELEVATION_PROJECT_ROOT_PATH configured to: '{configured_path_str}'") + + # 2. Resolve to absolute path if relative + # Relative path is assumed to be relative to the *ControlPanel project root* + # (the directory containing the 'controlpanel' package folder). + if not configured_path.is_absolute(): + # __file__ points to controlpanel/app_main.py + # controlpanel_package_dir is controlpanel/ + controlpanel_package_dir = Path(__file__).parent + # project_root_dir is the parent of controlpanel/ + project_root_dir = controlpanel_package_dir.parent + absolute_geoelevation_path = (project_root_dir / configured_path).resolve() + logging.debug(f"{_geoelevation_log_prefix} Resolved relative path '{configured_path}' to '{absolute_geoelevation_path}'") + configured_path = absolute_geoelevation_path + + # 3. Validate the path and the 'geoelevation' subfolder + potential_module_dir = configured_path / "geoelevation" # The actual package folder + if configured_path.is_dir() and potential_module_dir.is_dir() and (potential_module_dir / "__init__.py").is_file(): + logging.debug(f"{_geoelevation_log_prefix} Validated GeoElevation project root: '{configured_path}'") + logging.debug(f"{_geoelevation_log_prefix} Potential GeoElevation module directory: '{potential_module_dir}'") + + # 4. Add the *parent* of the 'geoelevation' module to sys.path + # This allows `from geoelevation import ...` + # So, we add `configured_path` to sys.path. + path_to_add_to_sys = str(configured_path) + path_added_to_sys = False + if path_to_add_to_sys not in sys.path: + sys.path.insert(0, path_to_add_to_sys) # Add to beginning for priority + path_added_to_sys = True + logging.info(f"{_geoelevation_log_prefix} Added '{path_to_add_to_sys}' to sys.path for GeoElevation import.") + + # 5. Attempt to import the specific function + try: + from geoelevation import get_point_elevation as ge_get_point_elevation + # from geoelevation import get_point_elevation as ge_get_point_elevation # If function is in __init__.py + GEOELEVATION_AVAILABLE = True + get_elevation_function = ge_get_point_elevation + logging.info(f"{_geoelevation_log_prefix} Module 'geoelevation.elevation_service.get_point_elevation' loaded successfully. Elevation feature ENABLED.") + + # Optional: Clean up sys.path if it was modified for this import only + # This is generally good practice to avoid polluting sys.path permanently + if path_added_to_sys and path_to_add_to_sys in sys.path: + try: + sys.path.remove(path_to_add_to_sys) + logging.debug(f"{_geoelevation_log_prefix} Removed '{path_to_add_to_sys}' from sys.path after successful import.") + except ValueError: + pass # Should not happen if path_added_to_sys is True + + except ImportError as e_import: + logging.warning(f"{_geoelevation_log_prefix} Failed to import 'get_point_elevation' from GeoElevation module at '{potential_module_dir}': {e_import}. Elevation feature DISABLED.") + GEOELEVATION_AVAILABLE = False + # If import failed, and we added the path, consider removing it too + if path_added_to_sys and path_to_add_to_sys in sys.path: + try: + sys.path.remove(path_to_add_to_sys) + logging.debug(f"{_geoelevation_log_prefix} Removed '{path_to_add_to_sys}' from sys.path after import failure.") + except ValueError: + pass + except Exception as e_general_import: + logging.error(f"{_geoelevation_log_prefix} An unexpected error occurred during GeoElevation import: {e_general_import}. Elevation feature DISABLED.", exc_info=True) + GEOELEVATION_AVAILABLE = False + if path_added_to_sys and path_to_add_to_sys in sys.path: # Cleanup + try: sys.path.remove(path_to_add_to_sys) + except ValueError: pass + + + else: # Path not valid (doesn't exist or geoelevation subfolder missing) + logging.warning(f"{_geoelevation_log_prefix} Configured GeoElevation path '{configured_path}' or its 'geoelevation' subfolder (with __init__.py) is not valid. Elevation feature DISABLED.") + GEOELEVATION_AVAILABLE = False + else: # Path not configured or empty + logging.info(f"{_geoelevation_log_prefix} GEOELEVATION_PROJECT_ROOT_PATH not configured or empty. Elevation feature DISABLED.") + GEOELEVATION_AVAILABLE = False + +except Exception as e_outer_config: + # Catch any error during the config reading/path manipulation itself + logging.error(f"{_geoelevation_log_prefix} Error processing GeoElevation configuration: {e_outer_config}. Elevation feature DISABLED.", exc_info=True) + GEOELEVATION_AVAILABLE = False +# --- >>> END OF NEW GEOELEVATION INTEGRATION CODE <<< --- + # --- Main Application Class --- class ControlPanelApp: @@ -362,8 +435,16 @@ class ControlPanelApp: # Set initial mode (Test or Normal/Local) self.update_image_mode() + + # --- Add a new attribute for elevation request tracking --- + self.active_elevation_requests: Dict[str, threading.Thread] = {} # Key: "src_lat_lon", Value: Thread + logging.info(f"{log_prefix} Application initialization complete.") + if GEOELEVATION_AVAILABLE: + logging.info(f"{log_prefix} GeoElevation functionality is AVAILABLE.") + else: + logging.warning(f"{log_prefix} GeoElevation functionality is UNAVAILABLE.") # --- Status Update Method --- def set_status(self, message: str): @@ -405,6 +486,148 @@ class ControlPanelApp: self.root.after_idle(_update) except Exception as e: logging.warning(f"{log_prefix} Error scheduling status update: {e}") + + def request_elevation_for_point(self, source_description: str, latitude: float, longitude: float): + """ + Requests elevation for a given point asynchronously if GeoElevation is available. + A unique identifier for the request is created to manage concurrent requests. + + Args: + source_description (str): A description of the point source (e.g., "SAR_Center", "SAR_Mouse_Click"). + latitude (float): Latitude of the point in decimal degrees. + longitude (float): Longitude of the point in decimal degrees. + """ + _log_prefix_req = "[App Elev Req]" + if not GEOELEVATION_AVAILABLE or get_elevation_function is None: + logging.warning(f"{_log_prefix_req} GeoElevation not available. Cannot request elevation for {source_description}.") + # Optionally, update UI to indicate unavailability if this is the first time + self._update_elevation_display(source_description, "GeoElevation N/A") + return + + if not (math.isfinite(latitude) and math.isfinite(longitude)): + logging.warning(f"{_log_prefix_req} Invalid coordinates for {source_description}: Lat={latitude}, Lon={longitude}. Skipping elevation request.") + self._update_elevation_display(source_description, "Invalid Coords") + return + + # Create a unique request ID based on source and coordinates to prevent duplicate threads for the exact same point if clicked rapidly + # This is a simple way; more robust would involve a proper task queue with cancellation. + request_id = f"{source_description}_{latitude:.6f}_{longitude:.6f}" + + # Check if a request for this exact ID is already running + if request_id in self.active_elevation_requests and self.active_elevation_requests[request_id].is_alive(): + logging.debug(f"{_log_prefix_req} Elevation request for '{request_id}' is already active. Skipping.") + return + + logging.info(f"{_log_prefix_req} Requesting elevation for {source_description} at ({latitude:.4f}, {longitude:.4f}). Request ID: {request_id}") + self._update_elevation_display(source_description, "Elevation: Requesting...") + + # Create and start a new thread for the elevation call + elevation_thread = threading.Thread( + target=self._get_elevation_worker, + args=(request_id, source_description, latitude, longitude, get_elevation_function), + name=f"ElevationWorker-{request_id}", + daemon=True + ) + self.active_elevation_requests[request_id] = elevation_thread + elevation_thread.start() + + def _get_elevation_worker( + self, + request_id: str, + source_description: str, + latitude: float, + longitude: float, + elevation_func: Callable + ): + """ + Worker function executed in a separate thread to call the GeoElevation service. + Sends the result or error back to the main thread via the Tkinter queue. + """ + _log_prefix_worker = f"[App Elev Work] ({request_id})" + # --- MODIFIED PAYLOAD STRUCTURE --- + result_payload: Dict[str, Any] = { + "request_id": request_id, + "source_description": source_description, + "latitude": latitude, + "longitude": longitude, + "elevation_value": None, # Stores the numeric elevation or None/NaN + "display_text": "Elev: Error", # Default display text in case of issues + "error": None + } + + try: + logging.debug(f"{_log_prefix_worker} Calling GeoElevation function...") + elevation = elevation_func(latitude, longitude, show_progress_dialog=False) + logging.debug(f"{_log_prefix_worker} GeoElevation call returned: {elevation}") + + if elevation is None: + result_payload["display_text"] = "Elev: N/A" # More concise + result_payload["elevation_value"] = None + elif elevation != elevation: # Check for NaN + result_payload["display_text"] = "Elev: NoData" # More concise + result_payload["elevation_value"] = float('nan') # Store NaN + else: + numeric_elevation = float(elevation) + result_payload["elevation_value"] = numeric_elevation + result_payload["display_text"] = f"{numeric_elevation:.1f} m" # Desired format + except RuntimeError as e_rt: + logging.error(f"{_log_prefix_worker} GeoElevation RuntimeError: {e_rt}") + result_payload["display_text"] = "Elev: Err(RT)" + result_payload["error"] = str(e_rt) + except Exception as e_generic: + logging.exception(f"{_log_prefix_worker} Unexpected error calling GeoElevation:") + result_payload["display_text"] = "Elev: Err(Sys)" + result_payload["error"] = str(e_generic) + finally: + if hasattr(self, 'tkinter_queue') and self.tkinter_queue is not None: + put_queue(self.tkinter_queue, ("ELEVATION_RESULT", result_payload), "tkinter", self) + if request_id in self.active_elevation_requests: + del self.active_elevation_requests[request_id] + logging.debug(f"{_log_prefix_worker} Worker finished.") + + + def _update_elevation_display(self, source_description: str, display_text_value: str): + """ + Updates the UI to show the elevation or status by calling the + appropriate method on the control_panel (UIPanel) instance. + + Args: + source_description (str): Describes the source of the elevation point. + Should match one of config.ELEVATION_SOURCE_* constants. + display_text_value (str): The pre-formatted text to display (e.g., "200.0 m", "Elev: N/A"). + """ + _log_prefix_disp = "[App Elev Disp]" + logging.debug(f"{_log_prefix_disp} For '{source_description}': {display_text_value}") + + control_panel_ui = getattr(self, "control_panel", None) + if not control_panel_ui: + logging.error(f"{_log_prefix_disp} ControlPanel UI instance not found. Cannot update elevation display.") + return + + try: + if source_description == config.ELEVATION_SOURCE_SAR_CENTER: + if hasattr(control_panel_ui, 'set_sar_center_elevation'): + control_panel_ui.set_sar_center_elevation(display_text_value) + else: + logging.warning(f"{_log_prefix_disp} Method 'set_sar_center_elevation' not found.") + elif source_description == config.ELEVATION_SOURCE_SAR_MOUSE: + if hasattr(control_panel_ui, 'set_sar_mouse_elevation'): + control_panel_ui.set_sar_mouse_elevation(display_text_value) + else: + logging.warning(f"{_log_prefix_disp} Method 'set_sar_mouse_elevation' not found.") + elif source_description == config.ELEVATION_SOURCE_MAP_MOUSE: + if hasattr(control_panel_ui, 'set_map_mouse_elevation'): + control_panel_ui.set_map_mouse_elevation(display_text_value) + else: + logging.warning(f"{_log_prefix_disp} Method 'set_map_mouse_elevation' not found.") + else: + logging.warning(f"{_log_prefix_disp} Unknown elevation source '{source_description}'. No specific UI label updated.") + # You might still want to update a general status if needed: + # self.statusbar.set_status_text(f"Elevation Info: [{source_description}] {display_text_value}") + + except Exception as e: + logging.exception(f"{_log_prefix_disp} Error updating elevation display for '{source_description}':") + # --- LUT Generation Methods --- def update_brightness_contrast_lut(self): @@ -1811,27 +2034,26 @@ class ControlPanelApp: # --- UI Display Update Helpers --- def _update_sar_ui_labels(self): - """Updates SAR related UI Entry widgets from AppState.""" + """Updates SAR related UI Entry widgets from AppState and requests SAR center elevation.""" cp = getattr(self, "control_panel", None) - # Check if control panel exists and its window is valid - if not cp or not cp.winfo_exists(): - return - + # ... (codice esistente per aggiornare lat_s, lon_s, orient_s, size_s) ... geo = self.state.current_sar_geo_info lat_s, lon_s, orient_s, size_s = "N/A", "N/A", "N/A", "N/A" - is_valid = geo and geo.get("valid") + is_valid_geo_for_ui = geo and geo.get("valid") + lat_deg_for_elevation: Optional[float] = None + lon_deg_for_elevation: Optional[float] = None - if is_valid: + if is_valid_geo_for_ui: try: - # Convert radians to degrees for display/formatting lat_d = math.degrees(geo["lat"]) lon_d = math.degrees(geo["lon"]) + lat_deg_for_elevation = lat_d # Store for elevation request + lon_deg_for_elevation = lon_d # Store for elevation request + # ... (resto della formattazione per lat_s, lon_s, etc.) ... orient_d = math.degrees(geo.get("orientation", 0.0)) - # Format using DMS utility lat_s = decimal_to_dms(lat_d, True) lon_s = decimal_to_dms(lon_d, False) - orient_s = f"{orient_d:.2f}°" # Format orientation - # Calculate size in km if possible + orient_s = f"{orient_d:.2f}°" scale_x = geo.get("scale_x", 0.0) width_px = geo.get("width_px", 0) scale_y = geo.get("scale_y", 0.0) @@ -1840,25 +2062,46 @@ class ControlPanelApp: size_w_km = (scale_x * width_px) / 1000.0 size_h_km = (scale_y * height_px) / 1000.0 size_s = f"W: {size_w_km:.1f} km, H: {size_h_km:.1f} km" + except Exception as e: logging.warning(f"[App UI Update] Error formatting SAR geo labels: {e}") lat_s, lon_s, orient_s, size_s = "Error", "Error", "Error", "Error" - is_valid = False # Mark as invalid if formatting fails + is_valid_geo_for_ui = False + lat_deg_for_elevation = None # Invalidate + lon_deg_for_elevation = None # Invalidate - # Update UI widgets safely using control panel methods + # Update UI widgets + # ... (chiamate a cp.set_sar_center_coords, etc.) ... try: - cp.set_sar_center_coords(lat_s, lon_s) - cp.set_sar_orientation(orient_s) - cp.set_sar_size_km(size_s) + if cp and cp.winfo_exists(): + cp.set_sar_center_coords(lat_s, lon_s) + cp.set_sar_orientation(orient_s) + cp.set_sar_size_km(size_s) except Exception as e: - # Catch errors if UI elements don't exist or Tcl errors logging.exception(f"[App UI Update] Error setting SAR labels: {e}") + + # --- >>> START OF NEW ELEVATION REQUEST FOR SAR CENTER <<< --- + if is_valid_geo_for_ui and lat_deg_for_elevation is not None and lon_deg_for_elevation is not None: + self.request_elevation_for_point( + source_description=config.ELEVATION_SOURCE_SAR_CENTER, # Define this in config.py + latitude=lat_deg_for_elevation, + longitude=lon_deg_for_elevation + ) + else: + # Clear or indicate N/A for SAR center elevation if geo is invalid + self._update_elevation_display(config.ELEVATION_SOURCE_SAR_CENTER, "GeoInfo N/A") + # --- >>> END OF NEW ELEVATION REQUEST FOR SAR CENTER <<< --- + # Clear mouse coords if GeoInfo becomes invalid - if not is_valid: + if not is_valid_geo_for_ui: try: - cp.set_mouse_coordinates("N/A", "N/A") - cp.set_map_mouse_coordinates("N/A", "N/A") + if cp and cp.winfo_exists(): + cp.set_mouse_coordinates("N/A", "N/A") + cp.set_map_mouse_coordinates("N/A", "N/A") + # Clear mouse elevation displays too + self._update_elevation_display(config.ELEVATION_SOURCE_SAR_MOUSE, "GeoInfo N/A") + self._update_elevation_display(config.ELEVATION_SOURCE_MAP_MOUSE, "GeoInfo N/A") except Exception: pass # Ignore errors if UI closed @@ -2043,6 +2286,8 @@ class ControlPanelApp: self._handle_map_click_update(payload) elif command == "SAR_METADATA_UPDATE": self._handle_sar_metadata_update(payload) + elif command == "ELEVATION_RESULT": + self._handle_elevation_result(payload) else: # Log if an unknown command is received logging.warning(f"{log_prefix} Unknown command: {command}") @@ -2110,6 +2355,32 @@ class ControlPanelApp: cp.set_map_mouse_coordinates("N/A", "N/A") except Exception: pass # Ignore errors if UI closed + + def _handle_elevation_result(self, payload: Dict[str, Any]): + """ + Handles the ELEVATION_RESULT command from the Tkinter queue. + Updates the UI with the elevation information. + """ + _log_prefix_res = "[App Elev Res]" + if self.state.shutting_down: + return + + try: + source_desc = payload.get("source_description", "UnknownPoint") + # --- USE THE PRE-FORMATTED DISPLAY_TEXT FROM PAYLOAD --- + display_text_for_ui = payload.get("display_text", "Elev: Error") + error_details = payload.get("error") + # elevation_val = payload.get("elevation_value") # Numeric value, can be used for other logic if needed + + logging.info(f"{_log_prefix_res} Received elevation result for '{source_desc}': {display_text_for_ui}") + if error_details: + logging.error(f"{_log_prefix_res} Error details for '{source_desc}': {error_details}") + + self._update_elevation_display(source_desc, display_text_for_ui) + + except Exception as e: + logging.exception(f"{_log_prefix_res} Error handling elevation result payload: {payload}") + self._update_elevation_display("Error", "Elev: UI Error") def _handle_show_map_update(self, payload: Optional[ImageType]): """Handles the SHOW_MAP command by delegating display to MapIntegrationManager.""" @@ -2144,36 +2415,101 @@ class ControlPanelApp: ) def _handle_sar_click_update(self, payload: Optional[Tuple[int, int]]): - """Updates the SAR click coordinates state and triggers a SAR redraw.""" + """Updates the SAR click coordinates state, triggers a SAR redraw, and requests elevation.""" log_prefix = "[App SAR Click State]" - if self.state.shutting_down: - return - # Validate payload (should be (x, y) tuple) + # ... (codice esistente per aggiornare self.state.last_sar_click_coords e self._trigger_sar_update()) ... if payload and isinstance(payload, tuple) and len(payload) == 2: - # Store the pixel coordinates in AppState self.state.last_sar_click_coords = payload - logging.debug( - f"{log_prefix} Updated state.last_sar_click_coords to {payload}" - ) - # Trigger SAR redraw pipeline to include the marker - self._trigger_sar_update() + logging.debug(f"{log_prefix} Updated state.last_sar_click_coords to {payload}") + self._trigger_sar_update() # Redraw SAR image with marker + + # --- >>> START OF NEW ELEVATION REQUEST <<< --- + # Get geographic coordinates for the clicked SAR pixel + # This reuses the logic that updates the "SAR Mouse" display + # It's a bit indirect but avoids duplicating the geo calculation here. + # Assumes process_mouse_queue will eventually update the UI stringvar + # and then we parse it back. A more direct way would be to have + # get_geo_coords_from_sar_pixel return the lat/lon directly. + + # For now, let's assume we need to convert payload (pixel) to geo + geo_info = self.state.current_sar_geo_info + disp_w = self.state.sar_display_width + disp_h = self.state.sar_display_height + + if (geo_info and geo_info.get("valid") and disp_w > 0 and disp_h > 0 and + geo_info.get("width_px", 0) > 0 and geo_info.get("height_px", 0) > 0): + try: + x_disp, y_disp = payload + orig_w, orig_h = geo_info["width_px"], geo_info["height_px"] + scale_x, scale_y = geo_info["scale_x"], geo_info["scale_y"] + ref_x, ref_y = geo_info["ref_x"], geo_info["ref_y"] + ref_lat_rad, ref_lon_rad = geo_info["lat"], geo_info["lon"] + # angle_rad = geo_info.get("orientation", 0.0) # Simplified calc doesn't use angle here + + orig_x_f = (x_disp / disp_w) * orig_w + orig_y_f = (y_disp / disp_h) * orig_h + pixel_delta_x_f = orig_x_f - ref_x + pixel_delta_y_f = ref_y - orig_y_f + meters_delta_x = pixel_delta_x_f * scale_x + meters_delta_y = pixel_delta_y_f * scale_y + + M_PER_DLAT = 111132.954 + M_PER_DLON_EQ = 111319.488 + m_per_dlon_val = max(abs(M_PER_DLON_EQ * math.cos(ref_lat_rad)), 1e-3) + lat_offset_deg_val = meters_delta_y / M_PER_DLAT + lon_offset_deg_val = meters_delta_x / m_per_dlon_val + final_lat_deg_val = math.degrees(ref_lat_rad) + lat_offset_deg_val + final_lon_deg_val = math.degrees(ref_lon_rad) + lon_offset_deg_val + + if math.isfinite(final_lat_deg_val) and abs(final_lat_deg_val) <= 90.0 and \ + math.isfinite(final_lon_deg_val) and abs(final_lon_deg_val) <= 180.0: + self.request_elevation_for_point( + source_description=config.ELEVATION_SOURCE_SAR_MOUSE, # Define this in config.py + latitude=final_lat_deg_val, + longitude=final_lon_deg_val + ) + else: + logging.warning(f"{log_prefix} Calculated invalid geo-coords for SAR click, skipping elevation.") + self._update_elevation_display(config.ELEVATION_SOURCE_SAR_MOUSE, "Invalid Geo") + + except Exception as e_geo_calc: + logging.exception(f"{log_prefix} Error calculating geo-coords for SAR click elevation request: {e_geo_calc}") + self._update_elevation_display(config.ELEVATION_SOURCE_SAR_MOUSE, "Geo Calc Err") + else: + logging.warning(f"{log_prefix} Insufficient data for SAR click geo-calculation, skipping elevation.") + self._update_elevation_display(config.ELEVATION_SOURCE_SAR_MOUSE, "Geo Data N/A") + # --- >>> END OF NEW ELEVATION REQUEST <<< --- else: logging.warning(f"{log_prefix} Received invalid payload: {payload}") def _handle_map_click_update(self, payload: Optional[Tuple[int, int]]): - """Updates the Map click coordinates state and triggers a Map redraw.""" + """Updates the Map click coordinates state, triggers a Map redraw, and requests elevation.""" log_prefix = "[App Map Click State]" - if self.state.shutting_down: - return - # Validate payload + # ... (codice esistente per aggiornare self.state.last_map_click_coords e self.trigger_map_redraw()) ... if payload and isinstance(payload, tuple) and len(payload) == 2: - # Store the pixel coordinates in AppState self.state.last_map_click_coords = payload - logging.debug( - f"{log_prefix} Updated state.last_map_click_coords to {payload}" - ) - # Trigger map redraw (recomposition should pick up the new marker state) - self.trigger_map_redraw(full_update=False) + logging.debug(f"{log_prefix} Updated state.last_map_click_coords to {payload}") + self.trigger_map_redraw(full_update=False) # Redraw map with marker + + # --- >>> START OF NEW ELEVATION REQUEST <<< --- + mgr = getattr(self, "map_integration_manager", None) + if mgr: + pixel_x, pixel_y = payload + geo_coords = mgr.get_geo_coords_from_map_pixel(pixel_x, pixel_y) + if geo_coords: + lat_deg, lon_deg = geo_coords + self.request_elevation_for_point( + source_description=config.ELEVATION_SOURCE_MAP_MOUSE, # Define this in config.py + latitude=lat_deg, + longitude=lon_deg + ) + else: + logging.warning(f"{log_prefix} Could not get geo-coords for map click, skipping elevation.") + self._update_elevation_display(config.ELEVATION_SOURCE_MAP_MOUSE, "Geo Conv Err") + else: + logging.warning(f"{log_prefix} MapIntegrationManager not available for map click elevation request.") + self._update_elevation_display(config.ELEVATION_SOURCE_MAP_MOUSE, "Map Mgr N/A") + # --- >>> END OF NEW ELEVATION REQUEST <<< --- else: logging.warning(f"{log_prefix} Received invalid payload: {payload}") diff --git a/controlpanel/config.py b/controlpanel/config.py index 450e086..fbbd6dd 100644 --- a/controlpanel/config.py +++ b/controlpanel/config.py @@ -205,4 +205,18 @@ KML_OUTPUT_DIRECTORY = "kml_output" AUTO_LAUNCH_GOOGLE_EARTH = False MAX_KML_FILES = 30 # Max KMLs to keep (0 or less disables cleanup) + +# Path to the root directory of the GeoElevation project +# (i.e. the folder that CONTAINS the subfolder 'geoelevation') +# Leave blank or None if GeoElevation should not be used. +# Examples: +# GEOELEVATION_PROJECT_ROOT_PATH = "/Users/yourname/development/GeoElevation" (absolute) +# GEOELEVATION_PROJECT_ROOT_PATH = "../GeoElevation" (relative to the MapViewer root) +GEOELEVATION_PROJECT_ROOT_PATH = r"C:\src\____GitProjects\GeoElevation" # Or the correct path +# String constants for identifying the source of an elevation request. +# These are used internally in app_main.py to manage UI updates. +ELEVATION_SOURCE_SAR_CENTER = "SAR_Center" +ELEVATION_SOURCE_SAR_MOUSE = "SAR_Mouse" +ELEVATION_SOURCE_MAP_MOUSE = "Map_Mouse" + # --- END OF FILE config.py --- diff --git a/controlpanel/gui/ui.py b/controlpanel/gui/ui.py index da05a46..b1916ef 100644 --- a/controlpanel/gui/ui.py +++ b/controlpanel/gui/ui.py @@ -62,15 +62,17 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls ) self.dropped_stats_var = tk.StringVar(value="Drop (Q): S=0, M=0, Tk=0, Mo=0") self.incomplete_stats_var = tk.StringVar(value="Incmpl (RX): S=0, M=0") - # Checkbox variable for metadata toggle (still needed here) self.show_meta_var = tk.BooleanVar(value=self.app.state.display_sar_metadata) + self.sar_center_elevation_var = tk.StringVar(value="Elev: N/A") + self.sar_mouse_elevation_var = tk.StringVar(value="Elev: N/A") + self.map_mouse_elevation_var = tk.StringVar(value="Elev: N/A") + # --- References to UI widgets --- self.mfd_color_labels: Dict[str, tk.Label] = {} - # References to metadata widgets are REMOVED from this class # --- Initialize UI structure --- - self.init_ui() # Call the UI building method + self.init_ui() logging.debug(f"{log_prefix} ControlPanel frame initialization complete.") @@ -79,510 +81,311 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls """Initializes and arranges the user interface widgets within this frame.""" log_prefix = "[UI Setup]" logging.debug(f"{log_prefix} Starting init_ui widget creation...") - # This frame (self) is placed by its parent (container_frame in App) - # DO NOT pack or grid self here. # --- 1. SAR Parameters Frame --- + # ... (Questa sezione rimane invariata) ... self.sar_params_frame = ttk.Labelframe(self, text="SAR Parameters", padding=5) self.sar_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(5, 2)) - - sar_row = 0 # Row counter for SAR grid - - # Test Image Checkbox + sar_row = 0 self.test_image_var = tk.IntVar(value=1 if config.ENABLE_TEST_MODE else 0) self.test_image_check = ttk.Checkbutton( - self.sar_params_frame, - text="Test Image", - variable=self.test_image_var, + self.sar_params_frame, text="Test Image", variable=self.test_image_var, command=self.app.update_image_mode, ) - self.test_image_check.grid( - row=sar_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2 - ) - - # Record SAR Checkbox + self.test_image_check.grid(row=sar_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2) self.record_sar_var = tk.BooleanVar(value=config.DEFAULT_SAR_RECORDING_ENABLED) self.record_sar_check = ttk.Checkbutton( - self.sar_params_frame, - text="Record SAR", - variable=self.record_sar_var, + self.sar_params_frame, text="Record SAR", variable=self.record_sar_var, command=self.app.toggle_sar_recording, ) - self.record_sar_check.grid( - row=sar_row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=2 - ) - + self.record_sar_check.grid(row=sar_row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=2) sar_row += 1 - - # SAR Size Combobox self.sar_size_label = ttk.Label(self.sar_params_frame, text="Size:") - self.sar_size_label.grid( - row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1 - ) + self.sar_size_label.grid(row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) self.sar_size_combo = ttk.Combobox( - self.sar_params_frame, - values=config.SAR_SIZE_FACTORS, - state="readonly", - width=6, + self.sar_params_frame, values=config.SAR_SIZE_FACTORS, state="readonly", width=6, ) - try: # Set initial value based on current state + try: factor = 1 if self.app.state.sar_display_width > 0: factor = max(1, config.SAR_WIDTH // self.app.state.sar_display_width) sz_str = f"1:{factor}" - if sz_str in config.SAR_SIZE_FACTORS: - self.sar_size_combo.set(sz_str) - else: - self.sar_size_combo.set(config.DEFAULT_SAR_SIZE) - except Exception: # Fallback to default if error - self.sar_size_combo.set(config.DEFAULT_SAR_SIZE) - self.sar_size_combo.grid( - row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1 - ) + if sz_str in config.SAR_SIZE_FACTORS: self.sar_size_combo.set(sz_str) + else: self.sar_size_combo.set(config.DEFAULT_SAR_SIZE) + except Exception: self.sar_size_combo.set(config.DEFAULT_SAR_SIZE) + self.sar_size_combo.grid(row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1) self.sar_size_combo.bind("<>", self.app.update_sar_size) - - # SAR Palette Combobox self.palette_label = ttk.Label(self.sar_params_frame, text="Palette:") self.palette_label.grid(row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1) self.palette_combo = ttk.Combobox( - self.sar_params_frame, - values=config.COLOR_PALETTES, - state="readonly", - width=8, - ) - self.palette_combo.set(self.app.state.sar_palette) # Set from state - self.palette_combo.grid( - row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1 + self.sar_params_frame, values=config.COLOR_PALETTES, state="readonly", width=8, ) + self.palette_combo.set(self.app.state.sar_palette) + self.palette_combo.grid(row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1) self.palette_combo.bind("<>", self.app.update_sar_palette) - sar_row += 1 - - # SAR Contrast Slider self.contrast_label = ttk.Label(self.sar_params_frame, text="Contrast:") - self.contrast_label.grid( - row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1 - ) + self.contrast_label.grid(row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) self.contrast_scale = ttk.Scale( - self.sar_params_frame, - orient=tk.HORIZONTAL, - from_=0.1, - to=3.0, - value=self.app.state.sar_contrast, # Set from state - command=self.app.update_contrast, + self.sar_params_frame, orient=tk.HORIZONTAL, from_=0.1, to=3.0, + value=self.app.state.sar_contrast, command=self.app.update_contrast, ) - self.contrast_scale.grid( - row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1 - ) - - # SAR Brightness Slider + self.contrast_scale.grid(row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1) self.brightness_label = ttk.Label(self.sar_params_frame, text="Brightness:") - self.brightness_label.grid( - row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1 - ) + self.brightness_label.grid(row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1) self.brightness_scale = ttk.Scale( - self.sar_params_frame, - orient=tk.HORIZONTAL, - from_=-100, - to=100, - value=self.app.state.sar_brightness, # Set from state - command=self.app.update_brightness, + self.sar_params_frame, orient=tk.HORIZONTAL, from_=-100, to=100, + value=self.app.state.sar_brightness, command=self.app.update_brightness, ) - self.brightness_scale.grid( - row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1 - ) - - # SAR Metadata Checkbox (Still created here as it belongs logically with SAR params) + self.brightness_scale.grid(row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1) sar_row += 1 - # self.show_meta_var is already created in __init__ self.show_meta_check = ttk.Checkbutton( - self.sar_params_frame, - text="Show SAR Metadata", - variable=self.show_meta_var, - command=self.app.toggle_sar_metadata_display, # Link to app callback + self.sar_params_frame, text="Show SAR Metadata", variable=self.show_meta_var, + command=self.app.toggle_sar_metadata_display, ) - self.show_meta_check.grid( - row=sar_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=(5, 2) - ) - - # Configure SAR frame column weights - self.sar_params_frame.columnconfigure(1, weight=1) # Allow sliders to expand + self.show_meta_check.grid(row=sar_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=(5, 2)) + self.sar_params_frame.columnconfigure(1, weight=1) self.sar_params_frame.columnconfigure(3, weight=1) + # --- 2. MFD Parameters Frame --- + # ... (Questa sezione rimane invariata) ... self.mfd_params_frame = ttk.Labelframe(self, text="MFD Parameters", padding=5) self.mfd_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2) - - mfd_categories_ordered = [ - "Occlusion", - "Cat A", - "Cat B", - "Cat C", - "Cat C1", - "Cat C2", - "Cat C3", - ] + mfd_categories_ordered = ["Occlusion", "Cat A", "Cat B", "Cat C", "Cat C1", "Cat C2", "Cat C3"] num_categories = len(mfd_categories_ordered) - for index, name in enumerate(mfd_categories_ordered): row = index // 2 col_offset = 0 if (index % 2 == 0) else 4 - - # Category Label cat_label = ttk.Label(self.mfd_params_frame, text=f"{name}:") - cat_label.grid( - row=row, column=0 + col_offset, sticky=tk.W, padx=(5, 1), pady=1 - ) - - # Intensity Slider Variable and Widget + cat_label.grid(row=row, column=0 + col_offset, sticky=tk.W, padx=(5,1), pady=1) intensity_var = tk.IntVar(value=config.DEFAULT_MFD_INTENSITY) - try: - intensity_var.set( - self.app.state.mfd_params["categories"][name]["intensity"] - ) - except Exception: - pass + try: intensity_var.set(self.app.state.mfd_params["categories"][name]["intensity"]) + except Exception: pass intensity_scale = ttk.Scale( - self.mfd_params_frame, - orient=tk.HORIZONTAL, - length=100, - from_=0, - to=255, - variable=intensity_var, - command=lambda v, n=name, var=intensity_var: ( - self.app.update_mfd_category_intensity(n, var.get()) - ), + self.mfd_params_frame, orient=tk.HORIZONTAL, length=100, from_=0, to=255, variable=intensity_var, + command=lambda v, n=name, var=intensity_var: self.app.update_mfd_category_intensity(n, var.get()), ) - intensity_scale.grid( - row=row, column=1 + col_offset, sticky=tk.EW, padx=1, pady=1 - ) - - # Color Chooser Button + intensity_scale.grid(row=row, column=1 + col_offset, sticky=tk.EW, padx=1, pady=1) color_button = ttk.Button( - self.mfd_params_frame, - text="Color", - width=5, - command=lambda n=name: self.app.choose_mfd_category_color(n), - ) - color_button.grid( - row=row, column=2 + col_offset, sticky=tk.W, padx=1, pady=1 - ) - - # Color Preview Label - color_label = tk.Label( - self.mfd_params_frame, text="", width=3, relief=tk.SUNKEN, borderwidth=1 + self.mfd_params_frame, text="Color", width=5, command=lambda n=name: self.app.choose_mfd_category_color(n), ) + color_button.grid(row=row, column=2 + col_offset, sticky=tk.W, padx=1, pady=1) + color_label = tk.Label(self.mfd_params_frame, text="", width=3, relief=tk.SUNKEN, borderwidth=1) try: bgr = self.app.state.mfd_params["categories"][name]["color"] hex_color = f"#{bgr[2]:02x}{bgr[1]:02x}{bgr[0]:02x}" color_label.config(background=hex_color) - except Exception: - color_label.config(background="grey") - color_label.grid( - row=row, column=3 + col_offset, sticky=tk.W, padx=(1, 5), pady=1 - ) + except Exception: color_label.config(background="grey") + color_label.grid(row=row, column=3 + col_offset, sticky=tk.W, padx=(1,5), pady=1) self.mfd_color_labels[name] = color_label - - # Raw Map Intensity Slider - last_cat_row = (num_categories - 1) // 2 + last_cat_row = (num_categories -1) // 2 raw_map_col_offset = 4 if (num_categories % 2 != 0) else 0 raw_map_row = last_cat_row if (num_categories % 2 != 0) else last_cat_row + 1 - raw_map_label = ttk.Label(self.mfd_params_frame, text="Raw Map:") - raw_map_label.grid( - row=raw_map_row, - column=0 + raw_map_col_offset, - sticky=tk.W, - padx=(5, 1), - pady=1, - ) - + raw_map_label.grid(row=raw_map_row, column=0 + raw_map_col_offset, sticky=tk.W, padx=(5,1), pady=1) raw_map_intensity_var = tk.IntVar(value=config.DEFAULT_MFD_RAW_MAP_INTENSITY) - try: - raw_map_intensity_var.set(self.app.state.mfd_params["raw_map_intensity"]) - except Exception: - pass - self.mfd_raw_map_intensity_var = raw_map_intensity_var # Keep reference - + try: raw_map_intensity_var.set(self.app.state.mfd_params["raw_map_intensity"]) + except Exception: pass + self.mfd_raw_map_intensity_var = raw_map_intensity_var raw_map_scale = ttk.Scale( - self.mfd_params_frame, - orient=tk.HORIZONTAL, - length=100, - from_=0, - to=255, - variable=raw_map_intensity_var, - command=lambda v: self.app.update_mfd_raw_map_intensity( - raw_map_intensity_var.get() - ), + self.mfd_params_frame, orient=tk.HORIZONTAL, length=100, from_=0, to=255, variable=raw_map_intensity_var, + command=lambda v: self.app.update_mfd_raw_map_intensity(raw_map_intensity_var.get()), ) - raw_map_scale.grid( - row=raw_map_row, - column=1 + raw_map_col_offset, - columnspan=3, - sticky=tk.EW, - padx=(1, 5), - pady=1, - ) - # Configure MFD frame column weights + raw_map_scale.grid(row=raw_map_row, column=1 + raw_map_col_offset, columnspan=3, sticky=tk.EW, padx=(1,5), pady=1) self.mfd_params_frame.columnconfigure(1, weight=1) self.mfd_params_frame.columnconfigure(5, weight=1) # --- 3. Map Parameters Frame --- + # ... (Questa sezione rimane invariata) ... self.map_params_frame = ttk.Labelframe(self, text="Map Parameters", padding=5) self.map_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2) - - map_row = 0 # Row counter for Map grid - - # Map Size Combobox + map_row = 0 self.map_size_label = ttk.Label(self.map_params_frame, text="Map Display Size:") - self.map_size_label.grid( - row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1 - ) + self.map_size_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5,2), pady=1) self.map_size_combo = ttk.Combobox( - self.map_params_frame, - values=config.MAP_SIZE_FACTORS, - state="readonly", - width=6, + self.map_params_frame, values=config.MAP_SIZE_FACTORS, state="readonly", width=6, ) self.map_size_combo.set(config.DEFAULT_MAP_SIZE) - self.map_size_combo.grid( - row=map_row, column=1, sticky=tk.EW, padx=(2, 10), pady=1 - ) + self.map_size_combo.grid(row=map_row, column=1, sticky=tk.EW, padx=(2,10), pady=1) self.map_size_combo.bind("<>", self.app.update_map_size) - - # Save Map Button self.save_map_button = ttk.Button( - self.map_params_frame, - text="Save Map View", - command=self.app.save_current_map_view, + self.map_params_frame, text="Save Map View", command=self.app.save_current_map_view, ) - self.save_map_button.grid( - row=map_row, column=2, columnspan=4, sticky=tk.E, padx=5, pady=1 - ) - + self.save_map_button.grid(row=map_row, column=2, columnspan=4, sticky=tk.E, padx=5, pady=1) map_row += 1 - - # SAR Overlay Checkbox - self.sar_overlay_var = tk.BooleanVar( - value=self.app.state.map_sar_overlay_enabled - ) + self.sar_overlay_var = tk.BooleanVar(value=self.app.state.map_sar_overlay_enabled) self.sar_overlay_check = ttk.Checkbutton( - self.map_params_frame, - text="Show SAR Overlay on Map", - variable=self.sar_overlay_var, + self.map_params_frame, text="Show SAR Overlay on Map", variable=self.sar_overlay_var, command=self.app.toggle_sar_overlay, ) - self.sar_overlay_check.grid( - row=map_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2 - ) - + self.sar_overlay_check.grid(row=map_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2) map_row += 1 - - # SAR Overlay Alpha Slider self.alpha_label = ttk.Label(self.map_params_frame, text="SAR Overlay Alpha:") - self.alpha_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) - self.sar_overlay_alpha_var = tk.DoubleVar( - value=self.app.state.map_sar_overlay_alpha - ) + self.alpha_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5,2), pady=1) + self.sar_overlay_alpha_var = tk.DoubleVar(value=self.app.state.map_sar_overlay_alpha) self.alpha_scale = ttk.Scale( - self.map_params_frame, - orient=tk.HORIZONTAL, - from_=0.0, - to=1.0, - variable=self.sar_overlay_alpha_var, + self.map_params_frame, orient=tk.HORIZONTAL, from_=0.0, to=1.0, variable=self.sar_overlay_alpha_var, ) self.alpha_scale.bind("", self.app.on_alpha_slider_release) - self.alpha_scale.grid( - row=map_row, column=1, columnspan=5, sticky=tk.EW, padx=(0, 5), pady=1 - ) - + self.alpha_scale.grid(row=map_row, column=1, columnspan=5, sticky=tk.EW, padx=(0,5), pady=1) map_row += 1 - - # SAR Shift Inputs and Apply Button shift_label = ttk.Label(self.map_params_frame, text="SAR Shift (deg):") - shift_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) + shift_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5,2), pady=1) lat_label = ttk.Label(self.map_params_frame, text="Lat:") - lat_label.grid(row=map_row, column=1, sticky=tk.W, padx=(0, 0), pady=1) + lat_label.grid(row=map_row, column=1, sticky=tk.W, padx=(0,0), pady=1) self.lat_shift_entry = ttk.Entry( self.map_params_frame, textvariable=self.sar_lat_shift_var, width=10 ) - self.lat_shift_entry.grid( - row=map_row, column=2, sticky=tk.W, padx=(0, 5), pady=1 - ) + self.lat_shift_entry.grid(row=map_row, column=2, sticky=tk.W, padx=(0,5), pady=1) lon_label = ttk.Label(self.map_params_frame, text="Lon:") - lon_label.grid(row=map_row, column=3, sticky=tk.W, padx=(5, 0), pady=1) + lon_label.grid(row=map_row, column=3, sticky=tk.W, padx=(5,0), pady=1) self.lon_shift_entry = ttk.Entry( self.map_params_frame, textvariable=self.sar_lon_shift_var, width=10 ) - self.lon_shift_entry.grid( - row=map_row, column=4, sticky=tk.W, padx=(0, 5), pady=1 - ) + self.lon_shift_entry.grid(row=map_row, column=4, sticky=tk.W, padx=(0,5), pady=1) self.apply_shift_button = ttk.Button( - self.map_params_frame, - text="Apply Shift", - command=self.app.apply_sar_overlay_shift, + self.map_params_frame, text="Apply Shift", command=self.app.apply_sar_overlay_shift, ) - self.apply_shift_button.grid( - row=map_row, column=5, sticky=tk.E, padx=(5, 5), pady=1 - ) - - # Configure Map frame column weights + self.apply_shift_button.grid(row=map_row, column=5, sticky=tk.E, padx=(5,5), pady=1) self.map_params_frame.columnconfigure(2, weight=1) self.map_params_frame.columnconfigure(4, weight=1) - # --- 4. Info Display Frame --- + # --- 4. Info Display Frame (MODIFIED) --- self.info_display_frame = ttk.Labelframe(self, text="Info Display", padding=5) self.info_display_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2) - info_row = 0 # Row counter for Info grid - button_width = 3 # Standard width for Go/GE buttons + info_row = 0 + button_width = 3 + entry_width_coords = 30 # Adjusted width for coords + elevation + entry_width_orient = 10 + entry_width_size = 18 + entry_width_stats_drop = 28 + entry_width_stats_incmpl = 15 + entry_width_elevation = 15 # Width for elevation labels - # --- Row 0: SAR Center Coords --- + # --- Row 0: SAR Center Coords & Elevation --- ref_label = ttk.Label(self.info_display_frame, text="SAR Center:") ref_label.grid(row=info_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) self.sar_center_entry = ttk.Entry( - self.info_display_frame, - textvariable=self.sar_center_coords_var, - state="readonly", - width=35, + self.info_display_frame, textvariable=self.sar_center_coords_var, + state="readonly", width=entry_width_coords ) - self.sar_center_entry.grid( - row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1 + self.sar_center_entry.grid(row=info_row, column=1, columnspan=2, sticky=tk.EW, padx=(0, 2), pady=1) + + # --- >>> NEW: SAR Center Elevation Label <<< --- + self.sar_center_elev_label = ttk.Label( + self.info_display_frame, textvariable=self.sar_center_elevation_var, + width=entry_width_elevation, anchor=tk.W, relief=tk.SUNKEN, borderwidth=1 ) + self.sar_center_elev_label.grid(row=info_row, column=3, sticky=tk.EW, padx=(2,2), pady=1) + # --- >>> END NEW <<< --- + self.ref_gmaps_button = ttk.Button( - self.info_display_frame, - text="Go", - width=button_width, - command=lambda: self.app.go_to_google_maps("sar_center"), - ) - self.ref_gmaps_button.grid( - row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1 + self.info_display_frame, text="Go", width=button_width, + command=lambda: self.app.go_to_google_maps("sar_center") ) + self.ref_gmaps_button.grid(row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1) self.ref_gearth_button = ttk.Button( - self.info_display_frame, - text="GE", - width=button_width, - command=lambda: self.app.go_to_google_earth("sar_center"), - ) - self.ref_gearth_button.grid( - row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1 + self.info_display_frame, text="GE", width=button_width, + command=lambda: self.app.go_to_google_earth("sar_center") ) + self.ref_gearth_button.grid(row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1) info_row += 1 # --- Row 1: Image Orient & Image Size --- orient_label = ttk.Label(self.info_display_frame, text="Image Orient:") orient_label.grid(row=info_row, column=0, sticky=tk.W, padx=5, pady=1) self.sar_orientation_entry = ttk.Entry( - self.info_display_frame, - textvariable=self.sar_orientation_var, - state="readonly", - width=15, - ) - self.sar_orientation_entry.grid( - row=info_row, column=1, sticky=tk.EW, padx=(0, 5), pady=1 + self.info_display_frame, textvariable=self.sar_orientation_var, + state="readonly", width=entry_width_orient ) + self.sar_orientation_entry.grid(row=info_row, column=1, sticky=tk.EW, padx=(0, 5), pady=1) + size_label = ttk.Label(self.info_display_frame, text="Image Size:") - size_label.grid(row=info_row, column=2, sticky=tk.W, padx=(10, 2), pady=1) + size_label.grid(row=info_row, column=2, sticky=tk.W, padx=(5, 2), pady=1) # Adjusted column self.sar_size_entry = ttk.Entry( - self.info_display_frame, - textvariable=self.sar_size_km_var, - state="readonly", - width=25, - ) - self.sar_size_entry.grid( - row=info_row, column=3, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1 + self.info_display_frame, textvariable=self.sar_size_km_var, + state="readonly", width=entry_width_size ) + # Span across remaining columns for Go/GE alignment + self.sar_size_entry.grid(row=info_row, column=3, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1) info_row += 1 - # --- Row 2: SAR Mouse Coords --- + # --- Row 2: SAR Mouse Coords & Elevation --- sar_mouse_label = ttk.Label(self.info_display_frame, text="SAR Mouse:") sar_mouse_label.grid(row=info_row, column=0, sticky=tk.W, padx=5, pady=1) self.mouse_latlon_entry = ttk.Entry( - self.info_display_frame, - textvariable=self.mouse_coords_var, - state="readonly", - width=35, + self.info_display_frame, textvariable=self.mouse_coords_var, + state="readonly", width=entry_width_coords ) - self.mouse_latlon_entry.grid( - row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1 + self.mouse_latlon_entry.grid(row=info_row, column=1, columnspan=2, sticky=tk.EW, padx=(0, 2), pady=1) + + # --- >>> NEW: SAR Mouse Elevation Label <<< --- + self.sar_mouse_elev_label = ttk.Label( + self.info_display_frame, textvariable=self.sar_mouse_elevation_var, + width=entry_width_elevation, anchor=tk.W, relief=tk.SUNKEN, borderwidth=1 ) + self.sar_mouse_elev_label.grid(row=info_row, column=3, sticky=tk.EW, padx=(2,2), pady=1) + # --- >>> END NEW <<< --- + self.sar_mouse_gmaps_button = ttk.Button( - self.info_display_frame, - text="Go", - width=button_width, - command=lambda: self.app.go_to_google_maps("sar_mouse"), - ) - self.sar_mouse_gmaps_button.grid( - row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1 + self.info_display_frame, text="Go", width=button_width, + command=lambda: self.app.go_to_google_maps("sar_mouse") ) + self.sar_mouse_gmaps_button.grid(row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1) self.sar_mouse_gearth_button = ttk.Button( - self.info_display_frame, - text="GE", - width=button_width, - command=lambda: self.app.go_to_google_earth("sar_mouse"), - ) - self.sar_mouse_gearth_button.grid( - row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1 + self.info_display_frame, text="GE", width=button_width, + command=lambda: self.app.go_to_google_earth("sar_mouse") ) + self.sar_mouse_gearth_button.grid(row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1) info_row += 1 - # --- Row 3: Map Mouse Coords --- + # --- Row 3: Map Mouse Coords & Elevation --- map_mouse_label = ttk.Label(self.info_display_frame, text="Map Mouse:") map_mouse_label.grid(row=info_row, column=0, sticky=tk.W, padx=5, pady=1) self.map_mouse_latlon_entry = ttk.Entry( - self.info_display_frame, - textvariable=self.map_mouse_coords_var, - state="readonly", - width=35, + self.info_display_frame, textvariable=self.map_mouse_coords_var, + state="readonly", width=entry_width_coords ) - self.map_mouse_latlon_entry.grid( - row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1 + self.map_mouse_latlon_entry.grid(row=info_row, column=1, columnspan=2, sticky=tk.EW, padx=(0, 2), pady=1) + + # --- >>> NEW: Map Mouse Elevation Label <<< --- + self.map_mouse_elev_label = ttk.Label( + self.info_display_frame, textvariable=self.map_mouse_elevation_var, + width=entry_width_elevation, anchor=tk.W, relief=tk.SUNKEN, borderwidth=1 ) + self.map_mouse_elev_label.grid(row=info_row, column=3, sticky=tk.EW, padx=(2,2), pady=1) + # --- >>> END NEW <<< --- + self.map_mouse_gmaps_button = ttk.Button( - self.info_display_frame, - text="Go", - width=button_width, - command=lambda: self.app.go_to_google_maps("map_mouse"), - ) - self.map_mouse_gmaps_button.grid( - row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1 + self.info_display_frame, text="Go", width=button_width, + command=lambda: self.app.go_to_google_maps("map_mouse") ) + self.map_mouse_gmaps_button.grid(row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1) self.map_mouse_gearth_button = ttk.Button( - self.info_display_frame, - text="GE", - width=button_width, - command=lambda: self.app.go_to_google_earth("map_mouse"), - ) - self.map_mouse_gearth_button.grid( - row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1 + self.info_display_frame, text="GE", width=button_width, + command=lambda: self.app.go_to_google_earth("map_mouse") ) + self.map_mouse_gearth_button.grid(row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1) info_row += 1 # --- Row 4: Drop & Incomplete Stats --- dropped_label_text = ttk.Label(self.info_display_frame, text="Stats Drop:") dropped_label_text.grid(row=info_row, column=0, sticky=tk.W, padx=5, pady=1) self.dropped_entry = ttk.Entry( - self.info_display_frame, - textvariable=self.dropped_stats_var, - state="readonly", - width=30, - ) - self.dropped_entry.grid( - row=info_row, column=1, sticky=tk.EW, padx=(0, 5), pady=1 + self.info_display_frame, textvariable=self.dropped_stats_var, + state="readonly", width=entry_width_stats_drop ) + self.dropped_entry.grid(row=info_row, column=1, columnspan=2, sticky=tk.EW, padx=(0, 5), pady=1) # Span 2 cols + incomplete_label_text = ttk.Label(self.info_display_frame, text="Incomplete:") - incomplete_label_text.grid( - row=info_row, column=2, sticky=tk.W, padx=(10, 2), pady=1 - ) + incomplete_label_text.grid(row=info_row, column=3, sticky=tk.W, padx=(5, 2), pady=1) # Start at col 3 self.incomplete_entry = ttk.Entry( - self.info_display_frame, - textvariable=self.incomplete_stats_var, - state="readonly", - width=15, - ) - self.incomplete_entry.grid( - row=info_row, column=3, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1 + self.info_display_frame, textvariable=self.incomplete_stats_var, + state="readonly", width=entry_width_stats_incmpl ) + self.incomplete_entry.grid(row=info_row, column=4, columnspan=2, sticky=tk.EW, padx=(0, 5), pady=1) # Span 2 cols info_row += 1 # --- Row 5: "GE All" Button --- @@ -594,85 +397,78 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls ) # Configure column weights for Info Display frame - self.info_display_frame.columnconfigure(1, weight=1) # Entry column - self.info_display_frame.columnconfigure(3, weight=1) # Entry column - self.info_display_frame.columnconfigure(4, weight=0) # Button Go - self.info_display_frame.columnconfigure(5, weight=0) # Button GE - logging.debug(f"{log_prefix} Info Display frame created.") + self.info_display_frame.columnconfigure(1, weight=1) # Coords entry + self.info_display_frame.columnconfigure(2, weight=1) # Coords entry (continued for span) / Size Label + self.info_display_frame.columnconfigure(3, weight=0) # Elevation Label / Size Entry / Incomplete Label + self.info_display_frame.columnconfigure(4, weight=0) # Go Button / Size Entry / Incomplete Entry + self.info_display_frame.columnconfigure(5, weight=0) # GE Button / Size Entry / Incomplete Entry - # --- 5. Metadata Display Frame (Creation REMOVED from here) --- - # This is now created and managed in ControlPanelApp + logging.debug(f"{log_prefix} Info Display frame created with elevation labels.") - # --- End of init_ui --- logging.debug(f"{log_prefix} init_ui widget creation complete.") # --- UI Update Methods --- + # ... (metodi set_sar_center_coords, set_mouse_coordinates, ecc. rimangono invariati) ... def set_sar_center_coords(self, latitude_str: str, longitude_str: str): - """Updates the SAR Center coordinates display.""" text = f"Lat={latitude_str}, Lon={longitude_str}" - try: - self.sar_center_coords_var.set(text) - except Exception as e: - logging.warning(f"[UI Update] Error setting SAR center coords: {e}") + try: self.sar_center_coords_var.set(text) + except Exception as e: logging.warning(f"[UI Update] Error setting SAR center coords: {e}") def set_mouse_coordinates(self, latitude_str: str, longitude_str: str): - """Updates the SAR Mouse coordinates display.""" text = f"Lat={latitude_str}, Lon={longitude_str}" - try: - self.mouse_coords_var.set(text) - except Exception as e: - logging.warning(f"[UI Update] Error setting SAR mouse coords: {e}") + try: self.mouse_coords_var.set(text) + except Exception as e: logging.warning(f"[UI Update] Error setting SAR mouse coords: {e}") def set_map_mouse_coordinates(self, latitude_str: str, longitude_str: str): - """Updates the Map Mouse coordinates display.""" text = f"Lat={latitude_str}, Lon={longitude_str}" - try: - self.map_mouse_coords_var.set(text) - except Exception as e: - logging.warning(f"[UI Update] Error setting Map mouse coords: {e}") + try: self.map_mouse_coords_var.set(text) + except Exception as e: logging.warning(f"[UI Update] Error setting Map mouse coords: {e}") def set_sar_orientation(self, orientation_deg_str: str): - """Updates the SAR Orientation display.""" - try: - self.sar_orientation_var.set(orientation_deg_str) - except Exception as e: - logging.warning(f"[UI Update] Error setting SAR orientation: {e}") + try: self.sar_orientation_var.set(orientation_deg_str) + except Exception as e: logging.warning(f"[UI Update] Error setting SAR orientation: {e}") def set_sar_size_km(self, size_text: str): - """Updates the SAR Size display.""" - try: - self.sar_size_km_var.set(size_text) - except Exception as e: - logging.warning(f"[UI Update] Error setting SAR size: {e}") + try: self.sar_size_km_var.set(size_text) + except Exception as e: logging.warning(f"[UI Update] Error setting SAR size: {e}") def set_statistics_display(self, dropped_text: str, incomplete_text: str): - """Updates the statistics display fields.""" try: self.dropped_stats_var.set(dropped_text) self.incomplete_stats_var.set(incomplete_text) - except Exception as e: - logging.warning(f"[UI Update] Error setting stats display: {e}") + except Exception as e: logging.warning(f"[UI Update] Error setting stats display: {e}") - def update_mfd_color_display( - self, category_name: str, color_bgr_tuple: Tuple[int, int, int] - ): - """Updates the background color of an MFD category preview label.""" + def update_mfd_color_display(self, category_name: str, color_bgr_tuple: Tuple[int, int, int]): if category_name in self.mfd_color_labels: lbl = self.mfd_color_labels[category_name] - # Format BGR tuple to hex color string #RRGGBB - hex_color = ( - f"#{color_bgr_tuple[2]:02x}" - f"{color_bgr_tuple[1]:02x}" - f"{color_bgr_tuple[0]:02x}" - ) + hex_color = f"#{color_bgr_tuple[2]:02x}{color_bgr_tuple[1]:02x}{color_bgr_tuple[0]:02x}" try: - # Update label background if it still exists - if lbl.winfo_exists(): - lbl.config(background=hex_color) - except Exception as e: - logging.exception( - f"[UI Update] Error updating MFD color for {category_name}: {e}" - ) + if lbl.winfo_exists(): lbl.config(background=hex_color) + except Exception as e: logging.exception(f"[UI Update] Error updating MFD color for {category_name}: {e}") + + def set_sar_center_elevation(self, elevation_text: str): + """Updates the SAR Center elevation display label.""" + try: + # The text is now pre-formatted by app_main.py + self.sar_center_elevation_var.set(elevation_text) + except Exception as e: + logging.warning(f"[UI Update] Error setting SAR center elevation: {e}") + + def set_sar_mouse_elevation(self, elevation_text: str): + """Updates the SAR Mouse elevation display label.""" + try: + # The text is now pre-formatted by app_main.py + self.sar_mouse_elevation_var.set(elevation_text) + except Exception as e: + logging.warning(f"[UI Update] Error setting SAR mouse elevation: {e}") + + def set_map_mouse_elevation(self, elevation_text: str): + """Updates the Map Mouse elevation display label.""" + try: + # The text is now pre-formatted by app_main.py + self.map_mouse_elevation_var.set(elevation_text) + except Exception as e: + logging.warning(f"[UI Update] Error setting Map mouse elevation: {e}") # --- StatusBar Class ---