diff --git a/app.py b/app.py index 4ff484a..7f83d9e 100644 --- a/app.py +++ b/app.py @@ -17,7 +17,7 @@ import os import logging import math import sys -import socket # Required for network setup +import socket # Required for network setup from typing import Optional, Tuple, Any, Dict, TYPE_CHECKING import datetime @@ -27,6 +27,7 @@ from tkinter import ttk from tkinter import colorchooser import numpy as np import screeninfo + # Conditional map imports are handled further down # --- Configuration Import --- @@ -36,6 +37,7 @@ import config # Import and call the setup function from the dedicated module try: from logging_config import setup_logging + # Configure logging as early as possible setup_logging() except ImportError: @@ -43,20 +45,27 @@ 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' + format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s", ) # --- Application Modules Import --- from ui import ControlPanel, StatusBar, create_main_window + # image_processing functions are used by other modules, App uses ImagePipeline now # from image_processing import ... from display import DisplayManager -from utils import put_queue, clear_queue, decimal_to_dms, generate_sar_kml, launch_google_earth +from utils import ( + put_queue, + clear_queue, + decimal_to_dms, + generate_sar_kml, + launch_google_earth, +) from network import create_udp_socket, close_udp_socket from receiver import UdpReceiver -from app_state import AppState # Centralized state -from test_mode_manager import TestModeManager # Manages test mode logic -from image_pipeline import ImagePipeline # Manages normal image processing +from app_state import AppState # Centralized state +from test_mode_manager import TestModeManager # Manages test mode logic +from image_pipeline import ImagePipeline # Manages normal image processing # --- Map related imports (Conditional) --- # Check if map modules are present before attempting specific imports @@ -64,17 +73,17 @@ map_libs_found = True try: import mercantile import pyproj - from PIL import Image # Needed by map modules usually + from PIL import Image # Needed by map modules usually except ImportError as map_lib_err: logging.warning( f"[App Init] Failed to import core map library ({map_lib_err}). Map functionality disabled." ) map_libs_found = False # Define placeholders for type hinting if core libs failed - BaseMapService = None # type: ignore - MapTileManager = None # type: ignore - MapDisplayWindow = None # type: ignore - MapIntegrationManager = None # type: ignore + BaseMapService = None # type: ignore + MapTileManager = None # type: ignore + MapDisplayWindow = None # type: ignore + MapIntegrationManager = None # type: ignore MapCalculationError = Exception if map_libs_found: @@ -84,11 +93,13 @@ if map_libs_found: from map_utils import ( get_bounding_box_from_center_size, get_tile_ranges_for_bbox, - MapCalculationError + MapCalculationError, ) from map_display import MapDisplayWindow + # Import the integration manager from map_integration import MapIntegrationManager + MAP_MODULES_LOADED = True except ImportError as map_import_err: logging.warning( @@ -96,10 +107,10 @@ if map_libs_found: ) MAP_MODULES_LOADED = False # Define placeholders if specific modules failed - BaseMapService = None # type: ignore - MapTileManager = None # type: ignore - MapDisplayWindow = None # type: ignore - MapIntegrationManager = None # type: ignore + BaseMapService = None # type: ignore + MapTileManager = None # type: ignore + MapDisplayWindow = None # type: ignore + MapIntegrationManager = None # type: ignore MapCalculationError = Exception else: MAP_MODULES_LOADED = False @@ -107,7 +118,7 @@ else: # Type checking block for App class reference in managers if TYPE_CHECKING: # This avoids circular imports at runtime but helps type checkers - pass # No direct import needed here as other modules import App + pass # No direct import needed here as other modules import App # --- Main Application Class --- @@ -116,6 +127,7 @@ class App: Main application class. Manages UI, display, processing, network, state, and orchestrates various managers (Test Mode, Image Pipeline, Map Integration). """ + # --- Class Attributes --- # Timers and offsets previously here are now managed by TestModeManager # Map components previously here are now managed by MapIntegrationManager @@ -131,7 +143,7 @@ class App: log_prefix = "[App Set Status]" # Check state exists and flag before proceeding # Use hasattr for robustness during init/shutdown - if not hasattr(self, 'state') or self.state.shutting_down: + if not hasattr(self, "state") or self.state.shutting_down: return new_status_prefix = f"Status: {message}" @@ -141,14 +153,20 @@ class App: def _update_status_text_on_main_thread(): """Internal function to update status text, runs in main GUI thread.""" # Check state again inside the scheduled function - if not hasattr(self, 'state') or self.state.shutting_down: + if not hasattr(self, "state") or self.state.shutting_down: return try: # Check if statusbar exists and is valid Tkinter widget - if not (hasattr(self, "statusbar") and isinstance(self.statusbar, tk.Widget) and self.statusbar.winfo_exists()): - # Use WARNING if statusbar is gone unexpectedly - logging.warning(f"{log_prefix} Statusbar widget not available, cannot update status.") - return + if not ( + hasattr(self, "statusbar") + and isinstance(self.statusbar, tk.Widget) + and self.statusbar.winfo_exists() + ): + # Use WARNING if statusbar is gone unexpectedly + logging.warning( + f"{log_prefix} Statusbar widget not available, cannot update status." + ) + return # Get current text and parse existing parts (keep info after '|') current_text: str = self.statusbar.cget("text") @@ -156,13 +174,15 @@ class App: suffix = "" # Rebuild suffix from parts after the first one if len(parts) > 1: - suffix_parts = [p.strip() for p in parts[1:] if p.strip()] - if suffix_parts: - suffix = " | " + " | ".join(suffix_parts) + suffix_parts = [p.strip() for p in parts[1:] if p.strip()] + if suffix_parts: + suffix = " | " + " | ".join(suffix_parts) # Combine new prefix and existing suffix final_text = f"{new_status_prefix}{suffix}" - logging.debug(f"{log_prefix} Updating status bar text to: '{final_text}'") + logging.debug( + f"{log_prefix} Updating status bar text to: '{final_text}'" + ) # Call StatusBar's method to update self.statusbar.set_status_text(final_text) @@ -176,14 +196,16 @@ class App: # Schedule the update on the main GUI thread try: - if hasattr(self, 'root') and self.root and self.root.winfo_exists(): + if hasattr(self, "root") and self.root and self.root.winfo_exists(): # Use after_idle to ensure it runs when Tkinter is idle self.root.after_idle(_update_status_text_on_main_thread) # else: Don't log if root is None or destroyed, expected during shutdown except Exception as e: # Log error only if not shutting down - if not hasattr(self, 'state') or not self.state.shutting_down: - logging.warning(f"{log_prefix} Error scheduling status update via after_idle: {e}") + if not hasattr(self, "state") or not self.state.shutting_down: + logging.warning( + f"{log_prefix} Error scheduling status update via after_idle: {e}" + ) # --- LUT Generation Methods --- def update_brightness_contrast_lut(self): @@ -192,9 +214,9 @@ class App: logging.debug(f"{log_prefix} Updating SAR Brightness/Contrast LUT...") # Check if state is initialized - if not hasattr(self, 'state'): - logging.error(f"{log_prefix} AppState not ready for LUT update.") - return + if not hasattr(self, "state"): + logging.error(f"{log_prefix} AppState not ready for LUT update.") + return try: # Read parameters from AppState @@ -202,17 +224,23 @@ class App: contrast_val = max(0.01, self.state.sar_contrast) brightness_val = self.state.sar_brightness except AttributeError: - # This case should be covered by the hasattr check above, but keep for safety - logging.error(f"{log_prefix} Error accessing state for SAR LUT parameters (AttributeError).") - return + # This case should be covered by the hasattr check above, but keep for safety + logging.error( + f"{log_prefix} Error accessing state for SAR LUT parameters (AttributeError)." + ) + return except Exception as e: - logging.error(f"{log_prefix} Unexpected error accessing state for SAR LUT params: {e}") - return + logging.error( + f"{log_prefix} Unexpected error accessing state for SAR LUT params: {e}" + ) + return # Calculate the LUT using numpy vectorized operations try: # Create an array representing pixel values 0-255 - lut_values = np.arange(256, dtype=np.float32) # Use float for calculation accuracy + lut_values = np.arange( + 256, dtype=np.float32 + ) # Use float for calculation accuracy # Apply contrast (multiplication) and brightness (addition) adjusted_values = (lut_values * contrast_val) + brightness_val # Clip the results to the valid 0-255 range and convert to uint8 @@ -224,7 +252,9 @@ class App: identity_lut = np.arange(256, dtype=np.uint8) # Store fallback LUT in state self.state.brightness_contrast_lut = identity_lut - logging.error(f"{log_prefix} Using identity SAR LUT as fallback due to calculation error.") + logging.error( + f"{log_prefix} Using identity SAR LUT as fallback due to calculation error." + ) return # Store the calculated LUT back into AppState @@ -237,7 +267,7 @@ class App: logging.debug(f"{log_prefix} Recalculating MFD Color LUT...") # Check if state is initialized - if not hasattr(self, 'state'): + if not hasattr(self, "state"): logging.error(f"{log_prefix} AppState not ready for MFD LUT update.") return @@ -248,13 +278,17 @@ class App: pixel_to_category = mfd_params["pixel_to_category"] categories = mfd_params["categories"] except AttributeError: - logging.error(f"{log_prefix} Error accessing mfd_params state (AttributeError).") + logging.error( + f"{log_prefix} Error accessing mfd_params state (AttributeError)." + ) return except KeyError as ke: logging.error(f"{log_prefix} Missing key in AppState mfd_params: {ke}") return except Exception as e: - logging.error(f"{log_prefix} Unexpected error accessing state for MFD LUT params: {e}") + logging.error( + f"{log_prefix} Unexpected error accessing state for MFD LUT params: {e}" + ) return # Initialize a new LUT array (256 entries, 3 channels BGR) with zeros @@ -268,9 +302,13 @@ class App: if category_name: # --- Handle Categorized Pixels (0-31 typically) --- - cat_data = categories[category_name] # Get category data (color, intensity) - base_bgr = cat_data["color"] # Base BGR color tuple - intensity_factor = cat_data["intensity"] / 255.0 # Intensity slider value (0-1) + cat_data = categories[ + category_name + ] # Get category data (color, intensity) + base_bgr = cat_data["color"] # Base BGR color tuple + intensity_factor = ( + cat_data["intensity"] / 255.0 + ) # Intensity slider value (0-1) # Calculate final color components applying intensity final_b = float(base_bgr[0]) * intensity_factor @@ -278,9 +316,15 @@ class App: final_r = float(base_bgr[2]) * intensity_factor # Clip and convert to integer for the LUT entry - new_lut[index_value, 0] = np.clip(int(round(final_b)), 0, 255) # Blue - new_lut[index_value, 1] = np.clip(int(round(final_g)), 0, 255) # Green - new_lut[index_value, 2] = np.clip(int(round(final_r)), 0, 255) # Red + new_lut[index_value, 0] = np.clip( + int(round(final_b)), 0, 255 + ) # Blue + new_lut[index_value, 1] = np.clip( + int(round(final_g)), 0, 255 + ) # Green + new_lut[index_value, 2] = np.clip( + int(round(final_r)), 0, 255 + ) # Red elif 32 <= index_value <= 255: # --- Handle Raw Map Pixels (32-255 typically) --- @@ -298,61 +342,82 @@ class App: # We default these to black (as per initial new_lut value) if category_name is None: # Log unexpected unmapped indices - logging.warning(f"{log_prefix} Index {index_value} has no assigned category. Defaulting to black.") + logging.warning( + f"{log_prefix} Index {index_value} has no assigned category. Defaulting to black." + ) # new_lut[index_value, :] is already [0, 0, 0] # Store the completed LUT back into AppState self.state.mfd_lut = new_lut - logging.info(f"{log_prefix} MFD LUT update complete and stored in AppState.") + logging.info( + f"{log_prefix} MFD LUT update complete and stored in AppState." + ) except KeyError as ke: - logging.error(f"{log_prefix} Missing category key '{ke}' during MFD LUT generation.") - self._apply_fallback_mfd_lut() # Apply fallback if structure error occurs + logging.error( + f"{log_prefix} Missing category key '{ke}' during MFD LUT generation." + ) + self._apply_fallback_mfd_lut() # Apply fallback if structure error occurs except Exception as e: - logging.critical(f"{log_prefix} CRITICAL error during MFD LUT generation:", exc_info=True) - self._apply_fallback_mfd_lut() # Apply fallback on critical errors + logging.critical( + f"{log_prefix} CRITICAL error during MFD LUT generation:", exc_info=True + ) + self._apply_fallback_mfd_lut() # Apply fallback on critical errors def _apply_fallback_mfd_lut(self): """Applies a simple grayscale ramp as a fallback MFD LUT in case of errors.""" log_prefix = "[MFD LUT Update]" - logging.error(f"{log_prefix} Applying fallback grayscale MFD LUT due to previous errors.") - if hasattr(self, 'state'): + logging.error( + f"{log_prefix} Applying fallback grayscale MFD LUT due to previous errors." + ) + if hasattr(self, "state"): try: - # Create a simple grayscale ramp (0-255) - gray_ramp = np.arange(256,dtype=np.uint8) - # Convert to BGR format for the LUT - fallback_lut = cv2.cvtColor(gray_ramp[:, np.newaxis], cv2.COLOR_GRAY2BGR)[:, 0, :] - self.state.mfd_lut = fallback_lut + # Create a simple grayscale ramp (0-255) + gray_ramp = np.arange(256, dtype=np.uint8) + # Convert to BGR format for the LUT + fallback_lut = cv2.cvtColor( + gray_ramp[:, np.newaxis], cv2.COLOR_GRAY2BGR + )[:, 0, :] + self.state.mfd_lut = fallback_lut except Exception as fallback_e: - logging.critical(f"{log_prefix} Failed even to create fallback MFD LUT: {fallback_e}") - # Ensure state LUT is at least *something* to avoid None errors later - self.state.mfd_lut = np.zeros((256, 3), dtype=np.uint8) - + logging.critical( + f"{log_prefix} Failed even to create fallback MFD LUT: {fallback_e}" + ) + # Ensure state LUT is at least *something* to avoid None errors later + self.state.mfd_lut = np.zeros((256, 3), dtype=np.uint8) # --- UI Callback Methods --- - def update_image_mode(self): # UI Callback + def update_image_mode(self): # UI Callback """Handles switching between Test and Normal Mode based on UI checkbox.""" log_prefix = "[App Mode Switch]" # Check essential components exist - if not hasattr(self, 'state') or not hasattr(self, 'test_mode_manager'): - logging.error(f"{log_prefix} State or TestModeManager not initialized. Cannot switch mode.") + if not hasattr(self, "state") or not hasattr(self, "test_mode_manager"): + logging.error( + f"{log_prefix} State or TestModeManager not initialized. Cannot switch mode." + ) return if self.state.shutting_down: - return # Ignore if shutting down + return # Ignore if shutting down try: is_test_req = False # Safely get checkbox state - if hasattr(self.control_panel, "test_image_var") and isinstance(self.control_panel.test_image_var, tk.Variable): + if hasattr(self.control_panel, "test_image_var") and isinstance( + self.control_panel.test_image_var, tk.Variable + ): is_test_req = self.control_panel.test_image_var.get() == 1 else: - logging.warning(f"{log_prefix} test_image_var not found or invalid in control_panel.") - # Fallback: assume current state to avoid unintended switch - is_test_req = self.state.test_mode_active + logging.warning( + f"{log_prefix} test_image_var not found or invalid in control_panel." + ) + # Fallback: assume current state to avoid unintended switch + is_test_req = self.state.test_mode_active # --- Perform switch only if requested mode differs from current state --- if is_test_req != self.state.test_mode_active: - logging.info(f"{log_prefix} Request to change Test Mode state to: {is_test_req}") + logging.info( + f"{log_prefix} Request to change Test Mode state to: {is_test_req}" + ) # Update the state flag first self.state.test_mode_active = is_test_req @@ -360,13 +425,15 @@ class App: if self.state.test_mode_active: # Attempt to activate the manager if self.test_mode_manager.activate(): - # If manager activated successfully, perform UI/State actions - self.activate_test_mode_ui_actions() + # If manager activated successfully, perform UI/State actions + self.activate_test_mode_ui_actions() else: - # If manager activation failed (e.g., missing test data) - logging.error(f"{log_prefix} TestModeManager activation failed. Reverting UI and state.") - # Try to revert the UI checkbox and state flag - self._revert_test_mode_ui() + # If manager activation failed (e.g., missing test data) + logging.error( + f"{log_prefix} TestModeManager activation failed. Reverting UI and state." + ) + # Try to revert the UI checkbox and state flag + self._revert_test_mode_ui() else: # Deactivate the manager (stops timers) self.test_mode_manager.deactivate() @@ -378,82 +445,110 @@ class App: # Update the status bar display self.update_status() else: - # Log if no change is needed - logging.debug(f"{log_prefix} Requested mode ({is_test_req}) is the same as current mode. No change.") + # Log if no change is needed + logging.debug( + f"{log_prefix} Requested mode ({is_test_req}) is the same as current mode. No change." + ) except tk.TclError as e: - # Handle Tkinter errors (e.g., widget destroyed) - logging.warning(f"{log_prefix} UI error accessing checkbox state (TclError): {e}") + # Handle Tkinter errors (e.g., widget destroyed) + logging.warning( + f"{log_prefix} UI error accessing checkbox state (TclError): {e}" + ) except AttributeError as ae: - # Handle potential errors if managers aren't fully initialized - logging.error(f"{log_prefix} Missing attribute during mode update (likely manager init issue): {ae}") + # Handle potential errors if managers aren't fully initialized + logging.error( + f"{log_prefix} Missing attribute during mode update (likely manager init issue): {ae}" + ) except Exception as e: - # Log any other unexpected errors during the mode switch process - logging.exception(f"{log_prefix} Unexpected error during mode update:") + # Log any other unexpected errors during the mode switch process + logging.exception(f"{log_prefix} Unexpected error during mode update:") - def update_sar_size(self, event=None): # UI Callback + def update_sar_size(self, event=None): # UI Callback """Callback for SAR size combobox change. Updates state and triggers processing.""" log_prefix = "[App CB SAR Size]" if self.state.shutting_down: - return # Ignore if shutting down + return # Ignore if shutting down # If map overlay is enabled, SAR size is fixed, prevent UI change # Check map manager existence as a proxy for map being active - map_active = hasattr(self, 'map_integration_manager') and self.map_integration_manager is not None - if config.ENABLE_MAP_OVERLAY and map_active: - logging.debug(f"{log_prefix} Ignoring SAR size change request (Map Overlay active).") - # Try to force the UI combobox back to the map-enforced value - try: - # Calculate the fixed factor based on current state (set during init if map on) - forced_factor = config.SAR_WIDTH // self.state.sar_display_width if self.state.sar_display_width > 0 else 1 - forced_size_str = f"1:{forced_factor}" - if hasattr(self.control_panel, 'sar_size_combo'): - # Only set if the current value differs to avoid unnecessary events - if self.control_panel.sar_size_combo.get() != forced_size_str: - self.control_panel.sar_size_combo.set(forced_size_str) - except Exception as e: - logging.warning(f"{log_prefix} Failed to reset SAR size combobox UI for map mode: {e}") - return # Exit without processing size change + map_active = ( + hasattr(self, "map_integration_manager") + and self.map_integration_manager is not None + ) + #if config.ENABLE_MAP_OVERLAY and map_active: + # logging.debug( + # f"{log_prefix} Ignoring SAR size change request (Map Overlay active)." + # ) + # # Try to force the UI combobox back to the map-enforced value + # try: + # # Calculate the fixed factor based on current state (set during init if map on) + # forced_factor = ( + # config.SAR_WIDTH // self.state.sar_display_width + # if self.state.sar_display_width > 0 + # else 1 + # ) + # forced_size_str = f"1:{forced_factor}" + # if hasattr(self.control_panel, "sar_size_combo"): + # # Only set if the current value differs to avoid unnecessary events + # if self.control_panel.sar_size_combo.get() != forced_size_str: + # self.control_panel.sar_size_combo.set(forced_size_str) + # except Exception as e: + # logging.warning( + # f"{log_prefix} Failed to reset SAR size combobox UI for map mode: {e}" + # ) + # return # Exit without processing size change # Proceed with size change if map overlay is not active try: - selected_size_str = self.control_panel.sar_size_combo.get() - logging.debug(f"{log_prefix} SAR display size selected: '{selected_size_str}'") + selected_size_str = self.control_panel.sar_size_combo.get() + logging.debug( + f"{log_prefix} SAR display size selected: '{selected_size_str}'" + ) - # Parse the factor from the string "1:N" - factor = 1 # Default factor - if selected_size_str != "1:1": - # Split by ':' and take the second part, convert to int - factor = int(selected_size_str.split(":")[1]) + # Parse the factor from the string "1:N" + factor = 1 # Default factor + if selected_size_str != "1:1": + # Split by ':' and take the second part, convert to int + factor = int(selected_size_str.split(":")[1]) - # Calculate new dimensions based on factor - new_width = max(1, config.SAR_WIDTH // factor) - new_height = max(1, config.SAR_HEIGHT // factor) + # Calculate new dimensions based on factor + new_width = max(1, config.SAR_WIDTH // factor) + new_height = max(1, config.SAR_HEIGHT // factor) - # Update the display size in AppState - self.state.update_sar_display_size(new_width, new_height) - # Trigger a SAR image update to reflect the new size - self._trigger_sar_update() # Calls ImagePipeline if not in test mode + # Update the display size in AppState + self.state.update_sar_display_size(new_width, new_height) + # Trigger a SAR image update to reflect the new size + self._trigger_sar_update() # Calls ImagePipeline if not in test mode except (ValueError, IndexError, TypeError) as e: - # Handle errors parsing the combobox value - logging.warning(f"{log_prefix} Invalid SAR size format: '{selected_size_str}'. Error: {e}. Resetting UI.") - # Reset UI to current state value as fallback - try: - current_factor = config.SAR_WIDTH // self.state.sar_display_width if self.state.sar_display_width > 0 else 1 - current_size_str = f"1:{current_factor}" - if hasattr(self.control_panel, 'sar_size_combo'): - self.control_panel.sar_size_combo.set(current_size_str) - except Exception as reset_e: - logging.warning(f"{log_prefix} Failed to reset SAR size combobox UI after error: {reset_e}") + # Handle errors parsing the combobox value + logging.warning( + f"{log_prefix} Invalid SAR size format: '{selected_size_str}'. Error: {e}. Resetting UI." + ) + # Reset UI to current state value as fallback + try: + current_factor = ( + config.SAR_WIDTH // self.state.sar_display_width + if self.state.sar_display_width > 0 + else 1 + ) + current_size_str = f"1:{current_factor}" + if hasattr(self.control_panel, "sar_size_combo"): + self.control_panel.sar_size_combo.set(current_size_str) + except Exception as reset_e: + logging.warning( + f"{log_prefix} Failed to reset SAR size combobox UI after error: {reset_e}" + ) except Exception as e: - # Log other unexpected errors - logging.exception(f"{log_prefix} Error processing SAR size update: {e}") + # Log other unexpected errors + logging.exception(f"{log_prefix} Error processing SAR size update: {e}") - def update_contrast(self, value_str: str): # UI Callback + def update_contrast(self, value_str: str): # UI Callback """Callback for SAR contrast slider. Updates state, LUT, and triggers display update.""" log_prefix = "[App CB SAR Contrast]" - if self.state.shutting_down: return # Ignore if shutting down + if self.state.shutting_down: + return # Ignore if shutting down try: # Convert slider value string to float contrast = float(value_str) @@ -462,18 +557,21 @@ class App: # Recalculate the SAR LUT based on the new contrast self.update_brightness_contrast_lut() # Trigger a display update to show the effect - self._trigger_sar_update() # Calls ImagePipeline if not in test mode + self._trigger_sar_update() # Calls ImagePipeline if not in test mode except ValueError: # Log error if slider value is not a valid float - logging.warning(f"{log_prefix} Invalid contrast value received from slider: {value_str}") + logging.warning( + f"{log_prefix} Invalid contrast value received from slider: {value_str}" + ) except Exception as e: # Log other unexpected errors logging.exception(f"{log_prefix} Error updating contrast: {e}") - def update_brightness(self, value_str: str): # UI Callback + def update_brightness(self, value_str: str): # UI Callback """Callback for SAR brightness slider. Updates state, LUT, and triggers display update.""" log_prefix = "[App CB SAR Brightness]" - if self.state.shutting_down: return # Ignore if shutting down + if self.state.shutting_down: + return # Ignore if shutting down try: # Convert slider value string to integer (can be float then int) brightness = int(float(value_str)) @@ -482,18 +580,21 @@ class App: # Recalculate the SAR LUT based on the new brightness self.update_brightness_contrast_lut() # Trigger a display update to show the effect - self._trigger_sar_update() # Calls ImagePipeline if not in test mode + self._trigger_sar_update() # Calls ImagePipeline if not in test mode except ValueError: # Log error if slider value is not a valid number - logging.warning(f"{log_prefix} Invalid brightness value received from slider: {value_str}") + logging.warning( + f"{log_prefix} Invalid brightness value received from slider: {value_str}" + ) except Exception as e: # Log other unexpected errors logging.exception(f"{log_prefix} Error updating brightness: {e}") - def update_sar_palette(self, event=None): # UI Callback + def update_sar_palette(self, event=None): # UI Callback """Callback for SAR palette combobox. Updates state and triggers display update.""" log_prefix = "[App CB SAR Palette]" - if self.state.shutting_down: return # Ignore if shutting down + if self.state.shutting_down: + return # Ignore if shutting down try: # Get the selected palette name from the combobox palette = self.control_panel.palette_combo.get() @@ -504,62 +605,90 @@ class App: # Update the palette name in AppState self.state.update_sar_parameters(palette=palette) # Trigger a display update to apply the new palette - self._trigger_sar_update() # Calls ImagePipeline if not in test mode + self._trigger_sar_update() # Calls ImagePipeline if not in test mode else: # Log warning and reset UI if palette is unknown - logging.warning(f"{log_prefix} Unknown palette selected: '{palette}'. Ignoring change.") + logging.warning( + f"{log_prefix} Unknown palette selected: '{palette}'. Ignoring change." + ) # Reset combobox to the current value stored in state self.control_panel.palette_combo.set(self.state.sar_palette) except Exception as e: # Log other unexpected errors logging.exception(f"{log_prefix} Error updating SAR palette: {e}") - def update_mfd_category_intensity(self, category_name: str, intensity_value: int): # UI Callback + def update_mfd_category_intensity( + self, category_name: str, intensity_value: int + ): # UI Callback """Callback for MFD category intensity slider. Updates state, LUT, and triggers display.""" log_prefix = "[App CB MFD Param Intensity]" - if self.state.shutting_down: return # Ignore if shutting down + if self.state.shutting_down: + return # Ignore if shutting down - logging.debug(f"{log_prefix} Category='{category_name}', Intensity={intensity_value}") + logging.debug( + f"{log_prefix} Category='{category_name}', Intensity={intensity_value}" + ) try: # Ensure intensity is within the valid range 0-255 - intensity = np.clip(intensity_value, 0, 255) # Value is already int fromIntVar + intensity = np.clip( + intensity_value, 0, 255 + ) # Value is already int fromIntVar # Check if category exists in state before updating if category_name in self.state.mfd_params["categories"]: # Update the intensity for the specific category in AppState - self.state.mfd_params["categories"][category_name]["intensity"] = intensity - logging.info(f"{log_prefix} Intensity for '{category_name}' set to {intensity} in AppState.") + self.state.mfd_params["categories"][category_name][ + "intensity" + ] = intensity + logging.info( + f"{log_prefix} Intensity for '{category_name}' set to {intensity} in AppState." + ) # Recalculate the MFD LUT to reflect the change self.update_mfd_lut() # Trigger an MFD display update - self._trigger_mfd_update() # Calls ImagePipeline if not in test mode + self._trigger_mfd_update() # Calls ImagePipeline if not in test mode else: # Log warning if the category name is not found - logging.warning(f"{log_prefix} Unknown MFD category received: '{category_name}'") + logging.warning( + f"{log_prefix} Unknown MFD category received: '{category_name}'" + ) except KeyError as ke: - logging.error(f"{log_prefix} KeyError accessing MFD params for '{category_name}': {ke}") + logging.error( + f"{log_prefix} KeyError accessing MFD params for '{category_name}': {ke}" + ) except Exception as e: # Log other unexpected errors - logging.exception(f"{log_prefix} Error updating MFD intensity for '{category_name}': {e}") + logging.exception( + f"{log_prefix} Error updating MFD intensity for '{category_name}': {e}" + ) - def choose_mfd_category_color(self, category_name: str): # UI Callback + def choose_mfd_category_color(self, category_name: str): # UI Callback """Callback for MFD category color button. Opens chooser, updates state, LUT, UI, and triggers display.""" log_prefix = "[App CB MFD Param Color]" - if self.state.shutting_down: return # Ignore if shutting down + if self.state.shutting_down: + return # Ignore if shutting down - logging.debug(f"{log_prefix} Color chooser requested for Category='{category_name}'") + logging.debug( + f"{log_prefix} Color chooser requested for Category='{category_name}'" + ) # Validate category name if category_name not in self.state.mfd_params["categories"]: - logging.warning(f"{log_prefix} Cannot choose color for unknown MFD category: '{category_name}'") - return + logging.warning( + f"{log_prefix} Cannot choose color for unknown MFD category: '{category_name}'" + ) + return try: # Get the current color (BGR tuple) from AppState to set initial color initial_bgr = self.state.mfd_params["categories"][category_name]["color"] # Convert BGR to HEX string for the color chooser - initial_hex = f"#{initial_bgr[2]:02x}{initial_bgr[1]:02x}{initial_bgr[0]:02x}" - logging.debug(f"{log_prefix} Opening color chooser (initial BGR: {initial_bgr}, initial HEX: {initial_hex})") + initial_hex = ( + f"#{initial_bgr[2]:02x}{initial_bgr[1]:02x}{initial_bgr[0]:02x}" + ) + logging.debug( + f"{log_prefix} Opening color chooser (initial BGR: {initial_bgr}, initial HEX: {initial_hex})" + ) # Open the Tkinter color chooser dialog # Returns a tuple: ((R, G, B), "#RRGGBB") or (None, None) if cancelled @@ -572,9 +701,13 @@ class App: # Extract the chosen RGB tuple (float 0-255 or int 0-255 depending on Tk version) rgb = color_code[0] # Convert RGB tuple to integer BGR tuple, clipping values - new_bgr = tuple(np.clip(int(c),0,255) for c in (rgb[2],rgb[1],rgb[0])) # B=rgb[2], G=rgb[1], R=rgb[0] + new_bgr = tuple( + np.clip(int(c), 0, 255) for c in (rgb[2], rgb[1], rgb[0]) + ) # B=rgb[2], G=rgb[1], R=rgb[0] - logging.info(f"{log_prefix} New color chosen for '{category_name}': RGB={rgb}, BGR={new_bgr}") + logging.info( + f"{log_prefix} New color chosen for '{category_name}': RGB={rgb}, BGR={new_bgr}" + ) # Update the color in AppState self.state.mfd_params["categories"][category_name]["color"] = new_bgr @@ -582,44 +715,63 @@ class App: self.update_mfd_lut() # Update the color preview label in the UI (schedule on main thread) if self.root and self.root.winfo_exists(): - # Ensure control panel and method exist before scheduling - if hasattr(self.control_panel, 'update_mfd_color_display'): - self.root.after_idle(self.control_panel.update_mfd_color_display, category_name, new_bgr) - else: - logging.warning(f"{log_prefix} control_panel.update_mfd_color_display method not found.") + # Ensure control panel and method exist before scheduling + if hasattr(self.control_panel, "update_mfd_color_display"): + self.root.after_idle( + self.control_panel.update_mfd_color_display, + category_name, + new_bgr, + ) + else: + logging.warning( + f"{log_prefix} control_panel.update_mfd_color_display method not found." + ) # Trigger an MFD display update - self._trigger_mfd_update() # Calls ImagePipeline if not in test mode + self._trigger_mfd_update() # Calls ImagePipeline if not in test mode else: - # Log if the user cancelled the color chooser - logging.debug(f"{log_prefix} Color selection cancelled by user.") + # Log if the user cancelled the color chooser + logging.debug(f"{log_prefix} Color selection cancelled by user.") except KeyError as ke: - logging.error(f"{log_prefix} KeyError accessing MFD params for '{category_name}': {ke}") + logging.error( + f"{log_prefix} KeyError accessing MFD params for '{category_name}': {ke}" + ) except Exception as e: - # Log other unexpected errors during color selection process - logging.exception(f"{log_prefix} Error during color selection for '{category_name}': {e}") + # Log other unexpected errors during color selection process + logging.exception( + f"{log_prefix} Error during color selection for '{category_name}': {e}" + ) - def update_mfd_raw_map_intensity(self, intensity_value: int): # UI Callback + def update_mfd_raw_map_intensity(self, intensity_value: int): # UI Callback """Callback for Raw Map intensity slider. Updates state, LUT, and triggers display.""" log_prefix = "[App CB MFD Param RawMap]" - if self.state.shutting_down: return # Ignore if shutting down + if self.state.shutting_down: + return # Ignore if shutting down - logging.debug(f"{log_prefix} Raw Map intensity slider changed -> {intensity_value}") + logging.debug( + f"{log_prefix} Raw Map intensity slider changed -> {intensity_value}" + ) try: # Ensure intensity is within the valid range 0-255 - intensity = np.clip(intensity_value, 0, 255) # Value is already int fromIntVar + intensity = np.clip( + intensity_value, 0, 255 + ) # Value is already int fromIntVar # Update the raw map intensity value in AppState self.state.mfd_params["raw_map_intensity"] = intensity - logging.info(f"{log_prefix} Raw Map intensity set to {intensity} in AppState.") + logging.info( + f"{log_prefix} Raw Map intensity set to {intensity} in AppState." + ) # Recalculate the MFD LUT self.update_mfd_lut() # Trigger an MFD display update - self._trigger_mfd_update() # Calls ImagePipeline if not in test mode + self._trigger_mfd_update() # Calls ImagePipeline if not in test mode except KeyError as ke: - logging.error(f"{log_prefix} KeyError accessing MFD params for raw_map_intensity: {ke}") + logging.error( + f"{log_prefix} KeyError accessing MFD params for raw_map_intensity: {ke}" + ) except Exception as e: - # Log other unexpected errors - logging.exception(f"{log_prefix} Error updating raw map intensity: {e}") + # Log other unexpected errors + logging.exception(f"{log_prefix} Error updating raw map intensity: {e}") # --- Initialization --- def __init__(self, root: tk.Tk): @@ -643,7 +795,9 @@ class App: # --- Data Queues --- self.sar_queue: queue.Queue = queue.Queue(maxsize=config.DEFAULT_SAR_QUEUE) self.mouse_queue: queue.Queue = queue.Queue(maxsize=config.DEFAULT_MOUSE_QUEUE) - self.tkinter_queue: queue.Queue = queue.Queue(maxsize=config.DEFAULT_TKINTER_QUEUE) + self.tkinter_queue: queue.Queue = queue.Queue( + maxsize=config.DEFAULT_TKINTER_QUEUE + ) self.mfd_queue: queue.Queue = queue.Queue(maxsize=config.DEFAULT_MFD_QUEUE) logging.debug(f"{log_prefix} Data queues initialized.") @@ -652,7 +806,9 @@ class App: # --- Calculate Initial Window Positions and Sizes --- # Calculate SAR display size first, as it might depend on map state - initial_sar_w, initial_sar_h = self._calculate_initial_sar_size(desired_factor_if_map=5) # Updates state if map active + initial_sar_w, initial_sar_h = self._calculate_initial_sar_size( + desired_factor_if_map=5 + ) # Updates state if map active # Calculate Tkinter and MFD positions self.tkinter_x, self.tkinter_y = self._calculate_tkinter_position(screen_h) self.mfd_x, self.mfd_y = self._calculate_mfd_position() @@ -660,10 +816,10 @@ class App: self.sar_x, self.sar_y = self._calculate_sar_position(screen_w, initial_sar_w) # Calculate potential map position (used if manager is created) map_x, map_y = self._calculate_map_position( - screen_w, - initial_sar_w, - # Pass the max map size used in MapDisplayWindow - max_map_width=MapDisplayWindow.MAX_DISPLAY_WIDTH + screen_w, + initial_sar_w, + # Pass the max map size used in MapDisplayWindow + max_map_width=MapDisplayWindow.MAX_DISPLAY_WIDTH, ) # Set Tkinter window position @@ -676,7 +832,7 @@ class App: # --- Initialize Sub-systems --- # 1. UI Components (need 'self' for callbacks) self.statusbar = StatusBar(self.root) - self.control_panel = ControlPanel(self.root, self) # Pass App instance + self.control_panel = ControlPanel(self.root, self) # Pass App instance logging.debug(f"{log_prefix} UI components created.") # 2. LUTs (read initial state, store back in state) @@ -686,30 +842,32 @@ class App: # 3. Display Manager (handles MFD/SAR OpenCV windows) self.display_manager = DisplayManager( - app=self, # Pass self for AppState access + app=self, # Pass self for AppState access sar_queue=self.sar_queue, mouse_queue=self.mouse_queue, sar_x=self.sar_x, sar_y=self.sar_y, mfd_x=self.mfd_x, mfd_y=self.mfd_y, - initial_sar_width=self.state.sar_display_width, # Use current state (potentially map-adjusted) - initial_sar_height=self.state.sar_display_height # Use current state + initial_sar_width=self.state.sar_display_width, # Use current state (potentially map-adjusted) + initial_sar_height=self.state.sar_display_height, # Use current state ) logging.debug(f"{log_prefix} DisplayManager created.") # Initialize display windows immediately (shows placeholders) try: self.display_manager.initialize_display_windows() except Exception as e: - self.set_status("Error: Display Init Failed") # Use self.set_status - logging.critical(f"{log_prefix} Display window initialization failed: {e}", exc_info=True) + self.set_status("Error: Display Init Failed") # Use self.set_status + logging.critical( + f"{log_prefix} Display window initialization failed: {e}", exc_info=True + ) # 4. Image Processing Pipeline (handles normal mode image processing) self.image_pipeline = ImagePipeline( app_state=self.state, sar_queue=self.sar_queue, mfd_queue=self.mfd_queue, - app=self, # Pass self for put_queue context + app=self, # Pass self for put_queue context ) logging.debug(f"{log_prefix} ImagePipeline created.") @@ -719,38 +877,48 @@ class App: root=self.root, sar_queue=self.sar_queue, mfd_queue=self.mfd_queue, - app=self, # Pass self for put_queue context + app=self, # Pass self for put_queue context ) logging.debug(f"{log_prefix} TestModeManager created.") # 6. Map Integration Manager (conditional initialization) self.map_integration_manager: Optional[MapIntegrationManager] = None if config.ENABLE_MAP_OVERLAY: - # Check if necessary libraries and manager class were loaded - if MAP_MODULES_LOADED and MapIntegrationManager is not None: - logging.info(f"{log_prefix} Map Overlay enabled. Initializing MapIntegrationManager...") - try: - # Create the manager instance - self.map_integration_manager = MapIntegrationManager( - app_state=self.state, - tkinter_queue=self.tkinter_queue, - app=self, - map_x=map_x, # Pass pre-calculated position - map_y=map_y - ) - logging.info(f"{log_prefix} MapIntegrationManager initialized successfully.") - except Exception as map_mgr_e: - # Log errors during map manager initialization - logging.exception(f"{log_prefix} Failed to initialize MapIntegrationManager:") - self.map_integration_manager = None # Ensure manager is None on error - self.set_status("Error: Map Init Failed") # Update status bar - else: - # Log error if map is enabled but components are missing - logging.error(f"{log_prefix} Map Overlay enabled but required modules/manager failed to load.") - self.set_status("Error: Map Modules Missing") + # Check if necessary libraries and manager class were loaded + if MAP_MODULES_LOADED and MapIntegrationManager is not None: + logging.info( + f"{log_prefix} Map Overlay enabled. Initializing MapIntegrationManager..." + ) + try: + # Create the manager instance + self.map_integration_manager = MapIntegrationManager( + app_state=self.state, + tkinter_queue=self.tkinter_queue, + app=self, + map_x=map_x, # Pass pre-calculated position + map_y=map_y, + ) + logging.info( + f"{log_prefix} MapIntegrationManager initialized successfully." + ) + except Exception as map_mgr_e: + # Log errors during map manager initialization + logging.exception( + f"{log_prefix} Failed to initialize MapIntegrationManager:" + ) + self.map_integration_manager = ( + None # Ensure manager is None on error + ) + self.set_status("Error: Map Init Failed") # Update status bar + else: + # Log error if map is enabled but components are missing + logging.error( + f"{log_prefix} Map Overlay enabled but required modules/manager failed to load." + ) + self.set_status("Error: Map Modules Missing") else: - # Log if map overlay is disabled in config - logging.debug(f"{log_prefix} Map Overlay is disabled in configuration.") + # Log if map overlay is disabled in config + logging.debug(f"{log_prefix} Map Overlay is disabled in configuration.") # 7. Set initial UI state labels AFTER all components potentially needed exist self._update_initial_ui_labels() @@ -758,19 +926,21 @@ class App: # 8. Network Setup self.local_ip: str = config.DEFAULT_SER_IP self.local_port: int = config.DEFAULT_SER_PORT - self.udp_socket: Optional[socket.socket] = None # Define attribute type - self.udp_receiver: Optional[UdpReceiver] = None # Define attribute type - self.udp_thread: Optional[threading.Thread] = None # Define attribute type + self.udp_socket: Optional[socket.socket] = None # Define attribute type + self.udp_receiver: Optional[UdpReceiver] = None # Define attribute type + self.udp_thread: Optional[threading.Thread] = None # Define attribute type # Setup receiver only if not using local images if not config.USE_LOCAL_IMAGES: - self._setup_network_receiver() # Calls set_status internally on success/failure + self._setup_network_receiver() # Calls set_status internally on success/failure else: - logging.info(f"{log_prefix} Network receiver disabled (USE_LOCAL_IMAGES=True).") + logging.info( + f"{log_prefix} Network receiver disabled (USE_LOCAL_IMAGES=True)." + ) # Status will be set later by image loader completion # 9. Initial Image Load Thread (runs in background) - self._start_initial_image_loader() # Calls _set_initial_display_from_loaded_data upon completion + self._start_initial_image_loader() # Calls _set_initial_display_from_loaded_data upon completion # 10. Start Queue Processors (run periodically on main thread) self.process_sar_queue() @@ -791,12 +961,11 @@ class App: logging.info(f"{log_prefix} Application initialization sequence complete.") - # --- Initialization Helper Methods --- def _get_screen_dimensions(self) -> Tuple[int, int]: """Gets primary screen dimensions using screeninfo, returning defaults on error.""" - log_prefix = "[App Init]" # Part of initialization + log_prefix = "[App Init]" # Part of initialization try: # Get list of monitors monitors = screeninfo.get_monitors() @@ -807,14 +976,20 @@ class App: screen = monitors[0] screen_w: int = screen.width screen_h: int = screen.height - logging.debug(f"{log_prefix} Detected Screen Dimensions: {screen_w}x{screen_h}") + logging.debug( + f"{log_prefix} Detected Screen Dimensions: {screen_w}x{screen_h}" + ) return screen_w, screen_h except Exception as e: # Log warning and return default values on any error - logging.warning(f"{log_prefix} Screen info error: {e}. Using default dimensions 1920x1080.") + logging.warning( + f"{log_prefix} Screen info error: {e}. Using default dimensions 1920x1080." + ) return 1920, 1080 - def _calculate_initial_sar_size(self, desired_factor_if_map: int = 4) -> Tuple[int, int]: + def _calculate_initial_sar_size( + self, desired_factor_if_map: int = 4 + ) -> Tuple[int, int]: """ Calculates initial SAR display size based on config and map state. Allows specifying the desired reduction factor if the map is active. @@ -833,18 +1008,20 @@ class App: map_enabled_and_loaded = config.ENABLE_MAP_OVERLAY and MAP_MODULES_LOADED if map_enabled_and_loaded: - # Use the specified factor for SAR size when map is active - forced_factor = max(1, desired_factor_if_map) # Ensure factor is at least 1 - initial_w = config.SAR_WIDTH // forced_factor - initial_h = config.SAR_HEIGHT // forced_factor - # Update the AppState with the map-enforced size - self.state.update_sar_display_size(initial_w, initial_h) - logging.info( - f"{log_prefix} Map overlay active, forcing SAR display size to 1:{forced_factor} " - f"({initial_w}x{initial_h})." - ) + # Use the specified factor for SAR size when map is active + forced_factor = max(1, desired_factor_if_map) # Ensure factor is at least 1 + initial_w = config.SAR_WIDTH // forced_factor + initial_h = config.SAR_HEIGHT // forced_factor + # Update the AppState with the map-enforced size + self.state.update_sar_display_size(initial_w, initial_h) + logging.info( + f"{log_prefix} Map overlay active, forcing SAR display size to 1:{forced_factor} " + f"({initial_w}x{initial_h})." + ) else: - logging.debug(f"{log_prefix} Using initial SAR display size from state: {initial_w}x{initial_h}.") + logging.debug( + f"{log_prefix} Using initial SAR display size from state: {initial_w}x{initial_h}." + ) return initial_w, initial_h @@ -863,11 +1040,13 @@ class App: def _calculate_mfd_position(self) -> Tuple[int, int]: """Calculates the initial X, Y position for the MFD display window.""" # Align X with Tkinter window, place near the top - x = self.tkinter_x # Use the already calculated tkinter_x + x = self.tkinter_x # Use the already calculated tkinter_x y = 10 return x, y - def _calculate_sar_position(self, screen_w: int, initial_sar_w: int) -> Tuple[int, int]: + def _calculate_sar_position( + self, screen_w: int, initial_sar_w: int + ) -> Tuple[int, int]: """ Calculates the initial X, Y position for the SAR display window. @@ -892,8 +1071,8 @@ class App: self, screen_w: int, current_sar_w: int, - max_map_width: int = 512 # Add max width argument - ) -> Tuple[int, int]: + max_map_width: int = 512, # Add max width argument + ) -> Tuple[int, int]: """ Calculates the initial X, Y position for the Map display window. @@ -918,7 +1097,9 @@ class App: def _setup_network_receiver(self): """Creates and starts the UDP socket and receiver thread.""" log_prefix = "[App Init Network]" - logging.info(f"{log_prefix} Attempting to start network receiver on {self.local_ip}:{self.local_port}") + logging.info( + f"{log_prefix} Attempting to start network receiver on {self.local_ip}:{self.local_port}" + ) # Create the UDP socket using the network utility function self.udp_socket = create_udp_socket(self.local_ip, self.local_port) @@ -927,10 +1108,10 @@ class App: # If socket created successfully, initialize the receiver try: self.udp_receiver = UdpReceiver( - app=self, # Pass App instance for state/config access + app=self, # Pass App instance for state/config access udp_socket=self.udp_socket, - set_new_sar_image_callback=self.handle_new_sar_data, # Pass SAR handler method - set_new_mfd_indices_image_callback=self.handle_new_mfd_data, # Pass MFD handler method + set_new_sar_image_callback=self.handle_new_sar_data, # Pass SAR handler method + set_new_mfd_indices_image_callback=self.handle_new_mfd_data, # Pass MFD handler method ) logging.info(f"{log_prefix} UdpReceiver instance created.") @@ -938,7 +1119,7 @@ class App: self.udp_thread = threading.Thread( target=self.udp_receiver.receive_udp_data, name="UDPReceiverThread", - daemon=True # Allows app to exit even if this thread hangs (though it checks shutdown flag) + daemon=True, # Allows app to exit even if this thread hangs (though it checks shutdown flag) ) self.udp_thread.start() logging.info(f"{log_prefix} UDP Receiver thread started.") @@ -947,12 +1128,15 @@ class App: except Exception as receiver_init_e: # Log critical error if UdpReceiver initialization fails - logging.critical(f"{log_prefix} Failed to initialize UdpReceiver: {receiver_init_e}", exc_info=True) - self.set_status("Error: Receiver Init Failed") - # Close the socket if receiver init failed - if self.udp_socket: - close_udp_socket(self.udp_socket) - self.udp_socket = None + logging.critical( + f"{log_prefix} Failed to initialize UdpReceiver: {receiver_init_e}", + exc_info=True, + ) + self.set_status("Error: Receiver Init Failed") + # Close the socket if receiver init failed + if self.udp_socket: + close_udp_socket(self.udp_socket) + self.udp_socket = None else: # Log error and set status if socket creation failed logging.error(f"{log_prefix} UDP socket creation failed.") @@ -969,92 +1153,110 @@ class App: logging.debug(f"{log_prefix} Starting initial image loading thread...") # Create and start the thread image_loading_thread = threading.Thread( - target=self.load_initial_images, # Target function in App + target=self.load_initial_images, # Target function in App name="ImageLoaderThread", - daemon=True # Allow app to exit even if this thread hangs + daemon=True, # Allow app to exit even if this thread hangs ) image_loading_thread.start() else: - logging.debug(f"{log_prefix} Skipping initial image loading (USE_LOCAL_IMAGES=False, ENABLE_TEST_MODE=False).") + logging.debug( + f"{log_prefix} Skipping initial image loading (USE_LOCAL_IMAGES=False, ENABLE_TEST_MODE=False)." + ) # If not loading images, set initial display immediately (placeholders or network wait) # Need to ensure status is set correctly if not loading anything. # Call the display setup directly, skipping the thread load. if self.root and self.root.winfo_exists(): - self.root.after_idle(self._set_initial_display_from_loaded_data) - + self.root.after_idle(self._set_initial_display_from_loaded_data) def _update_initial_ui_labels(self): """Sets the initial text for UI info labels based on default AppState.""" log_prefix = "[App Init]" logging.debug(f"{log_prefix} Setting initial UI info labels...") - if not hasattr(self, 'control_panel') or not self.control_panel: - logging.warning(f"{log_prefix} Control panel not ready, cannot set initial labels.") - return + if not hasattr(self, "control_panel") or not self.control_panel: + logging.warning( + f"{log_prefix} Control panel not ready, cannot set initial labels." + ) + return try: # --- Set SAR Center Label --- # Use default geo info from state for initial display default_geo = self.state.current_sar_geo_info - center_txt = "Image Ref: Lat=N/A, Lon=N/A" # Default text + center_txt = "Image Ref: Lat=N/A, Lon=N/A" # Default text # Only format if initial state claims validity (unlikely, but possible) - if default_geo and default_geo.get('valid', False): - try: - # Convert default radians back to degrees for display formatting - lat_s = decimal_to_dms(math.degrees(default_geo['lat']), True) - lon_s = decimal_to_dms(math.degrees(default_geo['lon']), False) - center_txt = f"Image Ref: Lat={lat_s}, Lon={lon_s}" - except KeyError as ke: - logging.error(f"{log_prefix} Missing key '{ke}' in initial default geo info.") - center_txt = "Image Ref: Data Error" - except Exception as format_err: - logging.error(f"{log_prefix} Error formatting initial geo label: {format_err}") - center_txt = "Image Ref: Format Error" + if default_geo and default_geo.get("valid", False): + try: + # Convert default radians back to degrees for display formatting + lat_s = decimal_to_dms(math.degrees(default_geo["lat"]), True) + lon_s = decimal_to_dms(math.degrees(default_geo["lon"]), False) + center_txt = f"Image Ref: Lat={lat_s}, Lon={lon_s}" + except KeyError as ke: + logging.error( + f"{log_prefix} Missing key '{ke}' in initial default geo info." + ) + center_txt = "Image Ref: Data Error" + except Exception as format_err: + logging.error( + f"{log_prefix} Error formatting initial geo label: {format_err}" + ) + center_txt = "Image Ref: Format Error" # Safely update the label widget, checking existence - if hasattr(self.control_panel, 'sar_center_label'): + if hasattr(self.control_panel, "sar_center_label"): self.control_panel.sar_center_label.config(text=center_txt) # --- Set SAR Orientation Label --- # Use the dedicated method on ControlPanel if available - if hasattr(self.control_panel, 'set_sar_orientation'): - self.control_panel.set_sar_orientation("N/A") # Initial value + if hasattr(self.control_panel, "set_sar_orientation"): + self.control_panel.set_sar_orientation("N/A") # Initial value # --- Set Mouse Coordinates Label --- # Use the dedicated method on ControlPanel if available - if hasattr(self.control_panel, 'set_mouse_coordinates'): - self.control_panel.set_mouse_coordinates("N/A", "N/A") # Initial value + if hasattr(self.control_panel, "set_mouse_coordinates"): + self.control_panel.set_mouse_coordinates("N/A", "N/A") # Initial value # --- Set Statistics Labels --- # Set initial text for drop/incomplete labels - initial_stats = self.state.get_statistics() # Get initial zeroed stats - drop_txt = (f"Drop(Q): S={initial_stats['dropped_sar_q']},M={initial_stats['dropped_mfd_q']}, " - f"Tk={initial_stats['dropped_tk_q']},Mo={initial_stats['dropped_mouse_q']}") - incmpl_txt = (f"Incmpl(RX): S={initial_stats['incomplete_sar_rx']}," - f"M={initial_stats['incomplete_mfd_rx']}") + initial_stats = self.state.get_statistics() # Get initial zeroed stats + drop_txt = ( + f"Drop(Q): S={initial_stats['dropped_sar_q']},M={initial_stats['dropped_mfd_q']}, " + f"Tk={initial_stats['dropped_tk_q']},Mo={initial_stats['dropped_mouse_q']}" + ) + incmpl_txt = ( + f"Incmpl(RX): S={initial_stats['incomplete_sar_rx']}," + f"M={initial_stats['incomplete_mfd_rx']}" + ) - if hasattr(self.control_panel, 'dropped_label'): + if hasattr(self.control_panel, "dropped_label"): self.control_panel.dropped_label.config(text=drop_txt) - if hasattr(self.control_panel, 'incomplete_label'): + if hasattr(self.control_panel, "incomplete_label"): self.control_panel.incomplete_label.config(text=incmpl_txt) logging.debug(f"{log_prefix} Initial UI state labels set.") except tk.TclError as e: - # Catch potential errors if UI elements are destroyed prematurely - logging.warning(f"{log_prefix} Error setting initial UI labels (TclError): {e}") + # Catch potential errors if UI elements are destroyed prematurely + logging.warning( + f"{log_prefix} Error setting initial UI labels (TclError): {e}" + ) except Exception as e: - # Catch other unexpected errors - logging.exception(f"{log_prefix} Unexpected error setting initial UI labels:") - + # Catch other unexpected errors + logging.exception( + f"{log_prefix} Unexpected error setting initial UI labels:" + ) # --- Network Data Handlers --- - def handle_new_sar_data(self, normalized_image_uint8: np.ndarray, geo_info_radians: Dict[str, Any]): + def handle_new_sar_data( + self, normalized_image_uint8: np.ndarray, geo_info_radians: Dict[str, Any] + ): """ Safely handles new SAR data received from the network receiver. Updates AppState and schedules main thread processing. This method is called by the UdpReceiver instance. """ - log_prefix = "[App CB SAR]" # Callback prefix - logging.debug(f"{log_prefix} Handling new SAR data (Shape: {normalized_image_uint8.shape}, Geo Valid: {geo_info_radians.get('valid', False)})...") + log_prefix = "[App CB SAR]" # Callback prefix + logging.debug( + f"{log_prefix} Handling new SAR data (Shape: {normalized_image_uint8.shape}, Geo Valid: {geo_info_radians.get('valid', False)})..." + ) # Check shutdown flag before processing if self.state.shutting_down: @@ -1069,11 +1271,15 @@ class App: # Schedule the main thread processing logic using after_idle # This ensures UI updates and processing happen safely on the GUI thread if self.root and self.root.winfo_exists(): - logging.debug(f"{log_prefix} Scheduling _process_sar_update_on_main_thread.") + logging.debug( + f"{log_prefix} Scheduling _process_sar_update_on_main_thread." + ) self.root.after_idle(self._process_sar_update_on_main_thread) else: # Log warning if root window is gone, cannot schedule update - logging.warning(f"{log_prefix} Cannot schedule SAR update: Root window destroyed or not available.") + logging.warning( + f"{log_prefix} Cannot schedule SAR update: Root window destroyed or not available." + ) def handle_new_mfd_data(self, image_indices: np.ndarray): """ @@ -1081,8 +1287,10 @@ class App: Updates AppState and schedules main thread processing. This method is called by the UdpReceiver instance. """ - log_prefix = "[App CB MFD]" # Callback prefix - logging.debug(f"{log_prefix} Handling new MFD index data (Shape: {image_indices.shape})...") + log_prefix = "[App CB MFD]" # Callback prefix + logging.debug( + f"{log_prefix} Handling new MFD index data (Shape: {image_indices.shape})..." + ) # Check shutdown flag if self.state.shutting_down: @@ -1096,10 +1304,14 @@ class App: # Schedule the main thread processing logic using after_idle if self.root and self.root.winfo_exists(): - logging.debug(f"{log_prefix} Scheduling _process_mfd_update_on_main_thread.") + logging.debug( + f"{log_prefix} Scheduling _process_mfd_update_on_main_thread." + ) self.root.after_idle(self._process_mfd_update_on_main_thread) else: - logging.warning(f"{log_prefix} Cannot schedule MFD update: Root window destroyed or not available.") + logging.warning( + f"{log_prefix} Cannot schedule MFD update: Root window destroyed or not available." + ) # --- Main Thread Processing Triggers --- def _process_sar_update_on_main_thread(self): @@ -1109,45 +1321,53 @@ class App: """ log_prefix = "[App MainThread SAR Update]" if self.state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected. Skipping.") - return + logging.debug(f"{log_prefix} Shutdown detected. Skipping.") + return logging.debug(f"{log_prefix} Processing scheduled SAR update...") # 1. Update UI Labels based on the latest state - self._update_sar_ui_labels() # Reads from self.state + self._update_sar_ui_labels() # Reads from self.state # 2. Trigger Image Processing Pipeline (for SAR display queue) # The pipeline checks the test_mode flag internally logging.debug(f"{log_prefix} Calling image_pipeline.process_sar_for_display...") try: - if hasattr(self, 'image_pipeline') and self.image_pipeline: - self.image_pipeline.process_sar_for_display() - else: - logging.error(f"{log_prefix} ImagePipeline not available.") + if hasattr(self, "image_pipeline") and self.image_pipeline: + self.image_pipeline.process_sar_for_display() + else: + logging.error(f"{log_prefix} ImagePipeline not available.") except Exception as e: - logging.exception(f"{log_prefix} Error calling ImagePipeline for SAR:") + logging.exception(f"{log_prefix} Error calling ImagePipeline for SAR:") # 3. Trigger Map Update (if map manager exists and geo is valid) - geo_info = self.state.current_sar_geo_info # Get current info + geo_info = self.state.current_sar_geo_info # Get current info is_geo_valid = geo_info and geo_info.get("valid", False) # Check if map manager was initialized successfully - map_manager_active = hasattr(self, 'map_integration_manager') and self.map_integration_manager is not None + map_manager_active = ( + hasattr(self, "map_integration_manager") + and self.map_integration_manager is not None + ) if map_manager_active and geo_info and geo_info.get("valid", False): - logging.debug(f"{log_prefix} Calling map_integration_manager.update_map_overlay...") - try: - # Pass current normalized image and geo info from state to the manager - self.map_integration_manager.update_map_overlay( - self.state.current_sar_normalized, - geo_info - ) - except Exception as e: - logging.exception(f"{log_prefix} Error calling map manager update:") - elif config.ENABLE_MAP_OVERLAY: # Log reason for skipping only if map is supposed to be on - if not map_manager_active: - logging.debug(f"{log_prefix} Skipping map update: MapIntegrationManager not available.") - elif not geo_info or not geo_info.get("valid", False): - logging.debug(f"{log_prefix} Skipping map update: GeoInfo not valid.") - + logging.debug( + f"{log_prefix} Calling map_integration_manager.update_map_overlay..." + ) + try: + # Pass current normalized image and geo info from state to the manager + self.map_integration_manager.update_map_overlay( + self.state.current_sar_normalized, geo_info + ) + except Exception as e: + logging.exception(f"{log_prefix} Error calling map manager update:") + elif ( + config.ENABLE_MAP_OVERLAY + ): # Log reason for skipping only if map is supposed to be on + if not map_manager_active: + logging.debug( + f"{log_prefix} Skipping map update: MapIntegrationManager not available." + ) + elif not geo_info or not geo_info.get("valid", False): + logging.debug(f"{log_prefix} Skipping map update: GeoInfo not valid.") + if is_geo_valid and config.ENABLE_KML_GENERATION: kml_log_prefix = "[App KML]" logging.debug(f"{kml_log_prefix} KML generation enabled. Proceeding...") @@ -1162,30 +1382,44 @@ class App: kml_output_path = os.path.join(kml_dir, kml_filename) # Genera il KML - logging.debug(f"{kml_log_prefix} Calling generate_sar_kml for path: {kml_output_path}") - kml_success = generate_sar_kml(geo_info, kml_output_path) # Passa geo_info + logging.debug( + f"{kml_log_prefix} Calling generate_sar_kml for path: {kml_output_path}" + ) + kml_success = generate_sar_kml( + geo_info, kml_output_path + ) # Passa geo_info if kml_success: - logging.info(f"{kml_log_prefix} KML file generated successfully: {kml_output_path}") + logging.info( + f"{kml_log_prefix} KML file generated successfully: {kml_output_path}" + ) # Lancia Google Earth se richiesto if config.AUTO_LAUNCH_GOOGLE_EARTH: - logging.debug(f"{kml_log_prefix} Auto-launch Google Earth enabled. Calling launch function...") + logging.debug( + f"{kml_log_prefix} Auto-launch Google Earth enabled. Calling launch function..." + ) launch_google_earth(kml_output_path) else: - logging.debug(f"{kml_log_prefix} Auto-launch Google Earth disabled.") + logging.debug( + f"{kml_log_prefix} Auto-launch Google Earth disabled." + ) else: logging.error(f"{kml_log_prefix} KML file generation failed.") except ImportError as ie: - # Logga se manca una libreria necessaria per KML - logging.error(f"{kml_log_prefix} Cannot generate KML due to missing library: {ie}") + # Logga se manca una libreria necessaria per KML + logging.error( + f"{kml_log_prefix} Cannot generate KML due to missing library: {ie}" + ) except Exception as e: - logging.exception(f"{kml_log_prefix} Error during KML generation/launch process:") + logging.exception( + f"{kml_log_prefix} Error during KML generation/launch process:" + ) elif is_geo_valid and not config.ENABLE_KML_GENERATION: - logging.debug(f"{log_prefix} KML generation disabled in config.") + logging.debug(f"{log_prefix} KML generation disabled in config.") # 4. Update FPS Statistics for SAR - self._update_fps_stats("sar") # Updates self.state counters + self._update_fps_stats("sar") # Updates self.state counters logging.debug(f"{log_prefix} Finished processing SAR update.") @@ -1196,139 +1430,159 @@ class App: """ log_prefix = "[App MainThread MFD Update]" if self.state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected. Skipping.") - return + logging.debug(f"{log_prefix} Shutdown detected. Skipping.") + return logging.debug(f"{log_prefix} Processing scheduled MFD update...") # 1. Trigger Image Processing Pipeline for MFD display # The pipeline checks the test_mode flag internally logging.debug(f"{log_prefix} Calling image_pipeline.process_mfd_for_display...") try: - if hasattr(self, 'image_pipeline') and self.image_pipeline: - self.image_pipeline.process_mfd_for_display() - else: - logging.error(f"{log_prefix} ImagePipeline not available.") + if hasattr(self, "image_pipeline") and self.image_pipeline: + self.image_pipeline.process_mfd_for_display() + else: + logging.error(f"{log_prefix} ImagePipeline not available.") except Exception as e: - logging.exception(f"{log_prefix} Error calling ImagePipeline for MFD:") + logging.exception(f"{log_prefix} Error calling ImagePipeline for MFD:") # 2. Update FPS Statistics for MFD - self._update_fps_stats("mfd") # Updates self.state counters + self._update_fps_stats("mfd") # Updates self.state counters logging.debug(f"{log_prefix} Finished processing MFD update.") def _update_sar_ui_labels(self): - """Helper method to update SAR related UI labels from AppState (runs in main thread).""" - log_prefix = "[App MainThread SAR Update]" # Part of the main thread update cycle - # Check if control panel exists and is valid - if not hasattr(self, 'control_panel') or not self.control_panel or not self.control_panel.winfo_exists(): - # logging.debug(f"{log_prefix} Control panel not available for UI label update.") # Can be noisy - return + """Helper method to update SAR related UI labels from AppState (runs in main thread).""" + log_prefix = ( + "[App MainThread SAR Update]" # Part of the main thread update cycle + ) + # Check if control panel exists and is valid + if ( + not hasattr(self, "control_panel") + or not self.control_panel + or not self.control_panel.winfo_exists() + ): + # logging.debug(f"{log_prefix} Control panel not available for UI label update.") # Can be noisy + return - geo_info = self.state.current_sar_geo_info - center_txt = "Image Ref: N/A" # Default text - orient_txt = "N/A" # Default text - is_valid_geo = geo_info and geo_info.get("valid", False) + geo_info = self.state.current_sar_geo_info + center_txt = "Image Ref: N/A" # Default text + orient_txt = "N/A" # Default text + is_valid_geo = geo_info and geo_info.get("valid", False) - if is_valid_geo: - try: - # Convert radians back to degrees for display formatting - lat_d = math.degrees(geo_info["lat"]) - lon_d = math.degrees(geo_info["lon"]) - orient_d = math.degrees(geo_info["orientation"]) - # Format using the utility function - lat_s = decimal_to_dms(lat_d, is_latitude=True) - lon_s = decimal_to_dms(lon_d, is_latitude=False) - # Format orientation string - orient_txt = f"{orient_d:.2f}°" # Show degrees with 2 decimal places - # Format center string - center_txt = f"Image Ref: Lat={lat_s}, Lon={lon_s}" - - scale_x = geo_info.get('scale_x', 0.0) - scale_y = geo_info.get('scale_y', 0.0) - width_px = geo_info.get('width_px', 0) - height_px = geo_info.get('height_px', 0) + if is_valid_geo: + try: + # Convert radians back to degrees for display formatting + lat_d = math.degrees(geo_info["lat"]) + lon_d = math.degrees(geo_info["lon"]) + orient_d = math.degrees(geo_info["orientation"]) + # Format using the utility function + lat_s = decimal_to_dms(lat_d, is_latitude=True) + lon_s = decimal_to_dms(lon_d, is_latitude=False) + # Format orientation string + orient_txt = f"{orient_d:.2f}°" # Show degrees with 2 decimal places + # Format center string + center_txt = f"Image Ref: Lat={lat_s}, Lon={lon_s}" - if scale_x > 0 and width_px > 0 and scale_y > 0 and height_px > 0: - width_km = (scale_x * width_px) / 1000.0 - height_km = (scale_y * height_px) / 1000.0 - # Formatta la stringa (es. W: 10.5 km, H: 9.8 km) - size_txt = f"W: {width_km:.1f} km, H: {height_km:.1f} km" - else: - logging.warning(f"{log_prefix} Cannot calculate SAR size in km due to invalid scale/dimensions in GeoInfo.") - size_txt = "Invalid Scale/Dims" - - except KeyError as ke: - # Log error if expected keys are missing in geo_info - logging.error(f"{log_prefix} Missing key '{ke}' in geo_info for UI update.") - center_txt = "Ref: Data Error" - orient_txt = "Data Error" - is_valid_geo = False # Mark as invalid if data is incomplete - except Exception as e: - # Log other formatting errors - logging.error(f"{log_prefix} Error formatting geo info for UI: {e}") - center_txt = "Ref: Format Error" - orient_txt = "Format Error" - is_valid_geo = False # Mark as invalid on formatting error + scale_x = geo_info.get("scale_x", 0.0) + scale_y = geo_info.get("scale_y", 0.0) + width_px = geo_info.get("width_px", 0) + height_px = geo_info.get("height_px", 0) - # --- Safely update UI elements --- - try: - # Update Center Label - if hasattr(self.control_panel, 'sar_center_label'): - self.control_panel.sar_center_label.config(text=center_txt) - # Update Orientation Label (using ControlPanel method) - if hasattr(self.control_panel, 'set_sar_orientation'): - self.control_panel.set_sar_orientation(orient_txt) - if hasattr(self.control_panel, 'set_sar_size_km'): - self.control_panel.set_sar_size_km(size_txt) - # Reset mouse coordinates display if geo becomes invalid - if not is_valid_geo and hasattr(self.control_panel, 'set_mouse_coordinates'): - self.control_panel.set_mouse_coordinates("N/A", "N/A") - # Log success at debug level - # logging.debug(f"{log_prefix} SAR UI labels updated.") # Can be noisy + if scale_x > 0 and width_px > 0 and scale_y > 0 and height_px > 0: + width_km = (scale_x * width_px) / 1000.0 + height_km = (scale_y * height_px) / 1000.0 + # Formatta la stringa (es. W: 10.5 km, H: 9.8 km) + size_txt = f"W: {width_km:.1f} km, H: {height_km:.1f} km" + else: + logging.warning( + f"{log_prefix} Cannot calculate SAR size in km due to invalid scale/dimensions in GeoInfo." + ) + size_txt = "Invalid Scale/Dims" - except tk.TclError as ui_err: - # Catch errors if UI widgets are destroyed prematurely (e.g., during shutdown) - if not self.state.shutting_down: - logging.warning(f"{log_prefix} Error updating SAR UI labels (TclError): {ui_err}") - except Exception as gen_err: - # Catch other unexpected errors during UI update - logging.exception(f"{log_prefix} Unexpected error updating SAR UI labels:") + except KeyError as ke: + # Log error if expected keys are missing in geo_info + logging.error( + f"{log_prefix} Missing key '{ke}' in geo_info for UI update." + ) + center_txt = "Ref: Data Error" + orient_txt = "Data Error" + is_valid_geo = False # Mark as invalid if data is incomplete + except Exception as e: + # Log other formatting errors + logging.error(f"{log_prefix} Error formatting geo info for UI: {e}") + center_txt = "Ref: Format Error" + orient_txt = "Format Error" + is_valid_geo = False # Mark as invalid on formatting error + + # --- Safely update UI elements --- + try: + # Update Center Label + if hasattr(self.control_panel, "sar_center_label"): + self.control_panel.sar_center_label.config(text=center_txt) + # Update Orientation Label (using ControlPanel method) + if hasattr(self.control_panel, "set_sar_orientation"): + self.control_panel.set_sar_orientation(orient_txt) + if hasattr(self.control_panel, "set_sar_size_km"): + self.control_panel.set_sar_size_km(size_txt) + # Reset mouse coordinates display if geo becomes invalid + if not is_valid_geo and hasattr( + self.control_panel, "set_mouse_coordinates" + ): + self.control_panel.set_mouse_coordinates("N/A", "N/A") + # Log success at debug level + # logging.debug(f"{log_prefix} SAR UI labels updated.") # Can be noisy + + except tk.TclError as ui_err: + # Catch errors if UI widgets are destroyed prematurely (e.g., during shutdown) + if not self.state.shutting_down: + logging.warning( + f"{log_prefix} Error updating SAR UI labels (TclError): {ui_err}" + ) + except Exception as gen_err: + # Catch other unexpected errors during UI update + logging.exception(f"{log_prefix} Unexpected error updating SAR UI labels:") def _update_fps_stats(self, img_type: str): - """Helper function to update FPS counters in AppState based on frame processing.""" - now = time.time() # Get current time - log_prefix = "[App FPS Update]" + """Helper function to update FPS counters in AppState based on frame processing.""" + now = time.time() # Get current time + log_prefix = "[App FPS Update]" - try: - if img_type == "sar": - # Increment SAR frame count in state - self.state.sar_frame_count += 1 - # Check if enough time has passed to calculate FPS - elapsed = now - self.state.sar_update_time - if elapsed >= config.LOG_UPDATE_INTERVAL: - # Calculate FPS - self.state.sar_fps = self.state.sar_frame_count / elapsed - # Reset timer and counter for next interval - self.state.sar_update_time = now - self.state.sar_frame_count = 0 - # Log calculated FPS at debug level - logging.debug(f"{log_prefix} SAR FPS calculated: {self.state.sar_fps:.2f}") - elif img_type == "mfd": - # Increment MFD frame count in state - self.state.mfd_frame_count += 1 - # Check if enough time has passed - elapsed = now - self.state.mfd_start_time - if elapsed >= config.LOG_UPDATE_INTERVAL: - # Calculate FPS - self.state.mfd_fps = self.state.mfd_frame_count / elapsed - # Reset timer and counter - self.state.mfd_start_time = now - self.state.mfd_frame_count = 0 - # Log calculated FPS at debug level - logging.debug(f"{log_prefix} MFD FPS calculated: {self.state.mfd_fps:.2f}") - except Exception as e: - # Prevent FPS calculation errors from stopping the application - logging.warning(f"{log_prefix} Error updating FPS stats for '{img_type}': {e}") + try: + if img_type == "sar": + # Increment SAR frame count in state + self.state.sar_frame_count += 1 + # Check if enough time has passed to calculate FPS + elapsed = now - self.state.sar_update_time + if elapsed >= config.LOG_UPDATE_INTERVAL: + # Calculate FPS + self.state.sar_fps = self.state.sar_frame_count / elapsed + # Reset timer and counter for next interval + self.state.sar_update_time = now + self.state.sar_frame_count = 0 + # Log calculated FPS at debug level + logging.debug( + f"{log_prefix} SAR FPS calculated: {self.state.sar_fps:.2f}" + ) + elif img_type == "mfd": + # Increment MFD frame count in state + self.state.mfd_frame_count += 1 + # Check if enough time has passed + elapsed = now - self.state.mfd_start_time + if elapsed >= config.LOG_UPDATE_INTERVAL: + # Calculate FPS + self.state.mfd_fps = self.state.mfd_frame_count / elapsed + # Reset timer and counter + self.state.mfd_start_time = now + self.state.mfd_frame_count = 0 + # Log calculated FPS at debug level + logging.debug( + f"{log_prefix} MFD FPS calculated: {self.state.mfd_fps:.2f}" + ) + except Exception as e: + # Prevent FPS calculation errors from stopping the application + logging.warning( + f"{log_prefix} Error updating FPS stats for '{img_type}': {e}" + ) # --- Trigger Methods --- # These methods are typically called by UI callbacks after parameters change. @@ -1338,49 +1592,63 @@ class App: """Triggers a SAR image update processing via ImagePipeline if not in test mode.""" log_prefix = "[App Trigger SAR]" if self.state.shutting_down: - return # Ignore if shutting down + return # Ignore if shutting down # Check if test mode is active. Test mode updates are handled by TestModeManager's timer. if not self.state.test_mode_active: # If not in test mode, trigger the normal image processing pipeline. - logging.debug(f"{log_prefix} Triggering SAR update processing via ImagePipeline.") + logging.debug( + f"{log_prefix} Triggering SAR update processing via ImagePipeline." + ) try: # Ensure image_pipeline exists before calling - if hasattr(self, 'image_pipeline') and self.image_pipeline: + if hasattr(self, "image_pipeline") and self.image_pipeline: self.image_pipeline.process_sar_for_display() else: - logging.error(f"{log_prefix} 'image_pipeline' not found. Cannot trigger SAR update.") + logging.error( + f"{log_prefix} 'image_pipeline' not found. Cannot trigger SAR update." + ) except Exception as e: # Log exceptions during the pipeline call - logging.exception(f"{log_prefix} Error calling image_pipeline.process_sar_for_display:") + logging.exception( + f"{log_prefix} Error calling image_pipeline.process_sar_for_display:" + ) else: # Log that the trigger is skipped because test mode is active. - logging.debug(f"{log_prefix} SAR update trigger skipped (Test Mode active, handled by TestModeManager timer).") - + logging.debug( + f"{log_prefix} SAR update trigger skipped (Test Mode active, handled by TestModeManager timer)." + ) def _trigger_mfd_update(self): """Triggers an MFD image update processing via ImagePipeline if not in test mode.""" log_prefix = "[App Trigger MFD]" if self.state.shutting_down: - return # Ignore if shutting down + return # Ignore if shutting down # Check if test mode is active. Test mode updates are handled by TestModeManager's timer. if not self.state.test_mode_active: # If not in test mode, trigger the normal image processing pipeline. - logging.debug(f"{log_prefix} Triggering MFD update processing via ImagePipeline.") + logging.debug( + f"{log_prefix} Triggering MFD update processing via ImagePipeline." + ) try: - # Ensure image_pipeline exists before calling - if hasattr(self, 'image_pipeline') and self.image_pipeline: + # Ensure image_pipeline exists before calling + if hasattr(self, "image_pipeline") and self.image_pipeline: self.image_pipeline.process_mfd_for_display() else: - logging.error(f"{log_prefix} 'image_pipeline' not found. Cannot trigger MFD update.") + logging.error( + f"{log_prefix} 'image_pipeline' not found. Cannot trigger MFD update." + ) except Exception as e: # Log exceptions during the pipeline call - logging.exception(f"{log_prefix} Error calling image_pipeline.process_mfd_for_display:") + logging.exception( + f"{log_prefix} Error calling image_pipeline.process_mfd_for_display:" + ) else: # Log that the trigger is skipped because test mode is active. - logging.debug(f"{log_prefix} MFD update trigger skipped (Test Mode active, handled by TestModeManager timer).") - + logging.debug( + f"{log_prefix} MFD update trigger skipped (Test Mode active, handled by TestModeManager timer)." + ) # --- Periodic Update Scheduling --- def schedule_periodic_updates(self): @@ -1395,8 +1663,10 @@ class App: try: self.update_status() except Exception as e: - # Log error during update but continue scheduling - logging.error(f"{log_prefix} Error during periodic status update execution: {e}") + # Log error during update but continue scheduling + logging.error( + f"{log_prefix} Error during periodic status update execution: {e}" + ) # Calculate interval in milliseconds from config (seconds) # Ensure a minimum delay to prevent overly frequent updates @@ -1420,7 +1690,9 @@ class App: """ log_prefix = "[App Image Loader]" if self.state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected. Aborting image loading thread.") + logging.debug( + f"{log_prefix} Shutdown detected. Aborting image loading thread." + ) return logging.info(f"{log_prefix} Initial image loading thread started.") @@ -1432,23 +1704,28 @@ class App: # Ensure test images are generated if test mode is enabled initially # The manager handles the check if data already exists. if config.ENABLE_TEST_MODE or self.state.test_mode_active: - if hasattr(self, 'test_mode_manager') and self.test_mode_manager: - logging.debug(f"{log_prefix} Ensuring test images are generated via TestModeManager...") - self.test_mode_manager._ensure_test_images() # Call manager's internal method - else: - logging.error(f"{log_prefix} TestModeManager not available to generate test images.") - + if hasattr(self, "test_mode_manager") and self.test_mode_manager: + logging.debug( + f"{log_prefix} Ensuring test images are generated via TestModeManager..." + ) + self.test_mode_manager._ensure_test_images() # Call manager's internal method + else: + logging.error( + f"{log_prefix} TestModeManager not available to generate test images." + ) # Load local images if configured if config.USE_LOCAL_IMAGES: logging.debug(f"{log_prefix} Loading local MFD image...") - self._load_local_mfd_image() # Loads into self.state.local_mfd_image_data_indices + self._load_local_mfd_image() # Loads into self.state.local_mfd_image_data_indices logging.debug(f"{log_prefix} Loading local SAR image...") - self._load_local_sar_image() # Loads into self.state.local_sar_image_data_raw + self._load_local_sar_image() # Loads into self.state.local_sar_image_data_raw # Schedule the final display setup on the main thread after loading is complete if self.root and self.root.winfo_exists(): - logging.debug(f"{log_prefix} Scheduling _set_initial_display_from_loaded_data on main thread.") + logging.debug( + f"{log_prefix} Scheduling _set_initial_display_from_loaded_data on main thread." + ) self.root.after_idle(self._set_initial_display_from_loaded_data) except Exception as e: @@ -1465,18 +1742,24 @@ class App: """Loads local MFD image data (indices) into AppState.""" log_prefix = "[App Image Loader]" # Default to random indices if loading fails - default_indices = np.random.randint(0, 256, (config.MFD_HEIGHT, config.MFD_WIDTH), dtype=np.uint8) + default_indices = np.random.randint( + 0, 256, (config.MFD_HEIGHT, config.MFD_WIDTH), dtype=np.uint8 + ) loaded_indices = None try: mfd_path = config.MFD_IMAGE_PATH if os.path.exists(mfd_path): - logging.warning(f"{log_prefix} Local MFD loading from file is NYI. Using random data.") + logging.warning( + f"{log_prefix} Local MFD loading from file is NYI. Using random data." + ) # Placeholder for actual loading logic (e.g., np.load) # Example: loaded_indices = np.load(mfd_path).astype(np.uint8) - loaded_indices = default_indices # Use default for now + loaded_indices = default_indices # Use default for now logging.debug(f"{log_prefix} Using placeholder random MFD indices.") else: - logging.warning(f"{log_prefix} Local MFD image file not found: {mfd_path}. Using random data.") + logging.warning( + f"{log_prefix} Local MFD image file not found: {mfd_path}. Using random data." + ) loaded_indices = default_indices # Store the result (loaded or default) in AppState @@ -1491,19 +1774,27 @@ class App: """Loads local SAR image data (raw) into AppState.""" log_prefix = "[App Image Loader]" # Default to zeros if loading fails - default_raw_data = np.zeros((config.SAR_HEIGHT, config.SAR_WIDTH), dtype=config.SAR_DATA_TYPE) + default_raw_data = np.zeros( + (config.SAR_HEIGHT, config.SAR_WIDTH), dtype=config.SAR_DATA_TYPE + ) loaded_raw_data = None try: # Use the load_image utility from image_processing module # This handles file existence check, loading, and type conversion - loaded_raw_data = load_image(config.SAR_IMAGE_PATH, config.SAR_DATA_TYPE) # load_image logs internally + loaded_raw_data = load_image( + config.SAR_IMAGE_PATH, config.SAR_DATA_TYPE + ) # load_image logs internally if loaded_raw_data is None or loaded_raw_data.size == 0: # load_image returns placeholder on error, check if it's different from default - logging.error(f"{log_prefix} Failed to load local SAR raw data from {config.SAR_IMAGE_PATH}. Using zeros.") - loaded_raw_data = default_raw_data # Ensure zeros on failure + logging.error( + f"{log_prefix} Failed to load local SAR raw data from {config.SAR_IMAGE_PATH}. Using zeros." + ) + loaded_raw_data = default_raw_data # Ensure zeros on failure else: - logging.info(f"{log_prefix} Loaded local SAR raw data into AppState (shape: {loaded_raw_data.shape}).") + logging.info( + f"{log_prefix} Loaded local SAR raw data into AppState (shape: {loaded_raw_data.shape})." + ) # Store the result (loaded or default) in AppState self.state.local_sar_image_data_raw = loaded_raw_data @@ -1521,7 +1812,9 @@ class App: """ log_prefix = "[App Init Display]" if self.state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected. Skipping initial display setup.") + logging.debug( + f"{log_prefix} Shutdown detected. Skipping initial display setup." + ) return is_test = self.state.test_mode_active @@ -1530,53 +1823,72 @@ class App: # Determine initial display content based on mode if not is_test and is_local: # --- Local Image Mode --- - logging.info(f"{log_prefix} Setting initial display based on loaded local images.") + logging.info( + f"{log_prefix} Setting initial display based on loaded local images." + ) # Display MFD (if loaded) if self.state.local_mfd_image_data_indices is not None: # Update current state from the loaded local data - self.state.current_mfd_indices = self.state.local_mfd_image_data_indices.copy() + self.state.current_mfd_indices = ( + self.state.local_mfd_image_data_indices.copy() + ) # Process the restored state via the pipeline self.image_pipeline.process_mfd_for_display() else: - logging.warning(f"{log_prefix} Local MFD data not loaded. MFD display will be blank/stale.") + logging.warning( + f"{log_prefix} Local MFD data not loaded. MFD display will be blank/stale." + ) # Display SAR (if loaded) if self.state.local_sar_image_data_raw is not None: # This method handles normalization, state update, UI reset, and pipeline call self.set_initial_sar_image(self.state.local_sar_image_data_raw) else: - # Handle case where SAR load failed but local mode is active - logging.warning(f"{log_prefix} Local SAR data not loaded. Displaying black SAR image.") - # Ensure normalized state buffer exists and is black - if self.state.current_sar_normalized is None: - self.state.current_sar_normalized = np.zeros((config.SAR_HEIGHT, config.SAR_WIDTH), dtype=np.uint8) - self.state.current_sar_normalized.fill(0) - # Mark geo as invalid and reset UI - self.state.current_sar_geo_info["valid"] = False - self._reset_ui_geo_info() - # Process the black image via the pipeline - self.image_pipeline.process_sar_for_display() + # Handle case where SAR load failed but local mode is active + logging.warning( + f"{log_prefix} Local SAR data not loaded. Displaying black SAR image." + ) + # Ensure normalized state buffer exists and is black + if self.state.current_sar_normalized is None: + self.state.current_sar_normalized = np.zeros( + (config.SAR_HEIGHT, config.SAR_WIDTH), dtype=np.uint8 + ) + self.state.current_sar_normalized.fill(0) + # Mark geo as invalid and reset UI + self.state.current_sar_geo_info["valid"] = False + self._reset_ui_geo_info() + # Process the black image via the pipeline + self.image_pipeline.process_sar_for_display() elif is_test: - # --- Test Mode --- - logging.info(f"{log_prefix} Test mode active. Display handled by TestModeManager timers.") - # Activation of test mode (in update_image_mode) starts the timers. + # --- Test Mode --- + logging.info( + f"{log_prefix} Test mode active. Display handled by TestModeManager timers." + ) + # Activation of test mode (in update_image_mode) starts the timers. else: - # --- Network Mode --- - logging.info(f"{log_prefix} Network mode active. Displaying initial placeholders.") - # Show placeholder images (these bypass the pipeline for simplicity) - self._show_network_placeholders() + # --- Network Mode --- + logging.info( + f"{log_prefix} Network mode active. Displaying initial placeholders." + ) + # Show placeholder images (these bypass the pipeline for simplicity) + self._show_network_placeholders() # --- Set Final Initial Status --- # Check if the map manager is active and if its initial loading thread is still running - map_manager_active = hasattr(self, 'map_integration_manager') and self.map_integration_manager is not None + map_manager_active = ( + hasattr(self, "map_integration_manager") + and self.map_integration_manager is not None + ) map_thread_running = False if map_manager_active: - # Check existence of thread attribute before accessing is_alive - map_thread_attr = getattr(self.map_integration_manager, '_map_initial_display_thread', None) - if map_thread_attr and isinstance(map_thread_attr, threading.Thread): - map_thread_running = map_thread_attr.is_alive() + # Check existence of thread attribute before accessing is_alive + map_thread_attr = getattr( + self.map_integration_manager, "_map_initial_display_thread", None + ) + if map_thread_attr and isinstance(map_thread_attr, threading.Thread): + map_thread_running = map_thread_attr.is_alive() map_is_loading = map_manager_active and map_thread_running @@ -1584,14 +1896,28 @@ class App: # If map is loading, its completion callback will set the status. if not map_is_loading: # Determine status based on mode - final_status = "Ready (Test Mode)" if is_test else \ - ("Ready (Local Mode)" if is_local else \ - (f"Listening UDP {self.local_ip}:{self.local_port}" if self.udp_socket else "Error: No Socket")) + final_status = ( + "Ready (Test Mode)" + if is_test + else ( + "Ready (Local Mode)" + if is_local + else ( + f"Listening UDP {self.local_ip}:{self.local_port}" + if self.udp_socket + else "Error: No Socket" + ) + ) + ) self.set_status(final_status) - logging.debug(f"{log_prefix} Set final initial status (map not loading): '{final_status}'") + logging.debug( + f"{log_prefix} Set final initial status (map not loading): '{final_status}'" + ) else: - # Status 'Loading initial map...' was likely set by MapIntegrationManager init - logging.debug(f"{log_prefix} Initial map display is still loading. Status will be updated upon completion.") + # Status 'Loading initial map...' was likely set by MapIntegrationManager init + logging.debug( + f"{log_prefix} Initial map display is still loading. Status will be updated upon completion." + ) def set_initial_sar_image(self, raw_image_data: np.ndarray): """ @@ -1599,7 +1925,8 @@ class App: resets UI Geo info, and triggers display via the ImagePipeline. """ log_prefix = "[App Init SAR Image]" - if self.state.shutting_down: return # Ignore if shutting down + if self.state.shutting_down: + return # Ignore if shutting down logging.debug(f"{log_prefix} Processing initial raw SAR image...") normalized: Optional[np.ndarray] = None @@ -1607,13 +1934,17 @@ class App: if raw_image_data is not None and raw_image_data.size > 0: try: # Normalize the raw data to uint8 using the utility function - normalized = normalize_image(raw_image_data, target_type=np.uint8) # Logs internally + normalized = normalize_image( + raw_image_data, target_type=np.uint8 + ) # Logs internally if normalized is None: # Log error if normalization itself failed logging.error(f"{log_prefix} Normalization of raw SAR data failed.") except Exception as e: # Log any unexpected errors during normalization - logging.exception(f"{log_prefix} Error during initial SAR normalization:") + logging.exception( + f"{log_prefix} Error during initial SAR normalization:" + ) else: # Log error if raw data is invalid logging.error(f"{log_prefix} Provided raw SAR data is invalid or empty.") @@ -1621,38 +1952,49 @@ class App: # Update AppState with the normalized image (or a black image on failure) if normalized is not None: self.state.current_sar_normalized = normalized - logging.debug(f"{log_prefix} Stored normalized initial SAR image in AppState.") + logging.debug( + f"{log_prefix} Stored normalized initial SAR image in AppState." + ) else: # Ensure the state buffer exists before filling with zeros - logging.warning(f"{log_prefix} Using black image fallback for initial SAR display.") + logging.warning( + f"{log_prefix} Using black image fallback for initial SAR display." + ) if self.state.current_sar_normalized is None: - # Create buffer if it doesn't exist - self.state.current_sar_normalized = np.zeros((config.SAR_HEIGHT, config.SAR_WIDTH), dtype=np.uint8) + # Create buffer if it doesn't exist + self.state.current_sar_normalized = np.zeros( + (config.SAR_HEIGHT, config.SAR_WIDTH), dtype=np.uint8 + ) # Fill existing buffer with zeros self.state.current_sar_normalized.fill(0) # Mark geo info as invalid for locally loaded images and reset UI display self.state.current_sar_geo_info["valid"] = False - self._reset_ui_geo_info() # Schedule UI label reset + self._reset_ui_geo_info() # Schedule UI label reset # Trigger the image pipeline to process and display the normalized (or black) image - logging.debug(f"{log_prefix} Triggering display of initial SAR image via ImagePipeline.") - if hasattr(self, 'image_pipeline') and self.image_pipeline: - self.image_pipeline.process_sar_for_display() # Use the pipeline + logging.debug( + f"{log_prefix} Triggering display of initial SAR image via ImagePipeline." + ) + if hasattr(self, "image_pipeline") and self.image_pipeline: + self.image_pipeline.process_sar_for_display() # Use the pipeline else: - logging.error(f"{log_prefix} ImagePipeline not available to display initial SAR.") + logging.error( + f"{log_prefix} ImagePipeline not available to display initial SAR." + ) logging.info(f"{log_prefix} Initial SAR image processed and queued.") - # --- Mode Switching UI Actions --- # These handle UI/State changes NOT directly related to manager timers/data generation def activate_test_mode_ui_actions(self): """Handles UI and state changes needed when activating test mode.""" log_prefix = "[App Test Activate]" - logging.info(f"{log_prefix} Performing UI/State actions for Test Mode activation.") - self.set_status("Activating Test Mode...") # Initial status + logging.info( + f"{log_prefix} Performing UI/State actions for Test Mode activation." + ) + self.set_status("Activating Test Mode...") # Initial status # Reset geo info display in UI as test mode doesn't use real geo self._reset_ui_geo_info() @@ -1666,12 +2008,13 @@ class App: self.set_status("Ready (Test Mode)") logging.info(f"{log_prefix} Test Mode UI/State actions complete.") - def deactivate_test_mode_ui_actions(self): """Handles UI and state changes needed when deactivating test mode.""" log_prefix = "[App Test Deactivate]" - logging.info(f"{log_prefix} Performing UI/State actions for Test Mode deactivation -> Normal Mode.") - self.set_status("Activating Normal Mode...") # Initial status + logging.info( + f"{log_prefix} Performing UI/State actions for Test Mode deactivation -> Normal Mode." + ) + self.set_status("Activating Normal Mode...") # Initial status # Test timers are stopped by TestModeManager.deactivate() before this is called @@ -1686,15 +2029,21 @@ class App: # --- Restore display based on configuration (Local or Network) --- if config.USE_LOCAL_IMAGES: # --- Local Image Mode Restoration --- - logging.info(f"{log_prefix} Restoring display from local images stored in AppState.") + logging.info( + f"{log_prefix} Restoring display from local images stored in AppState." + ) # MFD restore if self.state.local_mfd_image_data_indices is not None: # Update current display state from the loaded local backup - self.state.current_mfd_indices = self.state.local_mfd_image_data_indices.copy() + self.state.current_mfd_indices = ( + self.state.local_mfd_image_data_indices.copy() + ) # Process the restored state via the pipeline self.image_pipeline.process_mfd_for_display() else: - logging.warning(f"{log_prefix} No local MFD data in AppState to restore.") + logging.warning( + f"{log_prefix} No local MFD data in AppState to restore." + ) # Optionally queue a black MFD image or clear display? For now, do nothing. # SAR restore @@ -1702,65 +2051,93 @@ class App: # This method handles processing, state update, UI reset, and pipeline call self.set_initial_sar_image(self.state.local_sar_image_data_raw) else: - # Handle case where SAR load failed but local mode is active - logging.warning(f"{log_prefix} No local SAR data in AppState to restore. Displaying black.") - # Ensure normalized state is black, mark geo invalid, reset UI, and process black image - if self.state.current_sar_normalized is None: - self.state.current_sar_normalized = np.zeros((config.SAR_HEIGHT, config.SAR_WIDTH), dtype=np.uint8) - self.state.current_sar_normalized.fill(0) - self.state.current_sar_geo_info["valid"] = False - self._reset_ui_geo_info() - self.image_pipeline.process_sar_for_display() + # Handle case where SAR load failed but local mode is active + logging.warning( + f"{log_prefix} No local SAR data in AppState to restore. Displaying black." + ) + # Ensure normalized state is black, mark geo invalid, reset UI, and process black image + if self.state.current_sar_normalized is None: + self.state.current_sar_normalized = np.zeros( + (config.SAR_HEIGHT, config.SAR_WIDTH), dtype=np.uint8 + ) + self.state.current_sar_normalized.fill(0) + self.state.current_sar_geo_info["valid"] = False + self._reset_ui_geo_info() + self.image_pipeline.process_sar_for_display() # Set status for local mode self.set_status("Ready (Local Mode)") else: # --- Network Mode Restoration --- - logging.info(f"{log_prefix} Switched to Network mode. Displaying placeholders.") + logging.info( + f"{log_prefix} Switched to Network mode. Displaying placeholders." + ) # Queue placeholder images (these bypass the pipeline) self._show_network_placeholders() # Determine status based on current socket state socket_ok = self.udp_socket is not None and self.udp_socket.fileno() != -1 - status = f"Listening UDP {self.local_ip}:{self.local_port}" if socket_ok else "Error: No UDP Socket" + status = ( + f"Listening UDP {self.local_ip}:{self.local_port}" + if socket_ok + else "Error: No UDP Socket" + ) self.set_status(status) # Geo info will be updated when valid data arrives via network handler. logging.info(f"{log_prefix} Normal Mode UI/State actions complete.") def _reset_ui_geo_info(self): - """Schedules UI reset for geo-related labels on the main thread.""" - log_prefix = "[App UI Reset]" - # Check if root window exists before scheduling - if self.root and self.root.winfo_exists(): - # Schedule updates using after_idle for thread safety - # Check if control_panel and specific methods/widgets exist before calling config/set - if hasattr(self.control_panel, 'set_sar_orientation'): - self.root.after_idle(lambda: self.control_panel.set_sar_orientation("N/A")) - if hasattr(self.control_panel, 'set_mouse_coordinates'): - self.root.after_idle(lambda: self.control_panel.set_mouse_coordinates("N/A", "N/A")) - if hasattr(self.control_panel, 'sar_center_label'): - self.root.after_idle(lambda: self.control_panel.sar_center_label.config(text="Image Ref: Lat=N/A, Lon=N/A")) - logging.debug(f"{log_prefix} Geo UI label reset scheduled.") - # else: # Don't log if root gone, expected during shutdown + """Schedules UI reset for geo-related labels on the main thread.""" + log_prefix = "[App UI Reset]" + # Check if root window exists before scheduling + if self.root and self.root.winfo_exists(): + # Schedule updates using after_idle for thread safety + # Check if control_panel and specific methods/widgets exist before calling config/set + if hasattr(self.control_panel, "set_sar_orientation"): + self.root.after_idle( + lambda: self.control_panel.set_sar_orientation("N/A") + ) + if hasattr(self.control_panel, "set_mouse_coordinates"): + self.root.after_idle( + lambda: self.control_panel.set_mouse_coordinates("N/A", "N/A") + ) + if hasattr(self.control_panel, "sar_center_label"): + self.root.after_idle( + lambda: self.control_panel.sar_center_label.config( + text="Image Ref: Lat=N/A, Lon=N/A" + ) + ) + logging.debug(f"{log_prefix} Geo UI label reset scheduled.") + # else: # Don't log if root gone, expected during shutdown def _revert_test_mode_ui(self): - """Tries to uncheck the test mode checkbox in the UI and resets the state flag.""" - log_prefix = "[App Mode Switch]" # Part of mode switching logic - logging.warning(f"{log_prefix} Reverting Test Mode UI and state due to activation failure.") - # Schedule UI update on main thread if possible - if self.root and self.root.winfo_exists() and hasattr(self.control_panel, "test_image_var"): - try: - # Use after_idle to ensure UI update happens when Tkinter is ready - self.root.after_idle(self.control_panel.test_image_var.set, 0) # Set checkbox to unchecked - logging.debug(f"{log_prefix} Scheduled UI checkbox uncheck.") - except Exception as e: - logging.warning(f"{log_prefix} Failed to schedule uncheck of test mode checkbox: {e}") + """Tries to uncheck the test mode checkbox in the UI and resets the state flag.""" + log_prefix = "[App Mode Switch]" # Part of mode switching logic + logging.warning( + f"{log_prefix} Reverting Test Mode UI and state due to activation failure." + ) + # Schedule UI update on main thread if possible + if ( + self.root + and self.root.winfo_exists() + and hasattr(self.control_panel, "test_image_var") + ): + try: + # Use after_idle to ensure UI update happens when Tkinter is ready + self.root.after_idle( + self.control_panel.test_image_var.set, 0 + ) # Set checkbox to unchecked + logging.debug(f"{log_prefix} Scheduled UI checkbox uncheck.") + except Exception as e: + logging.warning( + f"{log_prefix} Failed to schedule uncheck of test mode checkbox: {e}" + ) - # Correct state flag regardless of UI success - if hasattr(self, 'state'): - self.state.test_mode_active = False - logging.debug(f"{log_prefix} Test mode state flag reverted to False.") + # Correct state flag regardless of UI success + if hasattr(self, "state"): + self.state.test_mode_active = False + logging.debug(f"{log_prefix} Test mode state flag reverted to False.") def _show_network_placeholders(self): """Queues placeholder images for MFD and SAR displays when in Network mode without data.""" @@ -1768,14 +2145,24 @@ class App: logging.debug(f"{log_prefix} Queueing network placeholder images.") try: # Create MFD placeholder (dark gray) - ph_mfd = np.full((config.INITIAL_MFD_HEIGHT, config.INITIAL_MFD_WIDTH, 3), 30, dtype=np.uint8) + ph_mfd = np.full( + (config.INITIAL_MFD_HEIGHT, config.INITIAL_MFD_WIDTH, 3), + 30, + dtype=np.uint8, + ) # Create SAR placeholder (lighter gray) - Use current display size from state - ph_sar = np.full((self.state.sar_display_height, self.state.sar_display_width, 3), 60, dtype=np.uint8) + ph_sar = np.full( + (self.state.sar_display_height, self.state.sar_display_width, 3), + 60, + dtype=np.uint8, + ) # Queue the placeholders directly, bypassing the processing pipeline put_queue(self.mfd_queue, ph_mfd, "mfd", self) put_queue(self.sar_queue, ph_sar, "sar", self) except Exception as e: - logging.exception(f"{log_prefix} Error creating or queueing placeholder images:") + logging.exception( + f"{log_prefix} Error creating or queueing placeholder images:" + ) # --- Mouse Coordinate Handling --- def process_mouse_queue(self): @@ -1783,8 +2170,9 @@ class App: Processes raw mouse coords from queue, calculates geo coords (using AppState), and queues the result string tuple onto the Tkinter queue. Runs periodically. """ - log_prefix = "[App GeoCalc]" # Calculation prefix - if self.state.shutting_down: return # Stop processing on shutdown + log_prefix = "[App GeoCalc]" # Calculation prefix + if self.state.shutting_down: + return # Stop processing on shutdown raw_coords = None try: @@ -1793,141 +2181,197 @@ class App: # Mark task done *after* getting item successfully self.mouse_queue.task_done() except queue.Empty: - pass # Normal case, queue is empty + pass # Normal case, queue is empty except Exception as e: # Log error getting from queue logging.exception(f"{log_prefix} Error getting from mouse queue:") # Don't reschedule immediately on error? Or reschedule anyway? Reschedule for now. pass # else: # Process if coords were retrieved - if raw_coords is not None and isinstance(raw_coords, tuple) and len(raw_coords) == 2: + if ( + raw_coords is not None + and isinstance(raw_coords, tuple) + and len(raw_coords) == 2 + ): x_disp, y_disp = raw_coords # Proceed with calculation geo = self.state.current_sar_geo_info disp_w = self.state.sar_display_width disp_h = self.state.sar_display_height - lat_s: str = "N/A" # Default result string - lon_s: str = "N/A" # Default result string + lat_s: str = "N/A" # Default result string + lon_s: str = "N/A" # Default result string # --- Check if geo info is valid for calculation --- # Requires valid geo dict, positive display and original dimensions, positive scale factors is_geo_valid_for_calc = ( - geo and geo.get("valid") and disp_w > 0 and disp_h > 0 and - geo.get("width_px", 0) > 0 and geo.get("height_px", 0) > 0 and - geo.get("scale_x", 0.0) > 0 and geo.get("scale_y", 0.0) > 0 and + geo + and geo.get("valid") + and disp_w > 0 + and disp_h > 0 + and geo.get("width_px", 0) > 0 + and geo.get("height_px", 0) > 0 + and geo.get("scale_x", 0.0) > 0 + and geo.get("scale_y", 0.0) > 0 + and # Check essential lat/lon keys exist - "lat" in geo and "lon" in geo and "ref_x" in geo and "ref_y" in geo + "lat" in geo + and "lon" in geo + and "ref_x" in geo + and "ref_y" in geo ) if is_geo_valid_for_calc: - logging.debug(f"{log_prefix} Processing mouse coords: Display({x_disp},{y_disp}) with valid GeoInfo.") - try: - # --- Extract necessary values from state --- - orig_w = geo["width_px"] - orig_h = geo["height_px"] - scale_x = geo["scale_x"] # meters per pixel - scale_y = geo["scale_y"] # meters per pixel - ref_x = geo["ref_x"] # pixel coord of ref lat/lon - ref_y = geo["ref_y"] # pixel coord of ref lat/lon - ref_lat_rad = geo["lat"] # radians - ref_lon_rad = geo["lon"] # radians - orient_rad = geo.get("orientation", 0.0) # radians (default to 0 if missing) - - orient_rad = -orient_rad #inverse angle + logging.debug( + f"{log_prefix} Processing mouse coords: Display({x_disp},{y_disp}) with valid GeoInfo." + ) + try: + # --- Extract necessary values from state --- + orig_w = geo["width_px"] + orig_h = geo["height_px"] + scale_x = geo["scale_x"] # meters per pixel + scale_y = geo["scale_y"] # meters per pixel + ref_x = geo["ref_x"] # pixel coord of ref lat/lon + ref_y = geo["ref_y"] # pixel coord of ref lat/lon + ref_lat_rad = geo["lat"] # radians + ref_lon_rad = geo["lon"] # radians + orient_rad = geo.get( + "orientation", 0.0 + ) # radians (default to 0 if missing) - # --- Coordinate Transformation --- - # 1. Normalize display coordinates to [0, 1] range - # Avoid division by zero checked by is_geo_valid_for_calc - nx_disp = x_disp / disp_w - ny_disp = y_disp / disp_h + # Read the original orientation angle from geo data (in radians) + original_orient_rad = geo.get("orientation", 0.0) - # 2. Apply Inverse Rotation if needed - nx_orig_norm, ny_orig_norm = nx_disp, ny_disp # Start with normalized display coords - if abs(orient_rad) > 1e-4: # Check if rotation is significant - logging.debug(f"{log_prefix} Applying inverse rotation (angle: {math.degrees(orient_rad):.2f} deg)...") - # Inverse rotation angle - arad_inv = -orient_rad - cosa = math.cos(arad_inv) - sina = math.sin(arad_inv) - # Rotation center (normalized coordinates: 0.5, 0.5) - cx, cy = 0.5, 0.5 - # Translate coordinates relative to center - tx = nx_disp - cx - ty = ny_disp - cy - # Perform 2D rotation - rtx = tx * cosa - ty * sina - rty = tx * sina + ty * cosa - # Translate back and store result - nx_orig_norm = rtx + cx - ny_orig_norm = rty + cy - logging.debug(f"{log_prefix} Inverse rotation applied. Norm coords: ({nx_orig_norm:.4f}, {ny_orig_norm:.4f})") + # The SAR image displayed to the user is rotated by -original_orient_rad. + # To find the geographic coordinate corresponding to the mouse click on the + # *displayed* image, we must apply the *inverse* of the display rotation + # to the normalized mouse coordinates *before* converting to geographic space. + # The inverse of a rotation by -original_orient_rad is a rotation by +original_orient_rad. + angle_for_inverse_rotation_rad = original_orient_rad + # --- Coordinate Transformation --- + # 1. Normalize display coordinates to [0, 1] range + # Avoid division by zero checked by is_geo_valid_for_calc + nx_disp = x_disp / disp_w + ny_disp = y_disp / disp_h + # 2. Apply Inverse Rotation if needed + nx_orig_norm, ny_orig_norm = ( + nx_disp, + ny_disp, + ) # Start with normalized display coords + if abs(angle_for_inverse_rotation_rad) > 1e-4: # Check if rotation is significant + logging.debug( + f"{log_prefix} Applying inverse rotation (angle: {math.degrees(angle_for_inverse_rotation_rad):.2f} deg)..." + ) + # Use the angle needed to undo the display rotation + arad_inv = angle_for_inverse_rotation_rad # Use the *original* angle + cosa = math.cos(arad_inv) + sina = math.sin(arad_inv) + # Rotation center (normalized coordinates: 0.5, 0.5) + cx, cy = 0.5, 0.5 + # Translate coordinates relative to center + tx = nx_disp - cx + ty = ny_disp - cy + # Perform 2D rotation + rtx = tx * cosa - ty * sina + rty = tx * sina + ty * cosa + # Translate back and store result + nx_orig_norm = rtx + cx + ny_orig_norm = rty + cy + logging.debug( + f"{log_prefix} Inverse rotation applied. Norm coords: ({nx_orig_norm:.4f}, {ny_orig_norm:.4f})" + ) - # 3. Convert normalized original coordinates back to original pixel coordinates - # Clamp result to valid pixel range [0, width/height - 1] - orig_x = max(0.0, min(nx_orig_norm * orig_w, orig_w - 1.0)) - orig_y = max(0.0, min(ny_orig_norm * orig_h, orig_h - 1.0)) - logging.debug(f"{log_prefix} Calculated original pixel coords: ({orig_x:.2f}, {orig_y:.2f})") + # 3. Convert normalized original coordinates back to original pixel coordinates + # Clamp result to valid pixel range [0, width/height - 1] + orig_x = max(0.0, min(nx_orig_norm * orig_w, orig_w - 1.0)) + orig_y = max(0.0, min(ny_orig_norm * orig_h, orig_h - 1.0)) + logging.debug( + f"{log_prefix} Calculated original pixel coords: ({orig_x:.2f}, {orig_y:.2f})" + ) + # --- Geodetic Calculation --- + # 4. Calculate pixel offset from reference point + # Note y-axis inversion: display Y increases downwards, geo latitude increases upwards + pixel_delta_x = orig_x - ref_x + pixel_delta_y = ( + ref_y - orig_y + ) # Positive delta_y means pointing North from ref + # 5. Convert pixel offset to meter offset using scale factors + meters_delta_x = pixel_delta_x * scale_x # Positive delta_x = East + meters_delta_y = pixel_delta_y * scale_y # Positive delta_y = North + logging.debug( + f"{log_prefix} Calculated offset from ref (pixels): dX={pixel_delta_x:.1f}, dY={pixel_delta_y:.1f}" + ) + logging.debug( + f"{log_prefix} Calculated offset from ref (meters): dX={meters_delta_x:.1f} (E), dY={meters_delta_y:.1f} (N)" + ) - # --- Geodetic Calculation --- - # 4. Calculate pixel offset from reference point - # Note y-axis inversion: display Y increases downwards, geo latitude increases upwards - pixel_delta_x = orig_x - ref_x - pixel_delta_y = ref_y - orig_y # Positive delta_y means pointing North from ref + # 6. Convert meter offsets to degree offsets (approximate using simple sphere/ellipse model) + # Constants for WGS84 approximation + M_PER_DLAT = ( + 111132.954 # Approx meters per degree latitude (constant) + ) + M_PER_DLON_EQ = ( + 111319.488 # Approx meters per degree longitude at equator + ) + # Adjust meters per degree longitude based on reference latitude + # Avoid division by zero near poles (cos(pi/2)=0) + m_per_dlon = max( + abs(M_PER_DLON_EQ * math.cos(ref_lat_rad)), 1e-3 + ) # Use small non-zero minimum + # Calculate degree offsets + lat_offset_deg = meters_delta_y / M_PER_DLAT + lon_offset_deg = meters_delta_x / m_per_dlon + logging.debug( + f"{log_prefix} Calculated offset (degrees): dLat={lat_offset_deg:.6f}, dLon={lon_offset_deg:.6f}" + ) - # 5. Convert pixel offset to meter offset using scale factors - meters_delta_x = pixel_delta_x * scale_x # Positive delta_x = East - meters_delta_y = pixel_delta_y * scale_y # Positive delta_y = North - logging.debug(f"{log_prefix} Calculated offset from ref (pixels): dX={pixel_delta_x:.1f}, dY={pixel_delta_y:.1f}") - logging.debug(f"{log_prefix} Calculated offset from ref (meters): dX={meters_delta_x:.1f} (E), dY={meters_delta_y:.1f} (N)") + # 7. Calculate final coordinates by adding offsets to reference coordinates + final_lat_deg = math.degrees(ref_lat_rad) + lat_offset_deg + final_lon_deg = math.degrees(ref_lon_rad) + lon_offset_deg + logging.debug( + f"{log_prefix} Calculated final coords (dec deg): Lat={final_lat_deg:.6f}, Lon={final_lon_deg:.6f}" + ) - - # 6. Convert meter offsets to degree offsets (approximate using simple sphere/ellipse model) - # Constants for WGS84 approximation - M_PER_DLAT = 111132.954 # Approx meters per degree latitude (constant) - M_PER_DLON_EQ = 111319.488 # Approx meters per degree longitude at equator - # Adjust meters per degree longitude based on reference latitude - # Avoid division by zero near poles (cos(pi/2)=0) - m_per_dlon = max(abs(M_PER_DLON_EQ * math.cos(ref_lat_rad)), 1e-3) # Use small non-zero minimum - - # Calculate degree offsets - lat_offset_deg = meters_delta_y / M_PER_DLAT - lon_offset_deg = meters_delta_x / m_per_dlon - logging.debug(f"{log_prefix} Calculated offset (degrees): dLat={lat_offset_deg:.6f}, dLon={lon_offset_deg:.6f}") - - - # 7. Calculate final coordinates by adding offsets to reference coordinates - final_lat_deg = math.degrees(ref_lat_rad) + lat_offset_deg - final_lon_deg = math.degrees(ref_lon_rad) + lon_offset_deg - logging.debug(f"{log_prefix} Calculated final coords (dec deg): Lat={final_lat_deg:.6f}, Lon={final_lon_deg:.6f}") - - - # --- Format Output --- - # 8. Validate calculated coordinates and format to DMS string - # Check for NaN/inf and reasonable lat/lon ranges - if (math.isfinite(final_lat_deg) and math.isfinite(final_lon_deg) and - abs(final_lat_deg) <= 90.0 and abs(final_lon_deg) <= 180.0): - # Use utility function for DMS conversion - lat_s = decimal_to_dms(final_lat_deg, is_latitude=True) - lon_s = decimal_to_dms(final_lon_deg, is_latitude=False) - # Check if DMS conversion itself returned an error string - if "Error" in lat_s or "Invalid" in lat_s or "Error" in lon_s or "Invalid" in lon_s: - logging.warning(f"{log_prefix} DMS conversion failed for valid decimal degrees.") - lat_s, lon_s = "Error DMS", "Error DMS" - else: - # Log warning if calculated coordinates are out of valid range - logging.warning(f"{log_prefix} Calculated coordinates out of valid range: Lat={final_lat_deg}, Lon={final_lon_deg}") - lat_s, lon_s = "Invalid Calc", "Invalid Calc" - - except KeyError as ke: - # Log error if required keys are missing in state - logging.error(f"{log_prefix} Missing key in geo_info state during calculation: {ke}") - lat_s, lon_s = "Error Key", "Error Key" - except Exception as calc_e: - # Log any other unexpected calculation errors - logging.exception(f"{log_prefix} Geo calculation error:") - lat_s, lon_s = "Calc Error", "Calc Error" + # --- Format Output --- + # 8. Validate calculated coordinates and format to DMS string + # Check for NaN/inf and reasonable lat/lon ranges + if ( + math.isfinite(final_lat_deg) + and math.isfinite(final_lon_deg) + and abs(final_lat_deg) <= 90.0 + and abs(final_lon_deg) <= 180.0 + ): + # Use utility function for DMS conversion + lat_s = decimal_to_dms(final_lat_deg, is_latitude=True) + lon_s = decimal_to_dms(final_lon_deg, is_latitude=False) + # Check if DMS conversion itself returned an error string + if ( + "Error" in lat_s + or "Invalid" in lat_s + or "Error" in lon_s + or "Invalid" in lon_s + ): + logging.warning( + f"{log_prefix} DMS conversion failed for valid decimal degrees." + ) + lat_s, lon_s = "Error DMS", "Error DMS" + else: + # Log warning if calculated coordinates are out of valid range + logging.warning( + f"{log_prefix} Calculated coordinates out of valid range: Lat={final_lat_deg}, Lon={final_lon_deg}" + ) + lat_s, lon_s = "Invalid Calc", "Invalid Calc" + except KeyError as ke: + # Log error if required keys are missing in state + logging.error( + f"{log_prefix} Missing key in geo_info state during calculation: {ke}" + ) + lat_s, lon_s = "Error Key", "Error Key" + except Exception as calc_e: + # Log any other unexpected calculation errors + logging.exception(f"{log_prefix} Geo calculation error:") + lat_s, lon_s = "Calc Error", "Calc Error" # else: Geo info invalid, keep default "N/A" strings # --- Queue Result --- @@ -1938,33 +2382,37 @@ class App: # Reschedule the processor to run again after a short delay if not self.state.shutting_down: - self._reschedule_queue_processor(self.process_mouse_queue, delay=50) # Check every 50ms - + self._reschedule_queue_processor( + self.process_mouse_queue, delay=50 + ) # Check every 50ms def put_mouse_coordinates_queue(self, coords_tuple: Tuple[str, str]): """Puts processed mouse coords tuple onto Tkinter queue using command structure.""" - log_prefix = "[App Mouse Queue Put]" # Specific prefix for this action + log_prefix = "[App Mouse Queue Put]" # Specific prefix for this action if self.state.shutting_down: - return # Don't queue if shutting down + return # Don't queue if shutting down # Structure: command, payload - command = 'MOUSE_COORDS' - payload = coords_tuple # Payload is the tuple (lat_s, lon_s) - logging.debug(f"{log_prefix} Putting command '{command}' with payload {payload} onto tkinter_queue.") + command = "MOUSE_COORDS" + payload = coords_tuple # Payload is the tuple (lat_s, lon_s) + logging.debug( + f"{log_prefix} Putting command '{command}' with payload {payload} onto tkinter_queue." + ) # Use the put_queue utility for safe queueing with drop counting put_queue( self.tkinter_queue, - (command, payload), # Send command and payload as a tuple - queue_name="tkinter", # Identify queue name for logging/stats - app_instance=self # Pass app instance for context + (command, payload), # Send command and payload as a tuple + queue_name="tkinter", # Identify queue name for logging/stats + app_instance=self, # Pass app instance for context ) # --- Queue Processors --- def process_sar_queue(self): """Gets processed SAR image from queue and displays it using DisplayManager.""" log_prefix = "[App QProc SAR]" - if self.state.shutting_down: return # Stop processing on shutdown + if self.state.shutting_down: + return # Stop processing on shutdown image_to_display = None try: @@ -1973,7 +2421,7 @@ class App: # Mark task as done immediately after getting self.sar_queue.task_done() except queue.Empty: - pass # Normal case, queue is empty + pass # Normal case, queue is empty except Exception as e: # Log error getting from queue logging.exception(f"{log_prefix} Error getting from SAR display queue:") @@ -1981,27 +2429,36 @@ class App: # Process item if successfully retrieved if image_to_display is not None: - logging.debug(f"{log_prefix} Dequeued SAR image (Shape: {getattr(image_to_display, 'shape', 'N/A')}). Calling DisplayManager...") - # Check if display manager exists - if hasattr(self, 'display_manager') and self.display_manager: - try: - # Call DisplayManager to show the image - self.display_manager.show_sar_image(image_to_display) - except Exception as display_e: - # Log errors during display call - logging.exception(f"{log_prefix} Error calling DisplayManager.show_sar_image:") - else: - logging.error(f"{log_prefix} DisplayManager not available to show SAR image.") + logging.debug( + f"{log_prefix} Dequeued SAR image (Shape: {getattr(image_to_display, 'shape', 'N/A')}). Calling DisplayManager..." + ) + # Check if display manager exists + if hasattr(self, "display_manager") and self.display_manager: + try: + # Call DisplayManager to show the image + self.display_manager.show_sar_image(image_to_display) + except Exception as display_e: + # Log errors during display call + logging.exception( + f"{log_prefix} Error calling DisplayManager.show_sar_image:" + ) + else: + logging.error( + f"{log_prefix} DisplayManager not available to show SAR image." + ) # else: Item was None (error during get or queue empty) # Reschedule next check always if not self.state.shutting_down: - self._reschedule_queue_processor(self.process_sar_queue) # Use default delay + self._reschedule_queue_processor( + self.process_sar_queue + ) # Use default delay def process_mfd_queue(self): """Gets processed MFD image from queue and displays it using DisplayManager.""" log_prefix = "[App QProc MFD]" - if self.state.shutting_down: return # Stop processing on shutdown + if self.state.shutting_down: + return # Stop processing on shutdown image_to_display = None try: @@ -2010,33 +2467,42 @@ class App: # Mark task done self.mfd_queue.task_done() except queue.Empty: - pass # Normal case + pass # Normal case except Exception as e: # Log error getting from queue logging.exception(f"{log_prefix} Error getting from MFD display queue:") # Process if item retrieved if image_to_display is not None: - logging.debug(f"{log_prefix} Dequeued MFD image (Shape: {getattr(image_to_display, 'shape', 'N/A')}). Calling DisplayManager...") + logging.debug( + f"{log_prefix} Dequeued MFD image (Shape: {getattr(image_to_display, 'shape', 'N/A')}). Calling DisplayManager..." + ) # Check if display manager exists - if hasattr(self, 'display_manager') and self.display_manager: - try: - # Call DisplayManager to show the image - self.display_manager.show_mfd_image(image_to_display) - except Exception as display_e: - # Log errors during display call - logging.exception(f"{log_prefix} Error calling DisplayManager.show_mfd_image:") + if hasattr(self, "display_manager") and self.display_manager: + try: + # Call DisplayManager to show the image + self.display_manager.show_mfd_image(image_to_display) + except Exception as display_e: + # Log errors during display call + logging.exception( + f"{log_prefix} Error calling DisplayManager.show_mfd_image:" + ) else: - logging.error(f"{log_prefix} DisplayManager not available to show MFD image.") + logging.error( + f"{log_prefix} DisplayManager not available to show MFD image." + ) # Reschedule next check always if not self.state.shutting_down: - self._reschedule_queue_processor(self.process_mfd_queue) # Use default delay + self._reschedule_queue_processor( + self.process_mfd_queue + ) # Use default delay def process_tkinter_queue(self): """Processes commands (mouse coords, map updates) from queue to update UI.""" log_prefix = "[App QProc Tkinter]" - if self.state.shutting_down: return # Stop processing on shutdown + if self.state.shutting_down: + return # Stop processing on shutdown item = None try: @@ -2045,7 +2511,7 @@ class App: # Mark task done *after* getting item successfully self.tkinter_queue.task_done() except queue.Empty: - pass # Normal case + pass # Normal case except Exception as e: # Log error getting from queue logging.exception(f"{log_prefix} Error getting from Tkinter queue:") @@ -2053,33 +2519,41 @@ class App: # Process item if successfully retrieved if item is not None: - try: - # Check if item is the expected command tuple structure - if isinstance(item, tuple) and len(item) == 2: - command, payload = item - logging.debug(f"{log_prefix} Dequeued Command:'{command}', Payload Type:{type(payload)}") + try: + # Check if item is the expected command tuple structure + if isinstance(item, tuple) and len(item) == 2: + command, payload = item + logging.debug( + f"{log_prefix} Dequeued Command:'{command}', Payload Type:{type(payload)}" + ) - # Handle different commands - if command == 'MOUSE_COORDS': - # Update mouse coordinate display in UI - self._handle_mouse_coords_update(payload) - elif command == 'SHOW_MAP': - # Update the map display window (delegated to manager) - self._handle_show_map_update(payload) - else: - # Log warning for unknown commands - logging.warning(f"{log_prefix} Unknown command received: {command}") - # Handle legacy None for mouse coordinates (can be removed if put_mouse_coordinates_queue always sends tuple) - # elif item is None: - # self._handle_mouse_coords_update(None) - else: - # Log warning for unexpected item types - logging.warning(f"{log_prefix} Dequeued unexpected item type: {type(item)}") + # Handle different commands + if command == "MOUSE_COORDS": + # Update mouse coordinate display in UI + self._handle_mouse_coords_update(payload) + elif command == "SHOW_MAP": + # Update the map display window (delegated to manager) + self._handle_show_map_update(payload) + else: + # Log warning for unknown commands + logging.warning( + f"{log_prefix} Unknown command received: {command}" + ) + # Handle legacy None for mouse coordinates (can be removed if put_mouse_coordinates_queue always sends tuple) + # elif item is None: + # self._handle_mouse_coords_update(None) + else: + # Log warning for unexpected item types + logging.warning( + f"{log_prefix} Dequeued unexpected item type: {type(item)}" + ) - except Exception as e: - # Log error during processing of the dequeued item - logging.exception(f"{log_prefix} Error processing dequeued Tkinter item:") - # Task already marked done, avoid potential infinite loops on bad data. + except Exception as e: + # Log error during processing of the dequeued item + logging.exception( + f"{log_prefix} Error processing dequeued Tkinter item:" + ) + # Task already marked done, avoid potential infinite loops on bad data. # Reschedule next check always if not self.state.shutting_down: @@ -2087,32 +2561,38 @@ class App: self._reschedule_queue_processor(self.process_tkinter_queue, delay=100) def _handle_mouse_coords_update(self, payload: Optional[Tuple[str, str]]): - """Updates the mouse coordinates UI label.""" - log_prefix = "[App QProc Tkinter]" # Part of Tkinter queue processing - lat_s, lon_s = ("N/A", "N/A") # Default values + """Updates the mouse coordinates UI label.""" + log_prefix = "[App QProc Tkinter]" # Part of Tkinter queue processing + lat_s, lon_s = ("N/A", "N/A") # Default values - # Check if payload is the expected tuple of strings - if payload is not None: - if isinstance(payload, tuple) and len(payload) == 2: - lat_s, lon_s = payload # Unpack the strings - else: - # Log error if payload is not None or the expected tuple format - logging.warning(f"{log_prefix} Invalid payload type/format for MOUSE_COORDS: {type(payload)}") - lat_s, lon_s = ("Error", "Error") # Indicate error in UI + # Check if payload is the expected tuple of strings + if payload is not None: + if isinstance(payload, tuple) and len(payload) == 2: + lat_s, lon_s = payload # Unpack the strings + else: + # Log error if payload is not None or the expected tuple format + logging.warning( + f"{log_prefix} Invalid payload type/format for MOUSE_COORDS: {type(payload)}" + ) + lat_s, lon_s = ("Error", "Error") # Indicate error in UI - # Safely update the UI label - try: - # Check if control panel and the specific method exist - if hasattr(self,'control_panel') and hasattr(self.control_panel, 'set_mouse_coordinates'): - # Call the method on the ControlPanel instance - self.control_panel.set_mouse_coordinates(lat_s, lon_s) - # else: # Don't log if UI elements not ready, expected during init/shutdown - except tk.TclError as e: - if not self.state.shutting_down: - logging.warning(f"{log_prefix} Error updating mouse coords UI (TclError): {e}") - except Exception as e: - # Log other unexpected errors during UI update - logging.warning(f"{log_prefix} Error updating mouse coords UI: {e}") + # Safely update the UI label + try: + # Check if control panel and the specific method exist + if hasattr(self, "control_panel") and hasattr( + self.control_panel, "set_mouse_coordinates" + ): + # Call the method on the ControlPanel instance + self.control_panel.set_mouse_coordinates(lat_s, lon_s) + # else: # Don't log if UI elements not ready, expected during init/shutdown + except tk.TclError as e: + if not self.state.shutting_down: + logging.warning( + f"{log_prefix} Error updating mouse coords UI (TclError): {e}" + ) + except Exception as e: + # Log other unexpected errors during UI update + logging.warning(f"{log_prefix} Error updating mouse coords UI: {e}") def _handle_show_map_update(self, payload: Optional[Image.Image]): """ @@ -2121,64 +2601,78 @@ class App: Args: payload (Optional[Image.Image]): The PIL Image object (or None) to display. """ - log_prefix = "[App QProc Tkinter]" # Part of Tkinter queue processing + log_prefix = "[App QProc Tkinter]" # Part of Tkinter queue processing logging.debug(f"{log_prefix} Processing SHOW_MAP command...") # Check if the map manager is active - if hasattr(self, 'map_integration_manager') and self.map_integration_manager: - try: - # Delegate the display task to the manager's method - self.map_integration_manager.display_map(payload) - except Exception as e: - # Log errors calling the manager's method - logging.exception(f"{log_prefix} Error calling map_integration_manager.display_map:") + if hasattr(self, "map_integration_manager") and self.map_integration_manager: + try: + # Delegate the display task to the manager's method + self.map_integration_manager.display_map(payload) + except Exception as e: + # Log errors calling the manager's method + logging.exception( + f"{log_prefix} Error calling map_integration_manager.display_map:" + ) else: - # Log warning if command received but map manager is not active - logging.warning(f"{log_prefix} Received SHOW_MAP command but MapIntegrationManager is not active.") - + # Log warning if command received but map manager is not active + logging.warning( + f"{log_prefix} Received SHOW_MAP command but MapIntegrationManager is not active." + ) def _reschedule_queue_processor(self, processor_func, delay: Optional[int] = None): """Helper method to reschedule a queue processor function using root.after.""" # Calculate default delay if none provided if delay is None: - # Default delay based on target MFD FPS for image queues (run slightly faster than FPS) - if processor_func in [self.process_sar_queue, self.process_mfd_queue]: - target_fps = config.MFD_FPS - # Calculate delay in ms, ensure minimum delay (e.g., 10ms) - delay = max(10, int(1000 / (target_fps * 1.5))) if target_fps > 0 else 20 # Default ~20ms if FPS is 0/invalid - else: - # Default delay for other queues (like tkinter, mouse) - check less often - delay = 100 # ms + # Default delay based on target MFD FPS for image queues (run slightly faster than FPS) + if processor_func in [self.process_sar_queue, self.process_mfd_queue]: + target_fps = config.MFD_FPS + # Calculate delay in ms, ensure minimum delay (e.g., 10ms) + delay = ( + max(10, int(1000 / (target_fps * 1.5))) if target_fps > 0 else 20 + ) # Default ~20ms if FPS is 0/invalid + else: + # Default delay for other queues (like tkinter, mouse) - check less often + delay = 100 # ms # Schedule the next call using root.after, checking root existence try: if self.root and self.root.winfo_exists(): - self.root.after(delay, processor_func) + self.root.after(delay, processor_func) # else: Don't warn if root is gone, expected during shutdown except Exception as e: - # Log error only if not shutting down - if not self.state.shutting_down: - logging.warning(f"[App Rescheduler] Error rescheduling {processor_func.__name__}: {e}") + # Log error only if not shutting down + if not self.state.shutting_down: + logging.warning( + f"[App Rescheduler] Error rescheduling {processor_func.__name__}: {e}" + ) # --- Status Update --- def update_status(self): """Updates status bar text and statistics labels periodically.""" log_prefix = "[App Status Update]" # Check shutdown flag and state initialization - if not hasattr(self, 'state') or self.state.shutting_down: - return + if not hasattr(self, "state") or self.state.shutting_down: + return # Avoid updating status if initial map load is in progress (status set by map manager) map_loading = False try: - # Check if status bar indicates map loading - if hasattr(self,'statusbar') and self.statusbar.winfo_exists() and "Loading initial map" in self.statusbar.cget("text"): - map_loading = True - logging.debug(f"{log_prefix} Skipping status update while initial map loads.") - return + # Check if status bar indicates map loading + if ( + hasattr(self, "statusbar") + and self.statusbar.winfo_exists() + and "Loading initial map" in self.statusbar.cget("text") + ): + map_loading = True + logging.debug( + f"{log_prefix} Skipping status update while initial map loads." + ) + return except Exception as status_check_e: - # Ignore errors checking status bar text - logging.debug(f"{log_prefix} Error checking status bar for map loading message: {status_check_e}") - + # Ignore errors checking status bar text + logging.debug( + f"{log_prefix} Error checking status bar for map loading message: {status_check_e}" + ) logging.debug(f"{log_prefix} Updating status bar and statistics labels...") # Get latest statistics dictionary from AppState (thread-safe read) @@ -2187,60 +2681,96 @@ class App: try: # --- Format Status String Components --- # Mode - mode = "Test" if self.state.test_mode_active else ("Local" if config.USE_LOCAL_IMAGES else "Network") + mode = ( + "Test" + if self.state.test_mode_active + else ("Local" if config.USE_LOCAL_IMAGES else "Network") + ) # Map Status - map_active = hasattr(self, 'map_integration_manager') and self.map_integration_manager is not None + map_active = ( + hasattr(self, "map_integration_manager") + and self.map_integration_manager is not None + ) map_stat = " MapOn" if config.ENABLE_MAP_OVERLAY and map_active else "" # FPS (use values directly from state) mfd_fps = f"MFD:{self.state.mfd_fps:.1f}fps" - sar_fps = f"SAR:{self.state.sar_fps:.1f}fps" if self.state.sar_fps > 0 else "SAR:N/A" + sar_fps = ( + f"SAR:{self.state.sar_fps:.1f}fps" + if self.state.sar_fps > 0 + else "SAR:N/A" + ) # Mouse Coordinates (get current text from UI label) - mouse_txt = "Mouse: N/A" # Default - if hasattr(self.control_panel,'mouse_latlon_label') and self.control_panel.mouse_latlon_label.winfo_exists(): + mouse_txt = "Mouse: N/A" # Default + if ( + hasattr(self.control_panel, "mouse_latlon_label") + and self.control_panel.mouse_latlon_label.winfo_exists() + ): try: # Get text directly, no need for full 'Mouse: ...' prefix if label holds it - mouse_label_text = self.control_panel.mouse_latlon_label.cget("text") + mouse_label_text = self.control_panel.mouse_latlon_label.cget( + "text" + ) # Extract coords part if label format is consistent if mouse_label_text.startswith("Mouse"): - mouse_txt = mouse_label_text # Keep full label text - else: # Assume label only contains coords - mouse_txt = f"Mouse: {mouse_label_text}" + mouse_txt = mouse_label_text # Keep full label text + else: # Assume label only contains coords + mouse_txt = f"Mouse: {mouse_label_text}" except Exception: - mouse_txt="Mouse: UI Error" # Fallback on error getting label text + mouse_txt = ( + "Mouse: UI Error" # Fallback on error getting label text + ) # Assemble final status string (first part of status bar) - status_prefix = f"Status: {mode}{map_stat}" # Use the prefix set by set_status + status_prefix = ( + f"Status: {mode}{map_stat}" # Use the prefix set by set_status + ) status_info = f"{mfd_fps} | {sar_fps} | {mouse_txt}" - full_status = f"{status_prefix} | {status_info}" # Combine prefix and info + full_status = f"{status_prefix} | {status_info}" # Combine prefix and info # --- Format Statistics Strings --- - drop_txt = (f"Drop(Q): S={stats['dropped_sar_q']},M={stats['dropped_mfd_q']}, " - f"Tk={stats['dropped_tk_q']},Mo={stats['dropped_mouse_q']}") - incmpl_txt = (f"Incmpl(RX): S={stats['incomplete_sar_rx']}," - f"M={stats['incomplete_mfd_rx']}") + drop_txt = ( + f"Drop(Q): S={stats['dropped_sar_q']},M={stats['dropped_mfd_q']}, " + f"Tk={stats['dropped_tk_q']},Mo={stats['dropped_mouse_q']}" + ) + incmpl_txt = ( + f"Incmpl(RX): S={stats['incomplete_sar_rx']}," + f"M={stats['incomplete_mfd_rx']}" + ) - logging.debug(f"{log_prefix} Formatted status strings. Status: '{full_status}', Drop: '{drop_txt}', Incmpl: '{incmpl_txt}'") + logging.debug( + f"{log_prefix} Formatted status strings. Status: '{full_status}', Drop: '{drop_txt}', Incmpl: '{incmpl_txt}'" + ) # --- Schedule UI updates on main thread using after_idle --- if self.root and self.root.winfo_exists(): # Update Status Bar - if hasattr(self,'statusbar') and self.statusbar.winfo_exists(): + if hasattr(self, "statusbar") and self.statusbar.winfo_exists(): # Update the full text using status bar's method self.root.after_idle(self.statusbar.set_status_text, full_status) # Update Dropped Label - if hasattr(self.control_panel,'dropped_label') and self.control_panel.dropped_label.winfo_exists(): - self.root.after_idle(self.control_panel.dropped_label.config, {"text": drop_txt}) + if ( + hasattr(self.control_panel, "dropped_label") + and self.control_panel.dropped_label.winfo_exists() + ): + self.root.after_idle( + self.control_panel.dropped_label.config, {"text": drop_txt} + ) # Update Incomplete Label - if hasattr(self.control_panel,'incomplete_label') and self.control_panel.incomplete_label.winfo_exists(): - self.root.after_idle(self.control_panel.incomplete_label.config, {"text": incmpl_txt}) + if ( + hasattr(self.control_panel, "incomplete_label") + and self.control_panel.incomplete_label.winfo_exists() + ): + self.root.after_idle( + self.control_panel.incomplete_label.config, {"text": incmpl_txt} + ) except tk.TclError as e: - # Log TclErrors (widget likely destroyed during update) only if not shutting down - if not self.state.shutting_down: - logging.warning(f"{log_prefix} TclError updating status UI: {e}") + # Log TclErrors (widget likely destroyed during update) only if not shutting down + if not self.state.shutting_down: + logging.warning(f"{log_prefix} TclError updating status UI: {e}") except Exception as e: - # Log other unexpected errors during formatting/scheduling - if not self.state.shutting_down: + # Log other unexpected errors during formatting/scheduling + if not self.state.shutting_down: logging.exception(f"{log_prefix} Error formatting/updating status UI:") # --- Cleanup --- @@ -2248,51 +2778,61 @@ class App: """Performs graceful shutdown of the application and its components.""" log_prefix = "[App Shutdown]" # Prevent double execution if called multiple times - if hasattr(self, 'state') and self.state.shutting_down: - logging.warning(f"{log_prefix} Close sequence already initiated. Ignoring request.") + if hasattr(self, "state") and self.state.shutting_down: + logging.warning( + f"{log_prefix} Close sequence already initiated. Ignoring request." + ) return # Ensure state exists before setting flag - if not hasattr(self, 'state'): - logging.error(f"{log_prefix} Cannot initiate shutdown: AppState not found.") - # Attempt basic exit? - sys.exit(1) + if not hasattr(self, "state"): + logging.error(f"{log_prefix} Cannot initiate shutdown: AppState not found.") + # Attempt basic exit? + sys.exit(1) - logging.info(f"{log_prefix} Close application requested. Starting shutdown sequence...") + logging.info( + f"{log_prefix} Close application requested. Starting shutdown sequence..." + ) # 1. Set Shutdown Flag in Central State logging.debug(f"{log_prefix} Setting shutdown flag to True in AppState.") self.state.shutting_down = True # Attempt to update status bar (might fail if UI closing) try: - # Use set_status for thread safety if called from non-main thread - self.set_status("Closing...") + # Use set_status for thread safety if called from non-main thread + self.set_status("Closing...") except Exception: - pass # Ignore errors setting status during shutdown + pass # Ignore errors setting status during shutdown # 2. Stop Test Mode Timers (via manager) logging.debug(f"{log_prefix} Stopping TestModeManager timers...") - if hasattr(self, 'test_mode_manager') and self.test_mode_manager: - self.test_mode_manager.stop_timers() + if hasattr(self, "test_mode_manager") and self.test_mode_manager: + self.test_mode_manager.stop_timers() else: - logging.debug(f"{log_prefix} TestModeManager not found or already cleaned up.") + logging.debug( + f"{log_prefix} TestModeManager not found or already cleaned up." + ) # 3. Stop Map Integration (if active) logging.debug(f"{log_prefix} Shutting down MapIntegrationManager...") - if hasattr(self, 'map_integration_manager') and self.map_integration_manager: - self.map_integration_manager.shutdown() # Closes map window + if hasattr(self, "map_integration_manager") and self.map_integration_manager: + self.map_integration_manager.shutdown() # Closes map window else: - logging.debug(f"{log_prefix} MapIntegrationManager not found or not active.") + logging.debug( + f"{log_prefix} MapIntegrationManager not found or not active." + ) # 4. Stop Periodic UI Updates & Queue Processors # These check the self.state.shutting_down flag internally, no explicit stop needed, # but log the intent. - logging.debug(f"{log_prefix} Signalling periodic status updates and queue processors to stop.") + logging.debug( + f"{log_prefix} Signalling periodic status updates and queue processors to stop." + ) # 5. Close UDP Socket logging.debug(f"{log_prefix} Closing UDP socket...") if self.udp_socket: - close_udp_socket(self.udp_socket) # Use utility function - self.udp_socket = None # Clear reference + close_udp_socket(self.udp_socket) # Use utility function + self.udp_socket = None # Clear reference logging.debug(f"{log_prefix} UDP socket closed.") else: logging.debug(f"{log_prefix} UDP socket was not open or already closed.") @@ -2302,19 +2842,23 @@ class App: logging.debug(f"{log_prefix} Waiting up to 0.5s for UDP receiver thread...") self.udp_thread.join(timeout=0.5) if self.udp_thread.is_alive(): - logging.warning(f"{log_prefix} UDP receiver thread did not exit cleanly after 0.5s.") + logging.warning( + f"{log_prefix} UDP receiver thread did not exit cleanly after 0.5s." + ) else: logging.debug(f"{log_prefix} UDP receiver thread exited.") elif self.udp_thread: - logging.debug(f"{log_prefix} UDP receiver thread reference exists but thread already finished.") + logging.debug( + f"{log_prefix} UDP receiver thread reference exists but thread already finished." + ) else: - logging.debug(f"{log_prefix} No UDP receiver thread instance found.") + logging.debug(f"{log_prefix} No UDP receiver thread instance found.") # 7. Shutdown Executor Pool from Receiver (if receiver exists) worker_pool = None - if hasattr(self, 'udp_receiver') and self.udp_receiver: - # Access executor attribute safely - worker_pool = getattr(self.udp_receiver, 'executor', None) + if hasattr(self, "udp_receiver") and self.udp_receiver: + # Access executor attribute safely + worker_pool = getattr(self.udp_receiver, "executor", None) if worker_pool: logging.info(f"{log_prefix} Shutting down worker pool (non-blocking)...") @@ -2324,20 +2868,30 @@ class App: logging.debug(f"{log_prefix} Worker pool shutdown initiated.") # Note: Tasks within the pool should ideally check the shutting_down flag too. except Exception as e: - logging.exception(f"{log_prefix} Exception during worker_pool shutdown: {e}") + logging.exception( + f"{log_prefix} Exception during worker_pool shutdown: {e}" + ) else: - logging.debug(f"{log_prefix} Worker pool not found or not initialized in UdpReceiver.") + logging.debug( + f"{log_prefix} Worker pool not found or not initialized in UdpReceiver." + ) # 8. Destroy Display Windows (MFD, SAR) via DisplayManager - logging.debug(f"{log_prefix} Destroying MFD/SAR display windows via DisplayManager...") - if hasattr(self, 'display_manager'): - self.display_manager.destroy_windows() # Handles internal logging and checks + logging.debug( + f"{log_prefix} Destroying MFD/SAR display windows via DisplayManager..." + ) + if hasattr(self, "display_manager"): + self.display_manager.destroy_windows() # Handles internal logging and checks else: - logging.debug(f"{log_prefix} DisplayManager not found or already cleaned up.") + logging.debug( + f"{log_prefix} DisplayManager not found or already cleaned up." + ) # 9. Brief wait for OpenCV events processing (belt-and-braces) try: - logging.debug(f"{log_prefix} Calling cv2.waitKey(5) for final OpenCV cleanup...") + logging.debug( + f"{log_prefix} Calling cv2.waitKey(5) for final OpenCV cleanup..." + ) cv2.waitKey(5) except Exception as e: logging.warning(f"{log_prefix} Error during final cv2.waitKey: {e}") @@ -2349,11 +2903,15 @@ class App: self.root.destroy() logging.info(f"{log_prefix} Tkinter root window destroyed.") except tk.TclError as e: - # Expected if window closed manually before app shutdown logic completed - logging.warning(f"{log_prefix} Error destroying Tkinter window (likely closed already): {e}") + # Expected if window closed manually before app shutdown logic completed + logging.warning( + f"{log_prefix} Error destroying Tkinter window (likely closed already): {e}" + ) except Exception as e: - # Log other unexpected errors during root window destruction - logging.exception(f"{log_prefix} Unexpected error destroying Tkinter window:") + # Log other unexpected errors during root window destruction + logging.exception( + f"{log_prefix} Unexpected error destroying Tkinter window:" + ) # --- Final Log Messages --- logging.info("-----------------------------------------") @@ -2368,29 +2926,29 @@ class App: # --- Main Execution Block --- if __name__ == "__main__": main_log_prefix = "[App Main]" - root = None # Tkinter root window instance - app = None # App instance + root = None # Tkinter root window instance + app = None # App instance try: # --- Pre-Initialization Checks --- # Check Map Module Dependencies *before* creating Tkinter window if map enabled if config.ENABLE_MAP_OVERLAY and not MAP_MODULES_LOADED: - # Log critical error and exit if map enabled but components missing - logging.critical( - f"{main_log_prefix} Map Overlay is enabled in config " - "but required map modules failed to load. Cannot start." + # Log critical error and exit if map enabled but components missing + logging.critical( + f"{main_log_prefix} Map Overlay is enabled in config " + "but required map modules failed to load. Cannot start." ) - sys.exit(1) # Exit with error code + sys.exit(1) # Exit with error code # --- Application Setup --- logging.debug(f"{main_log_prefix} Creating main Tkinter window...") # Create the root window using the UI utility function root = create_main_window( - title="Control Panel v1.1", # Updated version maybe? + title="Control Panel v1.1", # Updated version maybe? min_width=config.TKINTER_MIN_WIDTH, min_height=config.TKINTER_MIN_HEIGHT, - x_pos=10, # Initial position, App.__init__ recalculates and sets geometry - y_pos=10 + x_pos=10, # Initial position, App.__init__ recalculates and sets geometry + y_pos=10, ) logging.debug(f"{main_log_prefix} Main Tkinter window created.") @@ -2401,52 +2959,70 @@ if __name__ == "__main__": # Set the close button behaviour (calls app.close_app for graceful shutdown) root.protocol("WM_DELETE_WINDOW", app.close_app) - logging.debug(f"{main_log_prefix} WM_DELETE_WINDOW protocol set to call app.close_app.") + logging.debug( + f"{main_log_prefix} WM_DELETE_WINDOW protocol set to call app.close_app." + ) # --- Start Event Loop --- - logging.info(f"{main_log_prefix} Starting Tkinter main event loop (root.mainloop())...") + logging.info( + f"{main_log_prefix} Starting Tkinter main event loop (root.mainloop())..." + ) # This blocks until the root window is destroyed root.mainloop() # Code here is reached only after mainloop ends (typically via root.destroy() in close_app) logging.info(f"{main_log_prefix} Tkinter main event loop finished.") except SystemExit as exit_e: - # Catch sys.exit() calls for clean shutdown messages - if exit_e.code == 0: - # Normal exit initiated by close_app - logging.info(f"{main_log_prefix} Application exited normally (sys.exit code 0).") - else: - # Exit due to error (e.g., failed pre-check) - logging.warning(f"{main_log_prefix} Application exited with error code {exit_e.code}.") + # Catch sys.exit() calls for clean shutdown messages + if exit_e.code == 0: + # Normal exit initiated by close_app + logging.info( + f"{main_log_prefix} Application exited normally (sys.exit code 0)." + ) + else: + # Exit due to error (e.g., failed pre-check) + logging.warning( + f"{main_log_prefix} Application exited with error code {exit_e.code}." + ) except Exception as e: # Catch any other unhandled exceptions during setup or mainloop run - logging.critical(f"{main_log_prefix} UNHANDLED EXCEPTION OCCURRED:", exc_info=True) + logging.critical( + f"{main_log_prefix} UNHANDLED EXCEPTION OCCURRED:", exc_info=True + ) # Attempt emergency cleanup only if app instance exists and wasn't already shutting down - if app and hasattr(app,'state') and not app.state.shutting_down: - logging.error(f"{main_log_prefix} Attempting emergency cleanup due to unhandled exception...") + if app and hasattr(app, "state") and not app.state.shutting_down: + logging.error( + f"{main_log_prefix} Attempting emergency cleanup due to unhandled exception..." + ) try: # Call close directly, don't rely on mainloop anymore app.close_app() except SystemExit: - # close_app initiated the exit, this is expected here - pass + # close_app initiated the exit, this is expected here + pass except Exception as cleanup_e: - # Log any error during the emergency cleanup attempt - logging.exception(f"{main_log_prefix} Error during emergency cleanup: {cleanup_e}") + # Log any error during the emergency cleanup attempt + logging.exception( + f"{main_log_prefix} Error during emergency cleanup: {cleanup_e}" + ) # Exit with error code after attempting cleanup or if app never initialized sys.exit(1) finally: # This block executes after try/except, even after sys.exit() is called logging.info(f"{main_log_prefix} Application finally block reached.") # Final OpenCV cleanup attempt (belt-and-braces, may not be necessary if managers clean up) - logging.debug(f"{main_log_prefix} Final check: Destroying any remaining OpenCV windows...") + logging.debug( + f"{main_log_prefix} Final check: Destroying any remaining OpenCV windows..." + ) try: # This might destroy windows not managed by DisplayManager/MapIntegrationManager if any exist cv2.destroyAllWindows() except Exception as cv_err: - # Warning is sufficient here, main cleanup happened in close_app/managers - logging.warning(f"{main_log_prefix} Exception during final cv2.destroyAllWindows(): {cv_err}") + # Warning is sufficient here, main cleanup happened in close_app/managers + logging.warning( + f"{main_log_prefix} Exception during final cv2.destroyAllWindows(): {cv_err}" + ) logging.info("================ Application End ================") # Ensure all log messages are flushed before exit - logging.shutdown() \ No newline at end of file + logging.shutdown() diff --git a/config.py b/config.py index 95ca6c3..dbdb5ab 100644 --- a/config.py +++ b/config.py @@ -61,7 +61,7 @@ DEBUG_NETWORK = False # [Network] - Logs from functions within network.py (sock DEBUG_IMAGE_PROCESSING = False # [ImageProcessing] - Logs from functions within image_processing.py (normalize, resize, palette etc.). # --- Map Debug Flag --- -DEBUG_MAP_DETAILS = False # Set to True to enable detailed map processing logs (tiles, calculations, drawing) +DEBUG_MAP_DETAILS = False # Set to True to enable detailed map processing logs (tiles, calculations, drawing) # --- Other General Configuration --- @@ -153,7 +153,7 @@ MAP_SERVICE_PROVIDER = "osm" # Name of the service to use (must match map_servi # MAP_API_KEY = None # Add this if using a service that requires a key (e.g., Google) MAP_CACHE_DIRECTORY = "map_cache" # Root directory for cached tiles ENABLE_ONLINE_MAP_FETCHING = True # Allow downloading tiles if not in cache -DEFAULT_MAP_ZOOM_LEVEL = 14 # Initial zoom level for the test map (adjust as needed) 12 original, 13 little more big, +DEFAULT_MAP_ZOOM_LEVEL = 14 # Initial zoom level for the test map (adjust as needed) 12 original, 13 little more big, # Color for placeholder tiles when offline/download fails (RGB tuple) OFFLINE_MAP_PLACEHOLDER_COLOR = (200, 200, 200) # Light grey MAX_MAP_DISPLAY_WIDTH = 1024 @@ -165,16 +165,18 @@ MAX_MAP_DISPLAY_HEIGHT = 1024 # NOTE: Setting LAT/LON to 0.0 signals the MapIntegrationManager *NOT* # to display an initial default map area on startup. # The map will only appear after the first valid GeoInfo is received. -SAR_CENTER_LAT = 0.0 #40.7128 # Example: New York City Latitude (Degrees) -SAR_CENTER_LON = 0.0 #-74.0060 # Example: New York City Longitude (Degrees) +SAR_CENTER_LAT = 0.0 # 40.7128 # Example: New York City Latitude (Degrees) +SAR_CENTER_LON = 0.0 # -74.0060 # Example: New York City Longitude (Degrees) SAR_IMAGE_SIZE_KM = ( 50.0 # Example: Width/Height of the area to show on the map in Kilometers ) # --- KML / Google Earth Integration Configuration --- -ENABLE_KML_GENERATION = True # Imposta a True per generare file KML quando arrivano dati SAR validi -KML_OUTPUT_DIRECTORY = "kml_output" # Cartella dove salvare i file KML generati -AUTO_LAUNCH_GOOGLE_EARTH = False # Imposta a True per tentare di aprire automaticamente il KML generato con Google Earth Pro (se installato) +ENABLE_KML_GENERATION = ( + True # Imposta a True per generare file KML quando arrivano dati SAR validi +) +KML_OUTPUT_DIRECTORY = "kml_output" # Cartella dove salvare i file KML generati +AUTO_LAUNCH_GOOGLE_EARTH = False # Imposta a True per tentare di aprire automaticamente il KML generato con Google Earth Pro (se installato) # Opzionale: potresti aggiungere un percorso esplicito all'eseguibile di Google Earth se non è nel PATH # GOOGLE_EARTH_EXECUTABLE_PATH = "C:/Program Files/Google/Google Earth Pro/client/googleearth.exe" # Esempio Windows diff --git a/image_pipeline.py b/image_pipeline.py index 401fe6f..77b9bcc 100644 --- a/image_pipeline.py +++ b/image_pipeline.py @@ -14,7 +14,7 @@ images for display by the DisplayManager. # Standard library imports import logging -import queue # For type hinting +import queue # For type hinting import math from typing import Optional @@ -26,12 +26,14 @@ import cv2 from app_state import AppState from utils import put_queue from image_processing import ( - apply_color_palette, # Keep specific imports needed + apply_color_palette, # Keep specific imports needed resize_image, # normalize_image is used by receiver/test manager, not directly here ) + # Forward declaration for type hinting App instance from typing import TYPE_CHECKING + if TYPE_CHECKING: from app import App @@ -44,7 +46,7 @@ class ImagePipeline: app_state: AppState, sar_queue: queue.Queue, mfd_queue: queue.Queue, - app: 'App', # Use forward declaration for App type hint + app: "App", # Use forward declaration for App type hint ): """ Initializes the ImagePipeline. @@ -61,7 +63,7 @@ class ImagePipeline: self._app_state: AppState = app_state self._sar_queue: queue.Queue = sar_queue self._mfd_queue: queue.Queue = mfd_queue - self._app: 'App' = app # Store app reference + self._app: "App" = app # Store app reference logging.debug(f"{self._log_prefix} Initialization complete.") @@ -83,7 +85,9 @@ class ImagePipeline: # Validate Input Image from state current_normalized_sar = self._app_state.current_sar_normalized if current_normalized_sar is None or current_normalized_sar.size == 0: - logging.warning(f"{log_prefix} No normalized SAR image in AppState. Cannot process.") + logging.warning( + f"{log_prefix} No normalized SAR image in AppState. Cannot process." + ) return # Get parameters from state safely @@ -94,66 +98,90 @@ class ImagePipeline: target_w = self._app_state.sar_display_width target_h = self._app_state.sar_display_height except AttributeError as ae: - logging.error(f"{log_prefix} Missing required attribute in AppState: {ae}. Cannot process.") - return + logging.error( + f"{log_prefix} Missing required attribute in AppState: {ae}. Cannot process." + ) + return # Validate essential parameters if bc_lut is None: - logging.warning(f"{log_prefix} Brightness/Contrast LUT missing in AppState. Cannot process.") - return + logging.warning( + f"{log_prefix} Brightness/Contrast LUT missing in AppState. Cannot process." + ) + return if target_w <= 0 or target_h <= 0: - logging.warning(f"{log_prefix} Invalid target display dimensions ({target_w}x{target_h}). Cannot process.") - return - + logging.warning( + f"{log_prefix} Invalid target display dimensions ({target_w}x{target_h}). Cannot process." + ) + return logging.debug(f"{log_prefix} Processing SAR image from AppState...") # Work on a copy to avoid modifying state during processing # Ensure the input image is uint8 as expected by subsequent steps if current_normalized_sar.dtype != np.uint8: - logging.warning(f"{log_prefix} Input SAR normalized image is not uint8 ({current_normalized_sar.dtype}). Attempting conversion.") - try: - base_img = current_normalized_sar.astype(np.uint8) - except Exception as e: - logging.error(f"{log_prefix} Failed to convert input SAR to uint8: {e}. Aborting.") - return + logging.warning( + f"{log_prefix} Input SAR normalized image is not uint8 ({current_normalized_sar.dtype}). Attempting conversion." + ) + try: + base_img = current_normalized_sar.astype(np.uint8) + except Exception as e: + logging.error( + f"{log_prefix} Failed to convert input SAR to uint8: {e}. Aborting." + ) + return else: - base_img = current_normalized_sar.copy() - + base_img = current_normalized_sar.copy() try: # --- Processing Steps --- is_geo_valid = geo_info.get("valid", False) if geo_info else False - orient_rad = geo_info.get("orientation", 0.0) if is_geo_valid else 0.0 - - orient_rad = -orient_rad # invert angle + original_orient_rad = ( + geo_info.get("orientation", 0.0) if is_geo_valid else 0.0 + ) + + # Negate the angle for display purposes as requested. + orient_rad_for_display = -original_orient_rad + logging.debug( + f"{log_prefix} Original orientation: {math.degrees(original_orient_rad):.2f} deg. Using {-math.degrees(original_orient_rad):.2f} deg for display rotation." + ) # 1. Apply B/C LUT (from state) logging.debug(f"{log_prefix} Applying B/C LUT...") img = cv2.LUT(base_img, bc_lut) - if self._app_state.shutting_down: return # Check after each step + if self._app_state.shutting_down: + return # Check after each step # 2. Apply Color Palette (from state) if palette != "GRAY": logging.debug(f"{log_prefix} Applying color palette '{palette}'...") - img = apply_color_palette(img, palette) # Use imported function - if self._app_state.shutting_down: return + img = apply_color_palette(img, palette) # Use imported function + if self._app_state.shutting_down: + return else: logging.debug(f"{log_prefix} Skipping color palette (GRAY).") # 3. Apply Rotation - if is_geo_valid and abs(orient_rad) > 1e-4: - logging.debug(f"{log_prefix} Applying rotation (Angle: {math.degrees(orient_rad):.2f} deg)...") - img = self._rotate_image(img, orient_rad) # Use helper method - if img is None or self._app_state.shutting_down: return # Check helper result and shutdown + if is_geo_valid and abs(orient_rad_for_display) > 1e-4: + logging.debug( + f"{log_prefix} Applying rotation (Angle: {math.degrees(orient_rad_for_display):.2f} deg)..." + ) + img = self._rotate_image( + img, orient_rad_for_display + ) # Use helper method + if img is None or self._app_state.shutting_down: + return # Check helper result and shutdown else: - logging.debug(f"{log_prefix} Skipping rotation (Geo invalid or angle near zero).") - + logging.debug( + f"{log_prefix} Skipping rotation (Geo invalid or angle near zero)." + ) # 4. Resize Image logging.debug(f"{log_prefix} Resizing image to {target_w}x{target_h}...") - img = self._resize_sar_image(img, target_w, target_h) # Use helper method - if img is None or self._app_state.shutting_down: # Check helper result and shutdown - return + img = self._resize_sar_image(img, target_w, target_h) # Use helper method + if ( + img is None or self._app_state.shutting_down + ): # Check helper result and shutdown + return logging.debug(f"{log_prefix} Resize complete. Final shape: {img.shape}") # 5. Queue Final Image @@ -167,73 +195,80 @@ class ImagePipeline: logging.exception(f"{log_prefix} Error during SAR processing pipeline:") def _rotate_image(self, img: np.ndarray, angle_rad: float) -> Optional[np.ndarray]: - """ - Helper method to rotate an image using OpenCV. + """ + Helper method to rotate an image using OpenCV. - Args: - img (np.ndarray): Input image. - angle_rad (float): Rotation angle in radians. - - Returns: - Optional[np.ndarray]: The rotated image, or None on critical error. - """ - log_prefix = f"{self._log_prefix} SAR Rotate Helper" - try: - deg = math.degrees(angle_rad) - h, w = img.shape[:2] - center = (w // 2, h // 2) - # Get rotation matrix - M = cv2.getRotationMatrix2D(center, deg, 1.0) - # Determine border color (black for BGR, 0 for grayscale) - border_color = [0,0,0] if img.ndim == 3 else 0 - # Perform affine warp - rotated_img = cv2.warpAffine( - img, - M, - (w, h), # Output size same as input - flags=cv2.INTER_LINEAR, # Linear interpolation - borderMode=cv2.BORDER_CONSTANT, - borderValue=border_color # Fill borders with black - ) - logging.debug(f"{log_prefix} Rotation successful.") - return rotated_img - except Exception as e: - # Log error and return None to indicate failure - logging.exception(f"{log_prefix} Rotation warpAffine error:") - return None - - def _resize_sar_image(self, img: np.ndarray, target_w: int, target_h: int) -> Optional[np.ndarray]: - """ - Helper method to resize SAR image using the utility function. - - Args: + Args: img (np.ndarray): Input image. - target_w (int): Target width. - target_h (int): Target height. + angle_rad (float): Rotation angle in radians. - Returns: - Optional[np.ndarray]: The resized image, or None on error. - """ - log_prefix = f"{self._log_prefix} SAR Resize Helper" - # Basic validation - if img is None or target_w <= 0 or target_h <=0: - logging.error(f"{log_prefix} Invalid input for resize (Image None or invalid dims {target_w}x{target_h}).") - return None + Returns: + Optional[np.ndarray]: The rotated image, or None on critical error. + """ + log_prefix = f"{self._log_prefix} SAR Rotate Helper" + try: + deg = math.degrees(angle_rad) + h, w = img.shape[:2] + center = (w // 2, h // 2) + # Get rotation matrix + M = cv2.getRotationMatrix2D(center, deg, 1.0) + # Determine border color (black for BGR, 0 for grayscale) + border_color = [0, 0, 0] if img.ndim == 3 else 0 + # Perform affine warp + rotated_img = cv2.warpAffine( + img, + M, + (w, h), # Output size same as input + flags=cv2.INTER_LINEAR, # Linear interpolation + borderMode=cv2.BORDER_CONSTANT, + borderValue=border_color, # Fill borders with black + ) + logging.debug(f"{log_prefix} Rotation successful.") + return rotated_img + except Exception as e: + # Log error and return None to indicate failure + logging.exception(f"{log_prefix} Rotation warpAffine error:") + return None - # Check if resize is actually needed - if img.shape[1] == target_w and img.shape[0] == target_h: - logging.debug(f"{log_prefix} Image already target size. Skipping resize.") - return img + def _resize_sar_image( + self, img: np.ndarray, target_w: int, target_h: int + ) -> Optional[np.ndarray]: + """ + Helper method to resize SAR image using the utility function. - # Perform resize using the function from image_processing - resized_img = resize_image(img, target_w, target_h) # This function handles logging - if resized_img is None: - logging.error(f"{log_prefix} SAR Resize failed (resize_image returned None).") - return None # Propagate None on failure - else: - logging.debug(f"{log_prefix} Resize successful.") - return resized_img + Args: + img (np.ndarray): Input image. + target_w (int): Target width. + target_h (int): Target height. + Returns: + Optional[np.ndarray]: The resized image, or None on error. + """ + log_prefix = f"{self._log_prefix} SAR Resize Helper" + # Basic validation + if img is None or target_w <= 0 or target_h <= 0: + logging.error( + f"{log_prefix} Invalid input for resize (Image None or invalid dims {target_w}x{target_h})." + ) + return None + + # Check if resize is actually needed + if img.shape[1] == target_w and img.shape[0] == target_h: + logging.debug(f"{log_prefix} Image already target size. Skipping resize.") + return img + + # Perform resize using the function from image_processing + resized_img = resize_image( + img, target_w, target_h + ) # This function handles logging + if resized_img is None: + logging.error( + f"{log_prefix} SAR Resize failed (resize_image returned None)." + ) + return None # Propagate None on failure + else: + logging.debug(f"{log_prefix} Resize successful.") + return resized_img def process_mfd_for_display(self): """ @@ -258,7 +293,9 @@ class ImagePipeline: # Get MFD LUT from state mfd_lut = self._app_state.mfd_lut if mfd_lut is None: - logging.warning(f"{log_prefix} MFD LUT missing in AppState. Cannot process.") + logging.warning( + f"{log_prefix} MFD LUT missing in AppState. Cannot process." + ) return logging.debug(f"{log_prefix} Processing MFD indices from AppState...") @@ -266,37 +303,43 @@ class ImagePipeline: try: # --- Check shutdown before potentially heavy LUT application --- if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected before LUT application.") - return + logging.debug(f"{log_prefix} Shutdown detected before LUT application.") + return # --- Apply MFD LUT (from state) --- - logging.debug(f"{log_prefix} Applying MFD LUT (shape {mfd_lut.shape}) to indices (shape {current_mfd_indices.shape})...") + logging.debug( + f"{log_prefix} Applying MFD LUT (shape {mfd_lut.shape}) to indices (shape {current_mfd_indices.shape})..." + ) mfd_bgr = mfd_lut[current_mfd_indices] - logging.debug(f"{log_prefix} MFD LUT applied. Result shape: {mfd_bgr.shape}.") - + logging.debug( + f"{log_prefix} MFD LUT applied. Result shape: {mfd_bgr.shape}." + ) # --- Check shutdown again before queueing --- if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected after LUT application.") - return + logging.debug(f"{log_prefix} Shutdown detected after LUT application.") + return # --- Queue Result --- if mfd_bgr is not None: - # Use put_queue utility, passing the app instance for context - put_queue(self._mfd_queue, mfd_bgr.copy(), "mfd", self._app) - logging.debug(f"{log_prefix} Queued processed MFD image for display.") + # Use put_queue utility, passing the app instance for context + put_queue(self._mfd_queue, mfd_bgr.copy(), "mfd", self._app) + logging.debug(f"{log_prefix} Queued processed MFD image for display.") else: # This case should be rare if LUT is always valid and indices are valid - logging.error(f"{log_prefix} MFD BGR image is None after LUT application (unexpected).") + logging.error( + f"{log_prefix} MFD BGR image is None after LUT application (unexpected)." + ) except IndexError as e: # Handle index errors (e.g., index value > LUT size - 1) - min_idx, max_idx = "-","-" - try: # Safely try to get min/max for logging - if current_mfd_indices is not None: - min_idx = np.min(current_mfd_indices) - max_idx = np.max(current_mfd_indices) - except ValueError: pass # Ignore if array is empty etc. + min_idx, max_idx = "-", "-" + try: # Safely try to get min/max for logging + if current_mfd_indices is not None: + min_idx = np.min(current_mfd_indices) + max_idx = np.max(current_mfd_indices) + except ValueError: + pass # Ignore if array is empty etc. # Log error only if not shutting down if not self._app_state.shutting_down: logging.error( @@ -305,9 +348,9 @@ class ImagePipeline: f"LUT shape {mfd_lut.shape}" ) except Exception as e: - # Log other errors only if not shutting down - if not self._app_state.shutting_down: + # Log other errors only if not shutting down + if not self._app_state.shutting_down: logging.exception(f"{log_prefix} Error applying LUT to MFD indices:") -# --- END OF FILE image_pipeline.py --- \ No newline at end of file +# --- END OF FILE image_pipeline.py --- diff --git a/logging_config.py b/logging_config.py index 836aff1..12c0368 100644 --- a/logging_config.py +++ b/logging_config.py @@ -50,10 +50,10 @@ class DebugControlFilter(logging.Filter): # --- Filter DEBUG messages based on prefixes and config flags --- if record.levelno == logging.DEBUG: msg = record.getMessage() # Get the formatted message content - + # Check Map related prefixes first map_prefixes = ( - "[Map", # Cattura [MapTileManager], [MapIntegrationManager], [MapUtils], [MapService], [MapDisplay] + "[Map", # Cattura [MapTileManager], [MapIntegrationManager], [MapUtils], [MapService], [MapDisplay] # Aggiungi altri prefissi specifici se necessario (es. da helper interni) ) if msg.startswith(map_prefixes): @@ -250,7 +250,7 @@ def setup_logging(): f"Stat:{config.DEBUG_APP_STATUS}," f"Trig:{config.DEBUG_APP_TRIGGER}], " f"Disp:{config.DEBUG_DISPLAY_MANAGER}, " - f"Map:{config.DEBUG_MAP_DETAILS}, " + f"Map:{config.DEBUG_MAP_DETAILS}, " f"Util:{config.DEBUG_UTILS}, " f"Net:{config.DEBUG_NETWORK}, " f"ImgProc:{config.DEBUG_IMAGE_PROCESSING}" diff --git a/map_display.py b/map_display.py index 14b414c..e2b1c52 100644 --- a/map_display.py +++ b/map_display.py @@ -25,7 +25,7 @@ import config # For placeholder color class MapDisplayWindow: """Manages the OpenCV window used to display the map.""" - + MAX_DISPLAY_WIDTH = config.MAX_MAP_DISPLAY_WIDTH MAX_DISPLAY_HEIGHT = config.MAX_MAP_DISPLAY_HEIGHT @@ -66,22 +66,28 @@ class MapDisplayWindow: map_image_pil (Optional[Image.Image]): The map image (Pillow format) to display. If None, shows a placeholder/error image. """ - log_prefix = f"{self._log_prefix} ShowMap" # Specific prefix for this method + log_prefix = f"{self._log_prefix} ShowMap" # Specific prefix for this method # --- Logging Input --- if map_image_pil is None: - logging.warning(f"{log_prefix} Received None image payload.") + logging.warning(f"{log_prefix} Received None image payload.") elif Image is not None and isinstance(map_image_pil, Image.Image): - logging.debug(f"{log_prefix} Received PIL Image payload (Size: {map_image_pil.size}, Mode: {map_image_pil.mode}).") + logging.debug( + f"{log_prefix} Received PIL Image payload (Size: {map_image_pil.size}, Mode: {map_image_pil.mode})." + ) else: - # Log unexpected payload types - logging.error(f"{log_prefix} Received unexpected payload type: {type(map_image_pil)}. Cannot display.") - # Attempt to show placeholder instead of crashing - map_image_pil = None # Force placeholder generation + # Log unexpected payload types + logging.error( + f"{log_prefix} Received unexpected payload type: {type(map_image_pil)}. Cannot display." + ) + # Attempt to show placeholder instead of crashing + map_image_pil = None # Force placeholder generation # Check for Pillow dependency again, crucial for processing if Image is None: - logging.error(f"{log_prefix} Cannot process map: Pillow library not loaded.") + logging.error( + f"{log_prefix} Cannot process map: Pillow library not loaded." + ) # Maybe display a permanent error in the window if possible? Difficult without cv2. return @@ -93,12 +99,19 @@ class MapDisplayWindow: try: # Use fixed size for placeholder for simplicity, or config value? placeholder_size = (512, 512) - placeholder_color_rgb = getattr(config, "OFFLINE_MAP_PLACEHOLDER_COLOR", (200, 200, 200)) + placeholder_color_rgb = getattr( + config, "OFFLINE_MAP_PLACEHOLDER_COLOR", (200, 200, 200) + ) # Ensure color is valid tuple - if not (isinstance(placeholder_color_rgb, tuple) and len(placeholder_color_rgb) == 3): - placeholder_color_rgb = (200, 200, 200) # Fallback grey + if not ( + isinstance(placeholder_color_rgb, tuple) + and len(placeholder_color_rgb) == 3 + ): + placeholder_color_rgb = (200, 200, 200) # Fallback grey - placeholder_pil = Image.new("RGB", placeholder_size, color=placeholder_color_rgb) + placeholder_pil = Image.new( + "RGB", placeholder_size, color=placeholder_color_rgb + ) # Add text indication? Requires Pillow draw, adds complexity. Keep simple for now. # from PIL import ImageDraw # draw = ImageDraw.Draw(placeholder_pil) @@ -107,15 +120,23 @@ class MapDisplayWindow: # Convert placeholder PIL to NumPy BGR immediately placeholder_np = np.array(placeholder_pil) map_to_display_bgr = cv2.cvtColor(placeholder_np, cv2.COLOR_RGB2BGR) - logging.debug(f"{log_prefix} Placeholder generated (BGR Shape: {map_to_display_bgr.shape}).") + logging.debug( + f"{log_prefix} Placeholder generated (BGR Shape: {map_to_display_bgr.shape})." + ) except Exception as ph_err: - logging.exception(f"{log_prefix} Failed to create or convert placeholder image:") + logging.exception( + f"{log_prefix} Failed to create or convert placeholder image:" + ) # If even placeholder fails, create a very basic numpy array as last resort - map_to_display_bgr = np.full((256, 256, 3), 60, dtype=np.uint8) # Dark grey small square - logging.error(f"{log_prefix} Using minimal NumPy array as placeholder fallback.") + map_to_display_bgr = np.full( + (256, 256, 3), 60, dtype=np.uint8 + ) # Dark grey small square + logging.error( + f"{log_prefix} Using minimal NumPy array as placeholder fallback." + ) # --- Convert Valid PIL Image Payload to NumPy BGR --- - if map_to_display_bgr is None: # Only if placeholder wasn't created above + if map_to_display_bgr is None: # Only if placeholder wasn't created above try: # Convert PIL Image (expected RGB) to NumPy array map_image_np = np.array(map_image_pil) @@ -123,69 +144,99 @@ class MapDisplayWindow: if map_image_np.ndim == 2: # Convert grayscale numpy to BGR map_to_display_bgr = cv2.cvtColor(map_image_np, cv2.COLOR_GRAY2BGR) - logging.debug(f"{log_prefix} Converted Grayscale PIL to NumPy BGR (Shape: {map_to_display_bgr.shape}).") + logging.debug( + f"{log_prefix} Converted Grayscale PIL to NumPy BGR (Shape: {map_to_display_bgr.shape})." + ) elif map_image_np.ndim == 3 and map_image_np.shape[2] == 3: # Convert RGB (from Pillow) to BGR (for OpenCV) map_to_display_bgr = cv2.cvtColor(map_image_np, cv2.COLOR_RGB2BGR) - logging.debug(f"{log_prefix} Converted RGB PIL to NumPy BGR (Shape: {map_to_display_bgr.shape}).") + logging.debug( + f"{log_prefix} Converted RGB PIL to NumPy BGR (Shape: {map_to_display_bgr.shape})." + ) elif map_image_np.ndim == 3 and map_image_np.shape[2] == 4: - # Convert RGBA (from Pillow) to BGR (for OpenCV), discarding alpha + # Convert RGBA (from Pillow) to BGR (for OpenCV), discarding alpha map_to_display_bgr = cv2.cvtColor(map_image_np, cv2.COLOR_RGBA2BGR) - logging.debug(f"{log_prefix} Converted RGBA PIL to NumPy BGR (Shape: {map_to_display_bgr.shape}).") + logging.debug( + f"{log_prefix} Converted RGBA PIL to NumPy BGR (Shape: {map_to_display_bgr.shape})." + ) else: - raise ValueError(f"Unsupported NumPy array shape after PIL conversion: {map_image_np.shape}") + raise ValueError( + f"Unsupported NumPy array shape after PIL conversion: {map_image_np.shape}" + ) except Exception as e: - logging.exception(f"{log_prefix} Error converting received PIL image to OpenCV BGR format:") + logging.exception( + f"{log_prefix} Error converting received PIL image to OpenCV BGR format:" + ) # Fallback to basic numpy array on conversion error map_to_display_bgr = np.full((256, 256, 3), 60, dtype=np.uint8) - logging.error(f"{log_prefix} Using minimal NumPy array as fallback due to conversion error.") + logging.error( + f"{log_prefix} Using minimal NumPy array as fallback due to conversion error." + ) # --- Resize Image if Exceeds Max Dimensions --- try: img_h, img_w = map_to_display_bgr.shape[:2] if img_h > self.MAX_DISPLAY_HEIGHT or img_w > self.MAX_DISPLAY_WIDTH: - logging.debug(f"{log_prefix} Image ({img_w}x{img_h}) exceeds max size ({self.MAX_DISPLAY_WIDTH}x{self.MAX_DISPLAY_HEIGHT}). Resizing...") + logging.debug( + f"{log_prefix} Image ({img_w}x{img_h}) exceeds max size ({self.MAX_DISPLAY_WIDTH}x{self.MAX_DISPLAY_HEIGHT}). Resizing..." + ) # Calculate aspect ratio - ratio = min(self.MAX_DISPLAY_WIDTH / img_w, self.MAX_DISPLAY_HEIGHT / img_h) + ratio = min( + self.MAX_DISPLAY_WIDTH / img_w, self.MAX_DISPLAY_HEIGHT / img_h + ) new_w = int(img_w * ratio) new_h = int(img_h * ratio) # Resize using OpenCV - INTER_AREA is generally good for downscaling - map_to_display_bgr = cv2.resize(map_to_display_bgr, (new_w, new_h), interpolation=cv2.INTER_AREA) + map_to_display_bgr = cv2.resize( + map_to_display_bgr, (new_w, new_h), interpolation=cv2.INTER_AREA + ) logging.debug(f"{log_prefix} Resized map image to {new_w}x{new_h}.") else: - logging.debug(f"{log_prefix} Image size ({img_w}x{img_h}) is within limits. No resize needed.") + logging.debug( + f"{log_prefix} Image size ({img_w}x{img_h}) is within limits. No resize needed." + ) except Exception as resize_err: - logging.exception(f"{log_prefix} Error during map image resizing:") - # Continue with the unresized image if resize fails + logging.exception(f"{log_prefix} Error during map image resizing:") + # Continue with the unresized image if resize fails # --- Display using OpenCV --- try: # Log the final shape before showing final_shape = map_to_display_bgr.shape - logging.debug(f"{log_prefix} Attempting cv2.imshow with final image shape: {final_shape}") + logging.debug( + f"{log_prefix} Attempting cv2.imshow with final image shape: {final_shape}" + ) - new_shape = final_shape[:2] # (height, width) + new_shape = final_shape[:2] # (height, width) # Create and move window only once or if shape changes drastically if not self.window_initialized or new_shape != self._current_shape: - logging.debug(f"{log_prefix} First show or shape change for '{self.window_name}'. Creating/moving window.") + logging.debug( + f"{log_prefix} First show or shape change for '{self.window_name}'. Creating/moving window." + ) cv2.imshow(self.window_name, map_to_display_bgr) try: cv2.moveWindow(self.window_name, self.x_pos, self.y_pos) self.window_initialized = True self._current_shape = new_shape - logging.info(f"{log_prefix} Window '{self.window_name}' shown/moved to ({self.x_pos}, {self.y_pos}).") + logging.info( + f"{log_prefix} Window '{self.window_name}' shown/moved to ({self.x_pos}, {self.y_pos})." + ) # Allow window to draw/position - waitKey might be handled by caller loop # cv2.waitKey(1) except cv2.error as move_e: - logging.warning(f"{log_prefix} Could not move '{self.window_name}' window: {move_e}.") - self.window_initialized = True # Assume imshow worked - self._current_shape = new_shape # Update shape even if move failed + logging.warning( + f"{log_prefix} Could not move '{self.window_name}' window: {move_e}." + ) + self.window_initialized = True # Assume imshow worked + self._current_shape = new_shape # Update shape even if move failed else: # Just update the image content if window exists and shape is same - logging.debug(f"{log_prefix} Updating existing window '{self.window_name}' content.") + logging.debug( + f"{log_prefix} Updating existing window '{self.window_name}' content." + ) cv2.imshow(self.window_name, map_to_display_bgr) # Essential waitKey to process OpenCV events if called outside main loop, @@ -195,30 +246,46 @@ class MapDisplayWindow: except cv2.error as e: # Handle OpenCV errors (e.g., window closed manually) if "NULL window" in str(e) or "invalid window" in str(e): - logging.warning(f"{log_prefix} OpenCV window '{self.window_name}' seems closed. Will re-initialize on next valid image.") - self.window_initialized = False # Reset flag + logging.warning( + f"{log_prefix} OpenCV window '{self.window_name}' seems closed. Will re-initialize on next valid image." + ) + self.window_initialized = False # Reset flag else: # Log other OpenCV errors during display - logging.exception(f"{log_prefix} OpenCV error during final map display (imshow): {e}") + logging.exception( + f"{log_prefix} OpenCV error during final map display (imshow): {e}" + ) except Exception as e: # Log other unexpected errors during display - logging.exception(f"{log_prefix} Unexpected error displaying final map image: {e}") + logging.exception( + f"{log_prefix} Unexpected error displaying final map image: {e}" + ) # --- destroy_window method remains the same --- def destroy_window(self): """Explicitly destroys the managed OpenCV window.""" - logging.info(f"{self._log_prefix} Attempting to destroy window: '{self.window_name}'") + logging.info( + f"{self._log_prefix} Attempting to destroy window: '{self.window_name}'" + ) if self.window_initialized: try: cv2.destroyWindow(self.window_name) self.window_initialized = False - logging.info(f"{self._log_prefix} Window '{self.window_name}' destroyed successfully.") + logging.info( + f"{self._log_prefix} Window '{self.window_name}' destroyed successfully." + ) except cv2.error as e: - logging.warning(f"{self._log_prefix} Ignoring error destroying window '{self.window_name}' (may already be closed): {e}") + logging.warning( + f"{self._log_prefix} Ignoring error destroying window '{self.window_name}' (may already be closed): {e}" + ) except Exception as e: - logging.exception(f"{self._log_prefix} Unexpected error destroying window '{self.window_name}': {e}") + logging.exception( + f"{self._log_prefix} Unexpected error destroying window '{self.window_name}': {e}" + ) else: - logging.debug(f"{self._log_prefix} Window '{self.window_name}' was not initialized or already destroyed.") + logging.debug( + f"{self._log_prefix} Window '{self.window_name}' was not initialized or already destroyed." + ) # --- END OF FILE map_display.py --- diff --git a/map_integration.py b/map_integration.py index d32ee9c..26079f7 100644 --- a/map_integration.py +++ b/map_integration.py @@ -12,16 +12,17 @@ Acts as an intermediary between the main application and map-specific modules. # Standard library imports import logging import threading -import queue # For type hinting +import queue # For type hinting import math from typing import Optional, Dict, Any, Tuple, List # Third-party imports import numpy as np + try: from PIL import Image except ImportError: - Image = None # Handled in dependent modules, but check here too + Image = None # Handled in dependent modules, but check here too try: import mercantile except ImportError: @@ -30,13 +31,14 @@ try: import pyproj except ImportError: pyproj = None - + import cv2 # Local application imports import config from app_state import AppState from utils import put_queue + # Map specific modules that this manager orchestrates from map_services import get_map_service, BaseMapService from map_manager import MapTileManager @@ -44,12 +46,13 @@ from map_utils import ( get_bounding_box_from_center_size, get_tile_ranges_for_bbox, MapCalculationError, - calculate_meters_per_pixel + calculate_meters_per_pixel, ) from map_display import MapDisplayWindow # Forward declaration for type hinting App instance from typing import TYPE_CHECKING + if TYPE_CHECKING: from app import App @@ -61,7 +64,7 @@ class MapIntegrationManager: self, app_state: AppState, tkinter_queue: queue.Queue, - app: 'App', + app: "App", map_x: int, map_y: int, ): @@ -74,19 +77,22 @@ class MapIntegrationManager: self._app_state: AppState = app_state self._tkinter_queue: queue.Queue = tkinter_queue - self._app: 'App' = app + self._app: "App" = app # --- Dependency Checks --- - if Image is None: raise ImportError("Pillow library not found") - if mercantile is None: raise ImportError("mercantile library not found") - if pyproj is None: raise ImportError("pyproj library not found") + if Image is None: + raise ImportError("Pillow library not found") + if mercantile is None: + raise ImportError("mercantile library not found") + if pyproj is None: + raise ImportError("pyproj library not found") # --- Initialize Attributes --- self._map_service: Optional[BaseMapService] = None self._map_tile_manager: Optional[MapTileManager] = None self._map_display_window: Optional[MapDisplayWindow] = None self._map_initial_display_thread: Optional[threading.Thread] = None - self._geod: Optional[pyproj.Geod] = None # Initialize as None first + self._geod: Optional[pyproj.Geod] = None # Initialize as None first try: # --- Geodetic Calculator Initialization --- @@ -96,18 +102,22 @@ class MapIntegrationManager: # --- Initialize Other Map Components --- # 1. Get Map Service - service_name = getattr(config, 'MAP_SERVICE_PROVIDER', 'osm') - api_key = getattr(config, 'MAP_API_KEY', None) + service_name = getattr(config, "MAP_SERVICE_PROVIDER", "osm") + api_key = getattr(config, "MAP_API_KEY", None) self._map_service = get_map_service(service_name, api_key) if not self._map_service: raise ValueError(f"Failed to get map service '{service_name}'.") - logging.debug(f"{self._log_prefix} Map service '{self._map_service.name}' loaded.") + logging.debug( + f"{self._log_prefix} Map service '{self._map_service.name}' loaded." + ) # 2. Create Tile Manager self._map_tile_manager = MapTileManager( map_service=self._map_service, - cache_base_dir=getattr(config, 'MAP_CACHE_DIRECTORY', None), - enable_online_fetching=getattr(config, 'ENABLE_ONLINE_MAP_FETCHING', None) + cache_base_dir=getattr(config, "MAP_CACHE_DIRECTORY", None), + enable_online_fetching=getattr( + config, "ENABLE_ONLINE_MAP_FETCHING", None + ), ) logging.debug(f"{self._log_prefix} MapTileManager created.") @@ -115,140 +125,171 @@ class MapIntegrationManager: self._map_display_window = MapDisplayWindow( window_name="Map Overlay", x_pos=map_x, y_pos=map_y ) - logging.debug(f"{self._log_prefix} MapDisplayWindow created at ({map_x},{map_y}).") + logging.debug( + f"{self._log_prefix} MapDisplayWindow created at ({map_x},{map_y})." + ) # 4. Trigger Initial Map Display in Background Thread logging.debug(f"{self._log_prefix} Starting initial map display thread...") # Set status before starting thread self._app.set_status("Loading initial map...") self._map_initial_display_thread = threading.Thread( - target=self._display_initial_map_area_thread, - name="InitialMapDisplayThread", - daemon=True + target=self._display_initial_map_area_thread, + name="InitialMapDisplayThread", + daemon=True, ) self._map_initial_display_thread.start() - except (ImportError, ValueError, pyproj.exceptions.CRSError) as init_err: # Catch pyproj errors too - logging.critical(f"{self._log_prefix} Initialization failed: {init_err}") - # Ensure components are None if init fails midway - self._geod = None - self._map_service = None - self._map_tile_manager = None - self._map_display_window = None - raise # Re-raise critical errors + except ( + ImportError, + ValueError, + pyproj.exceptions.CRSError, + ) as init_err: # Catch pyproj errors too + logging.critical(f"{self._log_prefix} Initialization failed: {init_err}") + # Ensure components are None if init fails midway + self._geod = None + self._map_service = None + self._map_tile_manager = None + self._map_display_window = None + raise # Re-raise critical errors except Exception as e: - logging.exception(f"{self._log_prefix} Unexpected error during initialization:") - self._geod = None - self._map_service = None - self._map_tile_manager = None - self._map_display_window = None - raise # Re-raise other unexpected errors # Re-raise other unexpected errors - + logging.exception( + f"{self._log_prefix} Unexpected error during initialization:" + ) + self._geod = None + self._map_service = None + self._map_tile_manager = None + self._map_display_window = None + raise # Re-raise other unexpected errors # Re-raise other unexpected errors def _display_initial_map_area_thread(self): - """ - (Runs in background thread) Calculates the initial map area based on default - config settings and queues the result for display on the main thread, - *unless* the default coordinates in config are set to (0,0) which signals - to skip the initial display. - """ - log_prefix = f"{self._log_prefix} InitialMap" + """ + (Runs in background thread) Calculates the initial map area based on default + config settings and queues the result for display on the main thread, + *unless* the default coordinates in config are set to (0,0) which signals + to skip the initial display. + """ + log_prefix = f"{self._log_prefix} InitialMap" - # Check if default lat/lon are set to 0.0 to prevent initial display - if config.SAR_CENTER_LAT == 0.0 and config.SAR_CENTER_LON == 0.0: - # ... (codice per saltare e aggiornare lo stato, come prima) ... - # ... (assicurati che questa parte sia corretta come nella risposta precedente) ... - logging.debug(f"{log_prefix} Initial map display skipped based on config defaults (0,0). Waiting for valid GeoInfo.") - if not self._app_state.shutting_down: - status_msg = "Status Unavailable" # Default - try: - if self._app_state.test_mode_active: - status_msg = "Ready (Test Mode)" - elif config.USE_LOCAL_IMAGES: - status_msg = "Ready (Local Mode)" - else: - socket_ok = False - listening_info = "Error: No Network Socket" - if hasattr(self._app, 'udp_socket') and self._app.udp_socket: - if hasattr(self._app, 'local_ip') and hasattr(self._app, 'local_port'): - listening_info = f"Listening UDP {self._app.local_ip}:{self._app.local_port}" - socket_ok = True - else: - listening_info = "Listening UDP (IP/Port Unknown)" - socket_ok = True - status_msg = listening_info - status_msg += " | Map Ready (Waiting for GeoData)" - except Exception as e: - logging.exception(f"{log_prefix} Unexpected error determining status message:") - status_msg = "Error Getting Status | Map Ready (Waiting for GeoData)" - self._app.set_status(status_msg) - return # Esce dal thread + # Check if default lat/lon are set to 0.0 to prevent initial display + if config.SAR_CENTER_LAT == 0.0 and config.SAR_CENTER_LON == 0.0: + # ... (codice per saltare e aggiornare lo stato, come prima) ... + # ... (assicurati che questa parte sia corretta come nella risposta precedente) ... + logging.debug( + f"{log_prefix} Initial map display skipped based on config defaults (0,0). Waiting for valid GeoInfo." + ) + if not self._app_state.shutting_down: + status_msg = "Status Unavailable" # Default + try: + if self._app_state.test_mode_active: + status_msg = "Ready (Test Mode)" + elif config.USE_LOCAL_IMAGES: + status_msg = "Ready (Local Mode)" + else: + socket_ok = False + listening_info = "Error: No Network Socket" + if hasattr(self._app, "udp_socket") and self._app.udp_socket: + if hasattr(self._app, "local_ip") and hasattr( + self._app, "local_port" + ): + listening_info = f"Listening UDP {self._app.local_ip}:{self._app.local_port}" + socket_ok = True + else: + listening_info = "Listening UDP (IP/Port Unknown)" + socket_ok = True + status_msg = listening_info + status_msg += " | Map Ready (Waiting for GeoData)" + except Exception as e: + logging.exception( + f"{log_prefix} Unexpected error determining status message:" + ) + status_msg = ( + "Error Getting Status | Map Ready (Waiting for GeoData)" + ) + self._app.set_status(status_msg) + return # Esce dal thread - # Se le coordinate di default *non* sono (0,0), procedi - logging.debug(f"{log_prefix} Calculating initial map area based on non-zero config defaults...") + # Se le coordinate di default *non* sono (0,0), procedi + logging.debug( + f"{log_prefix} Calculating initial map area based on non-zero config defaults..." + ) + + # Check dependencies + if not (self._map_tile_manager and self._map_display_window): + logging.error( + f"{log_prefix} Map components not initialized. Aborting thread." + ) + put_queue(self._tkinter_queue, ("SHOW_MAP", None), "tkinter", self._app) + return + if self._app_state.shutting_down: + logging.debug(f"{log_prefix} Shutdown detected. Aborting.") + return + + map_image_pil: Optional[Image.Image] = None + try: + # --- MODIFICA QUI: Definisci 'zoom' PRIMA di usarlo --- + zoom = config.DEFAULT_MAP_ZOOM_LEVEL + logging.debug(f"{log_prefix} Using default zoom level: {zoom}") + # --- FINE MODIFICA --- + + # Usa default center/size da config + bbox = get_bounding_box_from_center_size( + config.SAR_CENTER_LAT, config.SAR_CENTER_LON, config.SAR_IMAGE_SIZE_KM + ) + if bbox is None: + raise MapCalculationError("Failed to calculate initial bounding box.") + + # Calcola i tile ranges USANDO la variabile zoom definita sopra + tile_ranges = get_tile_ranges_for_bbox(bbox, zoom) + if tile_ranges is None: + raise MapCalculationError("Failed to calculate initial tile ranges.") - # Check dependencies - if not (self._map_tile_manager and self._map_display_window): - logging.error(f"{log_prefix} Map components not initialized. Aborting thread.") - put_queue(self._tkinter_queue, ('SHOW_MAP', None), "tkinter", self._app) - return if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected. Aborting.") - return + logging.debug(f"{log_prefix} Shutdown detected before stitching.") + return - map_image_pil: Optional[Image.Image] = None - try: - # --- MODIFICA QUI: Definisci 'zoom' PRIMA di usarlo --- - zoom = config.DEFAULT_MAP_ZOOM_LEVEL - logging.debug(f"{log_prefix} Using default zoom level: {zoom}") - # --- FINE MODIFICA --- + # Ora puoi usare 'zoom' nel messaggio didebug(f"{log_prefix} Stitching initial map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})...") + map_image_pil = self._map_tile_manager.stitch_map_image( + zoom, tile_ranges[0], tile_ranges[1] + ) - # Usa default center/size da config - bbox = get_bounding_box_from_center_size( - config.SAR_CENTER_LAT, config.SAR_CENTER_LON, config.SAR_IMAGE_SIZE_KM + if self._app_state.shutting_down: + logging.debug(f"{log_prefix} Shutdown detected after stitching.") + return + + if map_image_pil: + logging.debug(f"{log_prefix} Initial map area stitched successfully.") + center_lat_deg = ( + config.SAR_CENTER_LAT + ) # Usa il default per la mappa iniziale + map_image_pil = self._draw_scale_bar( + map_image_pil, center_lat_deg, zoom ) - if bbox is None: - raise MapCalculationError("Failed to calculate initial bounding box.") + else: + logging.error(f"{log_prefix} Failed to stitch initial map area.") - # Calcola i tile ranges USANDO la variabile zoom definita sopra - tile_ranges = get_tile_ranges_for_bbox(bbox, zoom) - if tile_ranges is None: - raise MapCalculationError("Failed to calculate initial tile ranges.") - - if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected before stitching.") - return - - # Ora puoi usare 'zoom' nel messaggio didebug(f"{log_prefix} Stitching initial map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})...") - map_image_pil = self._map_tile_manager.stitch_map_image( - zoom, tile_ranges[0], tile_ranges[1] + except (ImportError, MapCalculationError) as e: + logging.error(f"{log_prefix} Calculation error: {e}") + map_image_pil = None + except Exception as e: + logging.exception(f"{log_prefix} Unexpected error calculating initial map:") + map_image_pil = None + finally: + if not self._app_state.shutting_down: + logging.debug( + f"{log_prefix} Queueing SHOW_MAP command for initial map." ) + put_queue( + self._tkinter_queue, + ("SHOW_MAP", map_image_pil), + "tkinter", + self._app, + ) + logging.debug(f"{log_prefix} Initial map display thread finished.") - if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected after stitching.") - return - - if map_image_pil: - logging.debug(f"{log_prefix} Initial map area stitched successfully.") - center_lat_deg = config.SAR_CENTER_LAT # Usa il default per la mappa iniziale - map_image_pil = self._draw_scale_bar(map_image_pil, center_lat_deg, zoom) - else: - logging.error(f"{log_prefix} Failed to stitch initial map area.") - - except (ImportError, MapCalculationError) as e: - logging.error(f"{log_prefix} Calculation error: {e}") - map_image_pil = None - except Exception as e: - logging.exception(f"{log_prefix} Unexpected error calculating initial map:") - map_image_pil = None - finally: - if not self._app_state.shutting_down: - logging.debug(f"{log_prefix} Queueing SHOW_MAP command for initial map.") - put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_pil), "tkinter", self._app) - logging.debug(f"{log_prefix} Initial map display thread finished.") - - - def update_map_overlay(self, sar_normalized_uint8: np.ndarray, geo_info_radians: Dict[str, Any]): + def update_map_overlay( + self, sar_normalized_uint8: np.ndarray, geo_info_radians: Dict[str, Any] + ): """ Calculates the map overlay based on current SAR data. Currently fetches map tiles and draws the SAR bounding box. @@ -270,13 +311,19 @@ class MapIntegrationManager: # +++ DETAILED COMPONENT CHECK +++ if not self._map_tile_manager: - logging.warning(f"{log_prefix} Skipping update: _map_tile_manager is not available (None or evaluates False).") + logging.warning( + f"{log_prefix} Skipping update: _map_tile_manager is not available (None or evaluates False)." + ) return if not self._map_display_window: - logging.warning(f"{log_prefix} Skipping update: _map_display_window is not available (None or evaluates False).") + logging.warning( + f"{log_prefix} Skipping update: _map_display_window is not available (None or evaluates False)." + ) return if not self._geod: - logging.warning(f"{log_prefix} Skipping update: _geod (geodetic calculator) is not available (None or evaluates False).") + logging.warning( + f"{log_prefix} Skipping update: _geod (geodetic calculator) is not available (None or evaluates False)." + ) return # +++ END DETAILED CHECK +++ @@ -286,40 +333,50 @@ class MapIntegrationManager: return # Check libraries (redundant if init succeeded, but safe) if Image is None or mercantile is None or pyproj is None: - logging.error(f"{log_prefix} Skipping update: Missing required map libraries.") + logging.error( + f"{log_prefix} Skipping update: Missing required map libraries." + ) return # Log start of calculation - logging.debug(f"{log_prefix} Starting map overlay calculation (Phase 1: BBox)...") + logging.debug( + f"{log_prefix} Starting map overlay calculation (Phase 1: BBox)..." + ) # Initialize variables to store map images map_image_with_overlay: Optional[Image.Image] = None - stitched_map_image: Optional[Image.Image] = None # Store the base map + stitched_map_image: Optional[Image.Image] = None # Store the base map try: # --- Calculate SAR Footprint Parameters --- # Extract center coordinates (convert back to degrees for utility functions) - center_lat_deg = math.degrees(geo_info_radians.get('lat', 0.0)) - center_lon_deg = math.degrees(geo_info_radians.get('lon', 0.0)) + center_lat_deg = math.degrees(geo_info_radians.get("lat", 0.0)) + center_lon_deg = math.degrees(geo_info_radians.get("lon", 0.0)) # Extract scale and dimensions to calculate size - scale_x = geo_info_radians.get('scale_x', 0.0) - width_px = geo_info_radians.get('width_px', 0) + scale_x = geo_info_radians.get("scale_x", 0.0) + width_px = geo_info_radians.get("width_px", 0) # Calculate size in KM, using default from config as fallback if scale_x > 0 and width_px > 0: - size_km = (scale_x * width_px) / 1000.0 - logging.debug(f"{log_prefix} Calculated approximate size based on scale_x * width_px: {size_km:.2f} km") + size_km = (scale_x * width_px) / 1000.0 + logging.debug( + f"{log_prefix} Calculated approximate size based on scale_x * width_px: {size_km:.2f} km" + ) else: - logging.error( - f"{log_prefix} Invalid scale_x ({scale_x}) or width_px ({width_px}) in received GeoInfo. " - f"Cannot determine map size from data. Using fallback default size: {config.SAR_IMAGE_SIZE_KM} km." - ) - size_km = config.SAR_IMAGE_SIZE_KM + logging.error( + f"{log_prefix} Invalid scale_x ({scale_x}) or width_px ({width_px}) in received GeoInfo. " + f"Cannot determine map size from data. Using fallback default size: {config.SAR_IMAGE_SIZE_KM} km." + ) + size_km = config.SAR_IMAGE_SIZE_KM # Get zoom level from config zoom = config.DEFAULT_MAP_ZOOM_LEVEL # --- Fetch and Stitch Base Map --- # 1. Calculate Geographic Bounding Box for fetching tiles - logging.debug(f"{log_prefix} Calculating map tile BBox for center ({center_lat_deg:.4f},{center_lon_deg:.4f}), size {size_km*1.2:.1f}km.") - fetch_bbox = get_bounding_box_from_center_size(center_lat_deg, center_lon_deg, size_km * 1.2) + logging.debug( + f"{log_prefix} Calculating map tile BBox for center ({center_lat_deg:.4f},{center_lon_deg:.4f}), size {size_km*1.2:.1f}km." + ) + fetch_bbox = get_bounding_box_from_center_size( + center_lat_deg, center_lon_deg, size_km * 1.2 + ) if fetch_bbox is None: raise MapCalculationError("Tile Bounding Box calculation failed.") @@ -331,77 +388,124 @@ class MapIntegrationManager: # --- Check shutdown flag before potentially long operation --- if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected before stitching base map.") - return + logging.debug( + f"{log_prefix} Shutdown detected before stitching base map." + ) + return # 3. Stitch Background Map Image using MapTileManager - logging.debug(f"{log_prefix} Stitching base map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})...") - stitched_map_image = self._map_tile_manager.stitch_map_image(zoom, tile_ranges[0], tile_ranges[1]) + logging.debug( + f"{log_prefix} Stitching base map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})..." + ) + stitched_map_image = self._map_tile_manager.stitch_map_image( + zoom, tile_ranges[0], tile_ranges[1] + ) # --- Validate Stitched Image and Log --- if stitched_map_image is None: - logging.error(f"{log_prefix} MapTileManager.stitch_map_image returned None. Cannot proceed.") - map_image_with_overlay = None - raise MapCalculationError("Failed to stitch base map image.") + logging.error( + f"{log_prefix} MapTileManager.stitch_map_image returned None. Cannot proceed." + ) + map_image_with_overlay = None + raise MapCalculationError("Failed to stitch base map image.") else: - logging.debug(f"{log_prefix} Base map stitched successfully (PIL Size: {stitched_map_image.size}, Mode: {stitched_map_image.mode}).") - map_image_with_overlay = stitched_map_image.copy() + logging.debug( + f"{log_prefix} Base map stitched successfully (PIL Size: {stitched_map_image.size}, Mode: {stitched_map_image.mode})." + ) + map_image_with_overlay = stitched_map_image.copy() # --- Check shutdown flag after stitching --- if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected after stitching base map.") - return + logging.debug( + f"{log_prefix} Shutdown detected after stitching base map." + ) + return # --- Calculate and Draw SAR Bounding Box --- # 4. Calculate Geographic Coordinates of SAR Corners - logging.debug(f"{log_prefix} Calculating SAR corner geographic coordinates...") + logging.debug( + f"{log_prefix} Calculating SAR corner geographic coordinates..." + ) sar_corners_deg = self._calculate_sar_corners_geo(geo_info_radians) if sar_corners_deg is None: - raise MapCalculationError("SAR corner geographic coordinate calculation failed.") + raise MapCalculationError( + "SAR corner geographic coordinate calculation failed." + ) # 5. Convert SAR Corner Geographic Coords to Pixel Coords on Stitched Map - logging.debug(f"{log_prefix} Converting SAR corners to map pixel coordinates...") - top_left_tile = mercantile.Tile(x=tile_ranges[0][0], y=tile_ranges[1][0], z=zoom) + logging.debug( + f"{log_prefix} Converting SAR corners to map pixel coordinates..." + ) + top_left_tile = mercantile.Tile( + x=tile_ranges[0][0], y=tile_ranges[1][0], z=zoom + ) map_display_bounds = mercantile.bounds(top_left_tile) sar_corners_pixels = self._geo_coords_to_map_pixels( coords_deg=sar_corners_deg, map_bounds=map_display_bounds, map_tile_ranges=tile_ranges, zoom=zoom, - stitched_map_shape=map_image_with_overlay.size[::-1] + stitched_map_shape=map_image_with_overlay.size[::-1], ) if sar_corners_pixels is None: raise MapCalculationError("SAR corner to map pixel conversion failed.") # 6. Draw the SAR Bounding Box Polygon on the map image copy - logging.debug(f"{log_prefix} Drawing SAR bounding box polygon on map image...") + logging.debug( + f"{log_prefix} Drawing SAR bounding box polygon on map image..." + ) try: - map_cv = cv2.cvtColor(np.array(map_image_with_overlay), cv2.COLOR_RGB2BGR) + map_cv = cv2.cvtColor( + np.array(map_image_with_overlay), cv2.COLOR_RGB2BGR + ) pts = np.array(sar_corners_pixels, np.int32).reshape((-1, 1, 2)) - cv2.polylines(map_cv, [pts], isClosed=True, color=(0, 0, 255), thickness=2) - map_image_with_overlay = Image.fromarray(cv2.cvtColor(map_cv, cv2.COLOR_BGR2RGB)) - logging.debug(f"{log_prefix} SAR bounding box drawn successfully on map.") + cv2.polylines( + map_cv, [pts], isClosed=True, color=(0, 0, 255), thickness=2 + ) + map_image_with_overlay = Image.fromarray( + cv2.cvtColor(map_cv, cv2.COLOR_BGR2RGB) + ) + logging.debug( + f"{log_prefix} SAR bounding box drawn successfully on map." + ) except Exception as draw_err: - logging.exception(f"{log_prefix} Error drawing SAR bounding box on map:") - map_image_with_overlay = stitched_map_image - logging.warning(f"{log_prefix} Proceeding with map display without SAR bounding box due to drawing error.") - - current_center_lat_deg = math.degrees(geo_info_radians['lat']) # Usa la lat attuale - map_image_with_overlay = self._draw_scale_bar(map_image_with_overlay, current_center_lat_deg, zoom) + logging.exception( + f"{log_prefix} Error drawing SAR bounding box on map:" + ) + map_image_with_overlay = stitched_map_image + logging.warning( + f"{log_prefix} Proceeding with map display without SAR bounding box due to drawing error." + ) + + current_center_lat_deg = math.degrees( + geo_info_radians["lat"] + ) # Usa la lat attuale + map_image_with_overlay = self._draw_scale_bar( + map_image_with_overlay, current_center_lat_deg, zoom + ) except MapCalculationError as e: - logging.error(f"{log_prefix} Map overlay calculation failed: {e}") - map_image_with_overlay = stitched_map_image + logging.error(f"{log_prefix} Map overlay calculation failed: {e}") + map_image_with_overlay = stitched_map_image except Exception as e: - logging.exception(f"{log_prefix} Unexpected error during map overlay update:") - map_image_with_overlay = stitched_map_image + logging.exception( + f"{log_prefix} Unexpected error during map overlay update:" + ) + map_image_with_overlay = stitched_map_image finally: # --- Queue Result for Main Thread Display --- if not self._app_state.shutting_down: payload_type = type(map_image_with_overlay) - payload_size = getattr(map_image_with_overlay, 'size', 'N/A') - logging.debug(f"{log_prefix} Queueing SHOW_MAP command for updated map overlay. Payload Type: {payload_type}, Size: {payload_size}") - put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_with_overlay), "tkinter", self._app) + payload_size = getattr(map_image_with_overlay, "size", "N/A") + logging.debug( + f"{log_prefix} Queueing SHOW_MAP command for updated map overlay. Payload Type: {payload_type}, Size: {payload_size}" + ) + put_queue( + self._tkinter_queue, + ("SHOW_MAP", map_image_with_overlay), + "tkinter", + self._app, + ) else: logging.debug(f"{log_prefix} Skipping queue put due to shutdown.") @@ -418,36 +522,57 @@ class MapIntegrationManager: logging.debug(f"{log_prefix} Calling MapDisplayWindow.show_map...") try: self._map_display_window.show_map(map_image_pil) - # Update app status only *after* the initial map load attempt completes + # Update app status only *after* the initial map load attempt completes self._update_app_status_after_map_load(map_image_pil is not None) except Exception as e: - logging.exception(f"{log_prefix} Error calling MapDisplayWindow.show_map:") + logging.exception( + f"{log_prefix} Error calling MapDisplayWindow.show_map:" + ) else: - logging.warning(f"{log_prefix} Map display window not available. Cannot display map.") + logging.warning( + f"{log_prefix} Map display window not available. Cannot display map." + ) def _update_app_status_after_map_load(self, success: bool): - """Updates the main application status after the initial map load attempt.""" - log_prefix = f"{self._log_prefix} Status Update" - try: - # Check if the status bar still shows the loading message - # Access status bar via self._app reference - if hasattr(self._app, 'statusbar') and "Loading initial map" in self._app.statusbar.cget("text"): - # Determine the final status based on success and current mode - if success: - current_mode = self._app.state.test_mode_active # Check current mode - status_msg = "Ready (Test Mode)" if current_mode else \ - ("Ready (Local Mode)" if config.USE_LOCAL_IMAGES else \ - (f"Listening UDP {self._app.local_ip}:{self._app.local_port}" if self._app.udp_socket else "Error: No Socket")) - else: - status_msg = "Error Loading Map" - - logging.debug(f"{log_prefix} Initial map load finished (Success: {success}). Setting App status to: '{status_msg}'") - self._app.set_status(status_msg) # Use App's method for thread safety - #else: # Status already updated by something else, do nothing - # logging.debug(f"{log_prefix} Skipping status update, map loading message not present.") - except Exception as e: - logging.warning(f"{log_prefix} Error checking/updating app status after map load: {e}") + """Updates the main application status after the initial map load attempt.""" + log_prefix = f"{self._log_prefix} Status Update" + try: + # Check if the status bar still shows the loading message + # Access status bar via self._app reference + if hasattr( + self._app, "statusbar" + ) and "Loading initial map" in self._app.statusbar.cget("text"): + # Determine the final status based on success and current mode + if success: + current_mode = ( + self._app.state.test_mode_active + ) # Check current mode + status_msg = ( + "Ready (Test Mode)" + if current_mode + else ( + "Ready (Local Mode)" + if config.USE_LOCAL_IMAGES + else ( + f"Listening UDP {self._app.local_ip}:{self._app.local_port}" + if self._app.udp_socket + else "Error: No Socket" + ) + ) + ) + else: + status_msg = "Error Loading Map" + logging.debug( + f"{log_prefix} Initial map load finished (Success: {success}). Setting App status to: '{status_msg}'" + ) + self._app.set_status(status_msg) # Use App's method for thread safety + # else: # Status already updated by something else, do nothing + # logging.debug(f"{log_prefix} Skipping status update, map loading message not present.") + except Exception as e: + logging.warning( + f"{log_prefix} Error checking/updating app status after map load: {e}" + ) def shutdown(self): """Cleans up map-related resources, like closing the display window.""" @@ -458,9 +583,9 @@ class MapIntegrationManager: if self._map_display_window: logging.debug(f"{log_prefix} Requesting MapDisplayWindow destroy...") try: - self._map_display_window.destroy_window() # Handles internal logging + self._map_display_window.destroy_window() # Handles internal logging except Exception as e: - logging.exception(f"{log_prefix} Error destroying MapDisplayWindow:") + logging.exception(f"{log_prefix} Error destroying MapDisplayWindow:") self._map_display_window = None # Stop initial display thread if still running (less critical as it's daemon) @@ -469,7 +594,7 @@ class MapIntegrationManager: # Clear tile manager cache? Optional, maybe not needed on normal shutdown. logging.debug(f"{log_prefix} Map integration shutdown complete.") - + def _calculate_sar_corners_geo( self, geo_info: Dict[str, Any] ) -> Optional[List[Tuple[float, float]]]: @@ -491,57 +616,62 @@ class MapIntegrationManager: logging.debug(f"{log_prefix} Calculating SAR corner geographic coordinates...") if not self._geod: - logging.error(f"{log_prefix} Geodetic calculator (pyproj.Geod) not initialized.") - return None + logging.error( + f"{log_prefix} Geodetic calculator (pyproj.Geod) not initialized." + ) + return None try: # Extract necessary info (ensure keys exist and values are valid) - center_lat_rad = geo_info['lat'] - center_lon_rad = geo_info['lon'] - orient_rad = geo_info['orientation'] - ref_x = geo_info['ref_x'] - ref_y = geo_info['ref_y'] - scale_x = geo_info['scale_x'] # meters/pixel - scale_y = geo_info['scale_y'] # meters/pixel - width = geo_info['width_px'] - height = geo_info['height_px'] + center_lat_rad = geo_info["lat"] + center_lon_rad = geo_info["lon"] + orient_rad = geo_info["orientation"] + ref_x = geo_info["ref_x"] + ref_y = geo_info["ref_y"] + scale_x = geo_info["scale_x"] # meters/pixel + scale_y = geo_info["scale_y"] # meters/pixel + width = geo_info["width_px"] + height = geo_info["height_px"] if not (scale_x > 0 and scale_y > 0 and width > 0 and height > 0): - logging.error(f"{log_prefix} Invalid scale or dimensions in geo_info.") - return None + logging.error(f"{log_prefix} Invalid scale or dimensions in geo_info.") + return None # 1. Calculate pixel coordinates of corners relative to the reference pixel (ref_x, ref_y) # Origin (0,0) is top-left. Y increases downwards in pixel space. corners_pixel = [ - (0 - ref_x, ref_y - 0), # Top-Left (dx, dy relative to ref, y inverted) - (width - 1 - ref_x, ref_y - 0), # Top-Right - (width - 1 - ref_x, ref_y - (height - 1)), # Bottom-Right - (0 - ref_x, ref_y - (height - 1)) # Bottom-Left + (0 - ref_x, ref_y - 0), # Top-Left (dx, dy relative to ref, y inverted) + (width - 1 - ref_x, ref_y - 0), # Top-Right + (width - 1 - ref_x, ref_y - (height - 1)), # Bottom-Right + (0 - ref_x, ref_y - (height - 1)), # Bottom-Left ] # 2. Convert pixel offsets to meter offsets corners_meters = [ (dx * scale_x, dy * scale_y) for dx, dy in corners_pixel - ] # (delta_meters_east, delta_meters_north) + ] # (delta_meters_east, delta_meters_north) # 3. Apply inverse rotation to meter offsets if necessary # The map needs the *geographic* corners, so we need to find where # the image corners land geographically. We start from the geo center # and calculate the destination point by applying the *rotated* meter offsets. corners_meters_rotated = [] - if abs(orient_rad) > 1e-6: # Apply rotation if significant - cos_o = math.cos(orient_rad) - sin_o = math.sin(orient_rad) - for dx_m, dy_m in corners_meters: - # Rotate the offset vector (dx_m, dy_m) by orient_rad - rot_dx = dx_m * cos_o - dy_m * sin_o - rot_dy = dx_m * sin_o + dy_m * cos_o - corners_meters_rotated.append((rot_dx, rot_dy)) - logging.debug(f"{log_prefix} Applied rotation ({math.degrees(orient_rad):.2f} deg) to meter offsets.") + if abs(orient_rad) > 1e-6: # Apply rotation if significant + cos_o = math.cos(orient_rad) + sin_o = math.sin(orient_rad) + for dx_m, dy_m in corners_meters: + # Rotate the offset vector (dx_m, dy_m) by orient_rad + rot_dx = dx_m * cos_o - dy_m * sin_o + rot_dy = dx_m * sin_o + dy_m * cos_o + corners_meters_rotated.append((rot_dx, rot_dy)) + logging.debug( + f"{log_prefix} Applied rotation ({math.degrees(orient_rad):.2f} deg) to meter offsets." + ) else: - corners_meters_rotated = corners_meters # No rotation needed - logging.debug(f"{log_prefix} Skipping rotation for meter offsets (angle near zero).") - + corners_meters_rotated = corners_meters # No rotation needed + logging.debug( + f"{log_prefix} Skipping rotation for meter offsets (angle near zero)." + ) # 4. Calculate geographic coordinates of corners using pyproj.Geod.fwd # This requires calculating distance and azimuth from the center to each rotated meter offset. @@ -560,25 +690,33 @@ class MapIntegrationManager: center_lat_deg = math.degrees(center_lat_rad) # Calculate the destination point - endlon, endlat, _ = self._geod.fwd(center_lon_deg, center_lat_deg, azimuth_deg, distance_m) + endlon, endlat, _ = self._geod.fwd( + center_lon_deg, center_lat_deg, azimuth_deg, distance_m + ) # Append (lon, lat) tuple in degrees sar_corners_geo_deg.append((endlon, endlat)) - logging.debug(f"{log_prefix} Calculated corner: Dist={distance_m:.1f}m, Az={azimuth_deg:.2f}deg -> Lon={endlon:.6f}, Lat={endlat:.6f}") + logging.debug( + f"{log_prefix} Calculated corner: Dist={distance_m:.1f}m, Az={azimuth_deg:.2f}deg -> Lon={endlon:.6f}, Lat={endlat:.6f}" + ) if len(sar_corners_geo_deg) != 4: - logging.error(f"{log_prefix} Failed to calculate all 4 corner coordinates.") - return None + logging.error( + f"{log_prefix} Failed to calculate all 4 corner coordinates." + ) + return None - logging.debug(f"{log_prefix} Successfully calculated 4 SAR corner geographic coordinates.") + logging.debug( + f"{log_prefix} Successfully calculated 4 SAR corner geographic coordinates." + ) return sar_corners_geo_deg except KeyError as ke: - logging.error(f"{log_prefix} Missing required key in geo_info: {ke}") - return None + logging.error(f"{log_prefix} Missing required key in geo_info: {ke}") + return None except Exception as e: - logging.exception(f"{log_prefix} Error calculating SAR corner coordinates:") - return None + logging.exception(f"{log_prefix} Error calculating SAR corner coordinates:") + return None # --- NUOVA FUNZIONE HELPER (SCHELETRO/PLACEHOLDER) --- def _geo_coords_to_map_pixels( @@ -587,7 +725,7 @@ class MapIntegrationManager: map_bounds: mercantile.LngLatBbox, map_tile_ranges: Tuple[Tuple[int, int], Tuple[int, int]], zoom: int, - stitched_map_shape: Tuple[int, int] # (height, width) + stitched_map_shape: Tuple[int, int], # (height, width) ) -> Optional[List[Tuple[int, int]]]: """ Converts a list of geographic coordinates (lon, lat degrees) to pixel @@ -607,14 +745,22 @@ class MapIntegrationManager: top-left of the stitched map image. Returns None on error. """ log_prefix = f"{self._log_prefix} Geo to Pixel" - logging.debug(f"{log_prefix} Converting {len(coords_deg)} geo coordinates to map pixels...") + logging.debug( + f"{log_prefix} Converting {len(coords_deg)} geo coordinates to map pixels..." + ) if mercantile is None: - logging.error(f"{log_prefix} Mercantile library not available.") - return None - if not stitched_map_shape or stitched_map_shape[0] <= 0 or stitched_map_shape[1] <= 0: - logging.error(f"{log_prefix} Invalid stitched map shape: {stitched_map_shape}") - return None + logging.error(f"{log_prefix} Mercantile library not available.") + return None + if ( + not stitched_map_shape + or stitched_map_shape[0] <= 0 + or stitched_map_shape[1] <= 0 + ): + logging.error( + f"{log_prefix} Invalid stitched map shape: {stitched_map_shape}" + ) + return None pixel_coords = [] map_height_px, map_width_px = stitched_map_shape @@ -644,18 +790,26 @@ class MapIntegrationManager: # 1. Find the projected meter coordinates (Web Mercator) of the top-left corner of the stitched area. tl_tile_mercator_bounds = mercantile.xy_bounds(min_tile_x, min_tile_y, zoom) map_origin_x_mercator = tl_tile_mercator_bounds.left - map_origin_y_mercator = tl_tile_mercator_bounds.top # Top has higher Y in Mercator + map_origin_y_mercator = ( + tl_tile_mercator_bounds.top + ) # Top has higher Y in Mercator # 2. Calculate the total span of the stitched map in Mercator meters max_tile_x = map_tile_ranges[0][1] max_tile_y = map_tile_ranges[1][1] br_tile_mercator_bounds = mercantile.xy_bounds(max_tile_x, max_tile_y, zoom) - map_total_width_mercator = br_tile_mercator_bounds.right - map_origin_x_mercator - map_total_height_mercator = map_origin_y_mercator - br_tile_mercator_bounds.bottom # Top Y > Bottom Y + map_total_width_mercator = ( + br_tile_mercator_bounds.right - map_origin_x_mercator + ) + map_total_height_mercator = ( + map_origin_y_mercator - br_tile_mercator_bounds.bottom + ) # Top Y > Bottom Y - if map_total_width_mercator <= 0 or map_total_height_mercator <=0: - logging.error(f"{log_prefix} Invalid map span in Mercator coordinates calculated.") - return None + if map_total_width_mercator <= 0 or map_total_height_mercator <= 0: + logging.error( + f"{log_prefix} Invalid map span in Mercator coordinates calculated." + ) + return None # 3. For each input geographic coordinate: for lon, lat in coords_deg: @@ -664,57 +818,97 @@ class MapIntegrationManager: # b. Calculate the coordinate relative to the map's top-left origin in Mercator meters relative_x_mercator = point_x_mercator - map_origin_x_mercator - relative_y_mercator = map_origin_y_mercator - point_y_mercator # Invert Y difference + relative_y_mercator = ( + map_origin_y_mercator - point_y_mercator + ) # Invert Y difference # c. Scale the relative Mercator coordinates to pixel coordinates based on the total map span and pixel dimensions - pixel_x = int(round((relative_x_mercator / map_total_width_mercator) * map_width_px)) - pixel_y = int(round((relative_y_mercator / map_total_height_mercator) * map_height_px)) + pixel_x = int( + round( + (relative_x_mercator / map_total_width_mercator) * map_width_px + ) + ) + pixel_y = int( + round( + (relative_y_mercator / map_total_height_mercator) + * map_height_px + ) + ) # Clamp pixel coordinates to be within the stitched map bounds pixel_x_clamped = max(0, min(pixel_x, map_width_px - 1)) pixel_y_clamped = max(0, min(pixel_y, map_height_px - 1)) if pixel_x != pixel_x_clamped or pixel_y != pixel_y_clamped: - logging.warning(f"{log_prefix} Clamped pixel coords for ({lon:.4f},{lat:.4f}): ({pixel_x},{pixel_y}) -> ({pixel_x_clamped},{pixel_y_clamped})") + logging.warning( + f"{log_prefix} Clamped pixel coords for ({lon:.4f},{lat:.4f}): ({pixel_x},{pixel_y}) -> ({pixel_x_clamped},{pixel_y_clamped})" + ) pixel_coords.append((pixel_x_clamped, pixel_y_clamped)) - logging.debug(f"{log_prefix} Converted ({lon:.4f},{lat:.4f}) -> MercatorRel({relative_x_mercator:.1f},{relative_y_mercator:.1f}) -> Pixel({pixel_x_clamped},{pixel_y_clamped})") + logging.debug( + f"{log_prefix} Converted ({lon:.4f},{lat:.4f}) -> MercatorRel({relative_x_mercator:.1f},{relative_y_mercator:.1f}) -> Pixel({pixel_x_clamped},{pixel_y_clamped})" + ) - logging.debug(f"{log_prefix} Successfully converted {len(pixel_coords)} coordinates to map pixels.") + logging.debug( + f"{log_prefix} Successfully converted {len(pixel_coords)} coordinates to map pixels." + ) return pixel_coords except Exception as e: - logging.exception(f"{log_prefix} Error converting geo coordinates to map pixels:") + logging.exception( + f"{log_prefix} Error converting geo coordinates to map pixels:" + ) return None - - def _draw_scale_bar(self, image_pil: Image.Image, latitude_deg: float, zoom: int) -> Image.Image: + + def _draw_scale_bar( + self, image_pil: Image.Image, latitude_deg: float, zoom: int + ) -> Image.Image: """Draws a simple scale bar onto the map image.""" log_prefix = f"{self._log_prefix} ScaleBar" - if image_pil is None: return None # Non fare nulla se l'immagine non c'è + if image_pil is None: + return None # Non fare nulla se l'immagine non c'è try: # 1. Calculate meters/pixel meters_per_pixel = calculate_meters_per_pixel(latitude_deg, zoom) if meters_per_pixel is None or meters_per_pixel <= 0: - logging.warning(f"{log_prefix} Invalid meters_per_pixel ({meters_per_pixel}). Cannot draw scale bar.") - return image_pil # Restituisci l'immagine originale + logging.warning( + f"{log_prefix} Invalid meters_per_pixel ({meters_per_pixel}). Cannot draw scale bar." + ) + return image_pil # Restituisci l'immagine originale # 2. Choose scale distance based on meters/pixel or image width # Obiettivo: barra lunga ~100-150 pixel img_w, img_h = image_pil.size - target_bar_px = max(50, min(150, img_w // 4)) # Lunghezza desiderata in pixel (adattiva) + target_bar_px = max( + 50, min(150, img_w // 4) + ) # Lunghezza desiderata in pixel (adattiva) # Trova una distanza "tonda" (1, 2, 5, 10, 20, 50, 100... km) # che corrisponda a circa target_bar_px - possible_distances_km = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000] + possible_distances_km = [ + 0.1, + 0.2, + 0.5, + 1, + 2, + 5, + 10, + 20, + 50, + 100, + 200, + 500, + 1000, + ] best_dist_km = 1 - min_diff = float('inf') + min_diff = float("inf") for dist_km in possible_distances_km: dist_m = dist_km * 1000.0 pixels = dist_m / meters_per_pixel diff = abs(pixels - target_bar_px) - if diff < min_diff and pixels > 10: # Assicura una barra minima + if diff < min_diff and pixels > 10: # Assicura una barra minima min_diff = diff best_dist_km = dist_km @@ -722,9 +916,11 @@ class MapIntegrationManager: scale_distance_meters = scale_distance_km * 1000.0 scale_bar_pixels = int(round(scale_distance_meters / meters_per_pixel)) - if scale_bar_pixels < 10: # Troppo piccola per essere utile - logging.warning(f"{log_prefix} Calculated scale bar length too small ({scale_bar_pixels}px). Skipping draw.") - return image_pil + if scale_bar_pixels < 10: # Troppo piccola per essere utile + logging.warning( + f"{log_prefix} Calculated scale bar length too small ({scale_bar_pixels}px). Skipping draw." + ) + return image_pil # 3. Prepare for drawing (Convert to OpenCV BGR) map_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR) @@ -732,36 +928,70 @@ class MapIntegrationManager: # 4. Define drawing parameters bar_x_start = 15 - bar_y = h - 20 # Posiziona in basso + bar_y = h - 20 # Posiziona in basso bar_thickness = 2 text_offset_y = -5 font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.5 font_thickness = 1 - color = (0, 0, 0) # Nero + color = (0, 0, 0) # Nero # 5. Draw the scale bar line - cv2.line(map_cv, (bar_x_start, bar_y), (bar_x_start + scale_bar_pixels, bar_y), color, bar_thickness) + cv2.line( + map_cv, + (bar_x_start, bar_y), + (bar_x_start + scale_bar_pixels, bar_y), + color, + bar_thickness, + ) # Draw small ticks at the ends - cv2.line(map_cv, (bar_x_start, bar_y - 3), (bar_x_start, bar_y + 3), color, bar_thickness) - cv2.line(map_cv, (bar_x_start + scale_bar_pixels, bar_y - 3), (bar_x_start + scale_bar_pixels, bar_y + 3), color, bar_thickness) - + cv2.line( + map_cv, + (bar_x_start, bar_y - 3), + (bar_x_start, bar_y + 3), + color, + bar_thickness, + ) + cv2.line( + map_cv, + (bar_x_start + scale_bar_pixels, bar_y - 3), + (bar_x_start + scale_bar_pixels, bar_y + 3), + color, + bar_thickness, + ) # 6. Draw the text label - label = f"{scale_distance_km} km" if scale_distance_km >= 1 else f"{int(scale_distance_meters)} m" + label = ( + f"{scale_distance_km} km" + if scale_distance_km >= 1 + else f"{int(scale_distance_meters)} m" + ) text_size, _ = cv2.getTextSize(label, font, font_scale, font_thickness) text_x = bar_x_start + (scale_bar_pixels // 2) - (text_size[0] // 2) - text_y = bar_y + text_offset_y - 5 # Posiziona sopra la barra - cv2.putText(map_cv, label, (text_x, text_y), font, font_scale, color, font_thickness, cv2.LINE_AA) + text_y = bar_y + text_offset_y - 5 # Posiziona sopra la barra + cv2.putText( + map_cv, + label, + (text_x, text_y), + font, + font_scale, + color, + font_thickness, + cv2.LINE_AA, + ) # 7. Convert back to PIL RGB - image_pil_with_scale = Image.fromarray(cv2.cvtColor(map_cv, cv2.COLOR_BGR2RGB)) - logging.debug(f"{log_prefix} Scale bar drawn ({label}, {scale_bar_pixels}px).") + image_pil_with_scale = Image.fromarray( + cv2.cvtColor(map_cv, cv2.COLOR_BGR2RGB) + ) + logging.debug( + f"{log_prefix} Scale bar drawn ({label}, {scale_bar_pixels}px)." + ) return image_pil_with_scale except Exception as e: logging.exception(f"{log_prefix} Error drawing scale bar:") - return image_pil # Restituisci l'originale in caso di errore + return image_pil # Restituisci l'originale in caso di errore -# --- END OF FILE map_integration.py --- \ No newline at end of file +# --- END OF FILE map_integration.py --- diff --git a/map_utils.py b/map_utils.py index 0f2bc50..547a852 100644 --- a/map_utils.py +++ b/map_utils.py @@ -195,6 +195,7 @@ def get_tile_ranges_for_bbox( logging.exception(f"{log_prefix} Error calculating tile ranges:") return None + def calculate_meters_per_pixel(latitude_deg: float, zoom: int) -> Optional[float]: """ Calculates the approximate ground resolution (meters per pixel) at a given @@ -213,21 +214,24 @@ def calculate_meters_per_pixel(latitude_deg: float, zoom: int) -> Optional[float if not (-90 <= latitude_deg <= 90): logging.warning(f"{log_prefix} Invalid latitude: {latitude_deg}") return None - if not (0 <= zoom <= 22): # Practical zoom range limit - logging.warning(f"{log_prefix} Invalid zoom level: {zoom}") - return None + if not (0 <= zoom <= 22): # Practical zoom range limit + logging.warning(f"{log_prefix} Invalid zoom level: {zoom}") + return None # Formula based on Earth circumference and tile size (usually 256px) # Meters per pixel = (Earth Circumference * cos(latitude)) / (Tile Size * 2^zoom) # Earth Circumference approx 40075016.686 meters at equator - C = 40075016.686 # meters - TILE_SIZE = 256 # pixels + C = 40075016.686 # meters + TILE_SIZE = 256 # pixels latitude_rad = math.radians(latitude_deg) meters_per_pixel = (C * math.cos(latitude_rad)) / (TILE_SIZE * (2**zoom)) - logging.debug(f"{log_prefix} Calculated meters/pixel at lat {latitude_deg:.4f}, zoom {zoom}: {meters_per_pixel:.4f}") + logging.debug( + f"{log_prefix} Calculated meters/pixel at lat {latitude_deg:.4f}, zoom {zoom}: {meters_per_pixel:.4f}" + ) return meters_per_pixel except Exception as e: logging.exception(f"{log_prefix} Error calculating meters per pixel:") return None + # --- END OF FILE map_utils.py --- diff --git a/receiver.py b/receiver.py index 4ec7fdb..d017142 100644 --- a/receiver.py +++ b/receiver.py @@ -808,7 +808,7 @@ class UdpReceiver: logging.warning( f"{geo_log_prefix} {image_key_log}: Invalid geo values found (ScaleValid={is_scale_valid}, LatValid={is_lat_valid}, LonValid={is_lon_valid}, OrientValid={is_orient_valid}). GeoInfo marked invalid." ) - geo_info_radians["valid"] = False # Ensure marked invalid + geo_info_radians["valid"] = False # Ensure marked invalid except OverflowError as oe: logging.error( diff --git a/test_mode_manager.py b/test_mode_manager.py index 88a4635..aa1a925 100644 --- a/test_mode_manager.py +++ b/test_mode_manager.py @@ -82,7 +82,9 @@ class TestModeManager: self._app_state.test_mfd_image_indices is None or self._app_state.test_sar_image_raw is None ): - logging.error(f"{log_prefix} Test image data missing in AppState! Cannot activate.") + logging.error( + f"{log_prefix} Test image data missing in AppState! Cannot activate." + ) # Should we revert the state flag? App handler should do this. return False # Indicate activation failure @@ -96,7 +98,7 @@ class TestModeManager: self._schedule_mfd_test_update() self._schedule_sar_test_update() logging.info(f"{log_prefix} Test Mode update loops scheduled.") - return True # Indicate success + return True # Indicate success def deactivate(self): """Deactivates the test mode by stopping update timers.""" @@ -126,7 +128,9 @@ class TestModeManager: self._app_state.test_mfd_image_indices = np.random.randint( low=0, high=256, size=mfd_shape, dtype=np.uint8 ) - logging.debug(f"{log_prefix} Generated random MFD indices (shape {mfd_shape}).") + logging.debug( + f"{log_prefix} Generated random MFD indices (shape {mfd_shape})." + ) # SAR Raw Data Generation sar_shape = (config.SAR_HEIGHT, config.SAR_WIDTH) @@ -135,47 +139,62 @@ class TestModeManager: dtype_info = np.iinfo(config.SAR_DATA_TYPE) min_val = dtype_info.min max_val = dtype_info.max - except ValueError: # Handle case where SAR_DATA_TYPE might be float - logging.warning(f"{log_prefix} SAR_DATA_TYPE {config.SAR_DATA_TYPE} is not integer. Generating float test data [0,1).") - min_val = 0.0 - max_val = 1.0 # Adjust range if floats are needed - # Generate floats if needed, otherwise stick to integers - if np.issubdtype(config.SAR_DATA_TYPE, np.floating): - self._app_state.test_sar_image_raw = np.random.rand(*sar_shape).astype(config.SAR_DATA_TYPE) - else: # Fallback to uint16 if type is weird but not float - logging.warning(f"{log_prefix} Unexpected SAR_DATA_TYPE {config.SAR_DATA_TYPE}. Falling back to uint16 generation.") - dtype_info = np.iinfo(np.uint16) - min_val = dtype_info.min - max_val = dtype_info.max - self._app_state.test_sar_image_raw = np.random.randint( - low=min_val, - high=max_val + 1, # randint is exclusive of high - size=sar_shape, - dtype=np.uint16 # Explicit fallback type - ) + except ValueError: # Handle case where SAR_DATA_TYPE might be float + logging.warning( + f"{log_prefix} SAR_DATA_TYPE {config.SAR_DATA_TYPE} is not integer. Generating float test data [0,1)." + ) + min_val = 0.0 + max_val = 1.0 # Adjust range if floats are needed + # Generate floats if needed, otherwise stick to integers + if np.issubdtype(config.SAR_DATA_TYPE, np.floating): + self._app_state.test_sar_image_raw = np.random.rand( + *sar_shape + ).astype(config.SAR_DATA_TYPE) + else: # Fallback to uint16 if type is weird but not float + logging.warning( + f"{log_prefix} Unexpected SAR_DATA_TYPE {config.SAR_DATA_TYPE}. Falling back to uint16 generation." + ) + dtype_info = np.iinfo(np.uint16) + min_val = dtype_info.min + max_val = dtype_info.max + self._app_state.test_sar_image_raw = np.random.randint( + low=min_val, + high=max_val + 1, # randint is exclusive of high + size=sar_shape, + dtype=np.uint16, # Explicit fallback type + ) # Generate integers if type allows if np.issubdtype(config.SAR_DATA_TYPE, np.integer): - self._app_state.test_sar_image_raw = np.random.randint( - low=min_val, - high=max_val + 1, # numpy randint is exclusive of high - size=sar_shape, - dtype=config.SAR_DATA_TYPE - ) + self._app_state.test_sar_image_raw = np.random.randint( + low=min_val, + high=max_val + 1, # numpy randint is exclusive of high + size=sar_shape, + dtype=config.SAR_DATA_TYPE, + ) - logging.debug(f"{log_prefix} Generated random SAR raw data (shape {sar_shape}, dtype {config.SAR_DATA_TYPE}).") + logging.debug( + f"{log_prefix} Generated random SAR raw data (shape {sar_shape}, dtype {config.SAR_DATA_TYPE})." + ) - logging.info(f"{log_prefix} Test images generated successfully into AppState.") + logging.info( + f"{log_prefix} Test images generated successfully into AppState." + ) except Exception as e: logging.exception(f"{log_prefix} Error generating test images:") # Set fallback state to avoid None values crashing later code if self._app_state.test_mfd_image_indices is None: - self._app_state.test_mfd_image_indices = np.zeros((config.MFD_HEIGHT, config.MFD_WIDTH), dtype=np.uint8) + self._app_state.test_mfd_image_indices = np.zeros( + (config.MFD_HEIGHT, config.MFD_WIDTH), dtype=np.uint8 + ) if self._app_state.test_sar_image_raw is None: - self._app_state.test_sar_image_raw = np.zeros((config.SAR_HEIGHT, config.SAR_WIDTH), dtype=config.SAR_DATA_TYPE) - logging.error(f"{log_prefix} Fallback test images (zeros) set in AppState due to generation error.") - + self._app_state.test_sar_image_raw = np.zeros( + (config.SAR_HEIGHT, config.SAR_WIDTH), dtype=config.SAR_DATA_TYPE + ) + logging.error( + f"{log_prefix} Fallback test images (zeros) set in AppState due to generation error." + ) def _schedule_mfd_test_update(self): """Schedules the next MFD test image update if active.""" @@ -185,25 +204,35 @@ class TestModeManager: # Call the update logic for one frame self._update_mfd_test_display() # Calculate delay based on configured MFD FPS - delay_ms = max(1, int(1000 / config.MFD_FPS)) if config.MFD_FPS > 0 else 40 # Default ~25fps + delay_ms = ( + max(1, int(1000 / config.MFD_FPS)) if config.MFD_FPS > 0 else 40 + ) # Default ~25fps try: # Schedule the next call using Tkinter's after method # Ensure root window still exists if self._root and self._root.winfo_exists(): - self._mfd_test_timer_id = self._root.after(delay_ms, self._schedule_mfd_test_update) - logging.debug(f"{log_prefix} Scheduled next update in {delay_ms} ms (ID: {self._mfd_test_timer_id}).") + self._mfd_test_timer_id = self._root.after( + delay_ms, self._schedule_mfd_test_update + ) + logging.debug( + f"{log_prefix} Scheduled next update in {delay_ms} ms (ID: {self._mfd_test_timer_id})." + ) else: - logging.warning(f"{log_prefix} Root window destroyed. Stopping MFD test updates.") - self._mfd_test_timer_id = None # Ensure timer ID is cleared + logging.warning( + f"{log_prefix} Root window destroyed. Stopping MFD test updates." + ) + self._mfd_test_timer_id = None # Ensure timer ID is cleared except Exception as e: - # Log error during scheduling but attempt to stop timer ID - logging.warning(f"{log_prefix} Error scheduling next MFD update: {e}") - self._mfd_test_timer_id = None + # Log error during scheduling but attempt to stop timer ID + logging.warning(f"{log_prefix} Error scheduling next MFD update: {e}") + self._mfd_test_timer_id = None else: # Log if scheduling stops due to state change - logging.debug(f"{log_prefix} Test mode inactive or shutting down. Stopping MFD updates.") - self._mfd_test_timer_id = None # Ensure timer ID is cleared + logging.debug( + f"{log_prefix} Test mode inactive or shutting down. Stopping MFD updates." + ) + self._mfd_test_timer_id = None # Ensure timer ID is cleared def _schedule_sar_test_update(self): """Schedules the next SAR test image update if active.""" @@ -219,19 +248,27 @@ class TestModeManager: # Schedule the next call using Tkinter's after method # Ensure root window still exists if self._root and self._root.winfo_exists(): - self._sar_test_timer_id = self._root.after(delay_ms, self._schedule_sar_test_update) - logging.debug(f"{log_prefix} Scheduled next update in {delay_ms} ms (ID: {self._sar_test_timer_id}).") + self._sar_test_timer_id = self._root.after( + delay_ms, self._schedule_sar_test_update + ) + logging.debug( + f"{log_prefix} Scheduled next update in {delay_ms} ms (ID: {self._sar_test_timer_id})." + ) else: - logging.warning(f"{log_prefix} Root window destroyed. Stopping SAR test updates.") - self._sar_test_timer_id = None # Ensure timer ID is cleared + logging.warning( + f"{log_prefix} Root window destroyed. Stopping SAR test updates." + ) + self._sar_test_timer_id = None # Ensure timer ID is cleared except Exception as e: - # Log error during scheduling but attempt to stop timer ID - logging.warning(f"{log_prefix} Error scheduling next SAR update: {e}") - self._sar_test_timer_id = None + # Log error during scheduling but attempt to stop timer ID + logging.warning(f"{log_prefix} Error scheduling next SAR update: {e}") + self._sar_test_timer_id = None else: # Log if scheduling stops due to state change - logging.debug(f"{log_prefix} Test mode inactive or shutting down. Stopping SAR updates.") - self._sar_test_timer_id = None # Ensure timer ID is cleared + logging.debug( + f"{log_prefix} Test mode inactive or shutting down. Stopping SAR updates." + ) + self._sar_test_timer_id = None # Ensure timer ID is cleared def stop_timers(self): """Cancels any active test mode update timers.""" @@ -242,12 +279,16 @@ class TestModeManager: # Check if root window exists before cancelling if self._root and self._root.winfo_exists(): self._root.after_cancel(self._mfd_test_timer_id) - logging.debug(f"{log_prefix} MFD test timer (ID: {self._mfd_test_timer_id}) cancelled.") + logging.debug( + f"{log_prefix} MFD test timer (ID: {self._mfd_test_timer_id}) cancelled." + ) except Exception as e: # Log warning if cancellation fails (e.g., ID invalid, window closed) - logging.warning(f"{log_prefix} Ignoring error cancelling MFD timer (ID: {self._mfd_test_timer_id}): {e}") + logging.warning( + f"{log_prefix} Ignoring error cancelling MFD timer (ID: {self._mfd_test_timer_id}): {e}" + ) finally: - self._mfd_test_timer_id = None # Always clear the ID + self._mfd_test_timer_id = None # Always clear the ID # Cancel SAR timer if active if self._sar_test_timer_id: @@ -255,12 +296,16 @@ class TestModeManager: # Check if root window exists if self._root and self._root.winfo_exists(): self._root.after_cancel(self._sar_test_timer_id) - logging.debug(f"{log_prefix} SAR test timer (ID: {self._sar_test_timer_id}) cancelled.") + logging.debug( + f"{log_prefix} SAR test timer (ID: {self._sar_test_timer_id}) cancelled." + ) except Exception as e: # Log warning if cancellation fails - logging.warning(f"{log_prefix} Ignoring error cancelling SAR timer (ID: {self._sar_test_timer_id}): {e}") + logging.warning( + f"{log_prefix} Ignoring error cancelling SAR timer (ID: {self._sar_test_timer_id}): {e}" + ) finally: - self._sar_test_timer_id = None # Always clear the ID + self._sar_test_timer_id = None # Always clear the ID def _update_mfd_test_display(self): """ @@ -273,25 +318,31 @@ class TestModeManager: logging.debug(f"{log_prefix} Shutdown detected. Skipping update.") return if not self._app_state.test_mode_active: - logging.debug(f"{log_prefix} Test mode not active. Skipping update.") - return # Should not happen if called from scheduler, but defensive check + logging.debug(f"{log_prefix} Test mode not active. Skipping update.") + return # Should not happen if called from scheduler, but defensive check # --- Get required data and parameters from AppState --- try: test_indices = self._app_state.test_mfd_image_indices mfd_lut = self._app_state.mfd_lut except AttributeError as ae: - logging.error(f"{log_prefix} Missing required attribute in AppState: {ae}. Cannot process.") - self.stop_timers() # Stop updates if state is broken - return + logging.error( + f"{log_prefix} Missing required attribute in AppState: {ae}. Cannot process." + ) + self.stop_timers() # Stop updates if state is broken + return # Validate data existence if test_indices is None: - logging.warning(f"{log_prefix} Test MFD indices data missing in AppState. Cannot process.") + logging.warning( + f"{log_prefix} Test MFD indices data missing in AppState. Cannot process." + ) # Attempt regeneration or stop? For now, log and return. return if mfd_lut is None: - logging.warning(f"{log_prefix} MFD LUT missing in AppState. Cannot process.") + logging.warning( + f"{log_prefix} MFD LUT missing in AppState. Cannot process." + ) # Maybe try regenerating LUT? For now, log and return. return @@ -299,25 +350,37 @@ class TestModeManager: try: # --- Scrolling --- # Use internal offset attribute, update it - self._test_mfd_offset = (self._test_mfd_offset + 2) % config.MFD_WIDTH # Scroll by 2 pixels + self._test_mfd_offset = ( + self._test_mfd_offset + 2 + ) % config.MFD_WIDTH # Scroll by 2 pixels # Apply roll using the internal offset scrolled_indices = np.roll(test_indices, -self._test_mfd_offset, axis=1) - logging.debug(f"{log_prefix} Applied scroll (offset: {self._test_mfd_offset}).") + logging.debug( + f"{log_prefix} Applied scroll (offset: {self._test_mfd_offset})." + ) # --- Check shutdown again before LUT application --- if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected after scroll. Skipping LUT.") + logging.debug( + f"{log_prefix} Shutdown detected after scroll. Skipping LUT." + ) return # --- Apply MFD LUT from AppState --- - logging.debug(f"{log_prefix} Applying MFD LUT (shape {mfd_lut.shape}) to indices (shape {scrolled_indices.shape})...") + logging.debug( + f"{log_prefix} Applying MFD LUT (shape {mfd_lut.shape}) to indices (shape {scrolled_indices.shape})..." + ) # Perform LUT lookup using NumPy indexing mfd_bgr_image = mfd_lut[scrolled_indices] - logging.debug(f"{log_prefix} MFD LUT applied. Result shape: {mfd_bgr_image.shape}.") + logging.debug( + f"{log_prefix} MFD LUT applied. Result shape: {mfd_bgr_image.shape}." + ) # --- Check shutdown again before queueing --- if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected after LUT. Skipping queue put.") + logging.debug( + f"{log_prefix} Shutdown detected after LUT. Skipping queue put." + ) return # --- Queue the processed image --- @@ -330,21 +393,22 @@ class TestModeManager: except IndexError as idx_err: # Handle potential index errors if LUT/indices dimensions mismatch - min_idx, max_idx = "-","-" - if scrolled_indices is not None: - try: - min_idx=np.min(scrolled_indices) - max_idx=np.max(scrolled_indices) - except ValueError: pass # Handle empty array case - lut_shape_str = str(mfd_lut.shape) if mfd_lut is not None else "None" - logging.error( - f"{log_prefix} MFD LUT IndexError: {idx_err}. " - f"Indices range maybe ({min_idx},{max_idx}). LUT shape {lut_shape_str}" - ) - self.stop_timers() # Stop test mode on critical error + min_idx, max_idx = "-", "-" + if scrolled_indices is not None: + try: + min_idx = np.min(scrolled_indices) + max_idx = np.max(scrolled_indices) + except ValueError: + pass # Handle empty array case + lut_shape_str = str(mfd_lut.shape) if mfd_lut is not None else "None" + logging.error( + f"{log_prefix} MFD LUT IndexError: {idx_err}. " + f"Indices range maybe ({min_idx},{max_idx}). LUT shape {lut_shape_str}" + ) + self.stop_timers() # Stop test mode on critical error except Exception as e: logging.exception(f"{log_prefix} Error during MFD test display update:") - self.stop_timers() # Stop test mode on critical error + self.stop_timers() # Stop test mode on critical error def _update_sar_test_display(self): """ @@ -358,8 +422,8 @@ class TestModeManager: logging.debug(f"{log_prefix} Shutdown detected. Skipping update.") return if not self._app_state.test_mode_active: - logging.debug(f"{log_prefix} Test mode not active. Skipping update.") - return + logging.debug(f"{log_prefix} Test mode not active. Skipping update.") + return # --- Get required data and parameters from AppState --- try: @@ -369,73 +433,102 @@ class TestModeManager: display_width = self._app_state.sar_display_width display_height = self._app_state.sar_display_height except AttributeError as ae: - logging.error(f"{log_prefix} Missing required attribute in AppState: {ae}. Cannot process.") - self.stop_timers() - return + logging.error( + f"{log_prefix} Missing required attribute in AppState: {ae}. Cannot process." + ) + self.stop_timers() + return # Validate data and parameters if test_raw_data is None: - logging.warning(f"{log_prefix} Test SAR raw data missing in AppState. Cannot process.") + logging.warning( + f"{log_prefix} Test SAR raw data missing in AppState. Cannot process." + ) return if bc_lut is None: - logging.warning(f"{log_prefix} SAR Brightness/Contrast LUT missing in AppState. Cannot process.") + logging.warning( + f"{log_prefix} SAR Brightness/Contrast LUT missing in AppState. Cannot process." + ) return if display_width <= 0 or display_height <= 0: - logging.warning(f"{log_prefix} Invalid SAR display dimensions ({display_width}x{display_height}) in AppState. Cannot process.") - return + logging.warning( + f"{log_prefix} Invalid SAR display dimensions ({display_width}x{display_height}) in AppState. Cannot process." + ) + return logging.debug(f"{log_prefix} Processing SAR test frame...") try: # --- Processing Pipeline (using functions from image_processing) --- # 1. Normalize Raw Data to uint8 - logging.debug(f"{log_prefix} Normalizing raw test data (shape {test_raw_data.shape}, dtype {test_raw_data.dtype}) to uint8...") + logging.debug( + f"{log_prefix} Normalizing raw test data (shape {test_raw_data.shape}, dtype {test_raw_data.dtype}) to uint8..." + ) img = normalize_image(test_raw_data, target_type=np.uint8) - if img is None or self._app_state.shutting_down: # Check result and shutdown - if img is None: logging.error(f"{log_prefix} Normalization failed.") - else: logging.debug(f"{log_prefix} Shutdown after normalization.") + if ( + img is None or self._app_state.shutting_down + ): # Check result and shutdown + if img is None: + logging.error(f"{log_prefix} Normalization failed.") + else: + logging.debug(f"{log_prefix} Shutdown after normalization.") return - logging.debug(f"{log_prefix} Normalization complete. Image shape: {img.shape}.") + logging.debug( + f"{log_prefix} Normalization complete. Image shape: {img.shape}." + ) # 2. Apply Brightness/Contrast LUT logging.debug(f"{log_prefix} Applying B/C LUT (shape {bc_lut.shape})...") img = cv2.LUT(img, bc_lut) - if self._app_state.shutting_down: # Check shutdown - logging.debug(f"{log_prefix} Shutdown after B/C LUT.") - return + if self._app_state.shutting_down: # Check shutdown + logging.debug(f"{log_prefix} Shutdown after B/C LUT.") + return logging.debug(f"{log_prefix} B/C LUT applied.") # 3. Apply Color Palette (if not GRAY) if palette != "GRAY": logging.debug(f"{log_prefix} Applying color palette: {palette}...") img = apply_color_palette(img, palette) - if self._app_state.shutting_down: # Check shutdown + if self._app_state.shutting_down: # Check shutdown logging.debug(f"{log_prefix} Shutdown after palette.") return - logging.debug(f"{log_prefix} Palette '{palette}' applied. Image shape: {img.shape}.") + logging.debug( + f"{log_prefix} Palette '{palette}' applied. Image shape: {img.shape}." + ) else: - logging.debug(f"{log_prefix} Skipping color palette (GRAY selected).") - + logging.debug(f"{log_prefix} Skipping color palette (GRAY selected).") # 4. Resize Image to display dimensions - logging.debug(f"{log_prefix} Resizing image to {display_width}x{display_height}...") + logging.debug( + f"{log_prefix} Resizing image to {display_width}x{display_height}..." + ) img = resize_image(img, display_width, display_height) - if img is None or self._app_state.shutting_down: # Check result and shutdown - if img is None: logging.error(f"{log_prefix} Resize failed.") - else: logging.debug(f"{log_prefix} Shutdown after resize.") + if ( + img is None or self._app_state.shutting_down + ): # Check result and shutdown + if img is None: + logging.error(f"{log_prefix} Resize failed.") + else: + logging.debug(f"{log_prefix} Shutdown after resize.") return logging.debug(f"{log_prefix} Resize complete. Image shape: {img.shape}.") # --- Scrolling --- # Use internal offset attribute, update it - self._test_sar_offset = (self._test_sar_offset + 1) % display_width # Scroll by 1 pixel + self._test_sar_offset = ( + self._test_sar_offset + 1 + ) % display_width # Scroll by 1 pixel # Apply roll using the internal offset img = np.roll(img, -self._test_sar_offset, axis=1) - logging.debug(f"{log_prefix} Applied scroll (offset: {self._test_sar_offset}).") + logging.debug( + f"{log_prefix} Applied scroll (offset: {self._test_sar_offset})." + ) # --- Check shutdown again before queueing --- if self._app_state.shutting_down: - logging.debug(f"{log_prefix} Shutdown detected after scroll. Skipping queue put.") + logging.debug( + f"{log_prefix} Shutdown detected after scroll. Skipping queue put." + ) return # --- Queue the processed image --- @@ -448,7 +541,7 @@ class TestModeManager: except Exception as e: logging.exception(f"{log_prefix} Error during SAR test display update:") - self.stop_timers() # Stop test mode on critical error + self.stop_timers() # Stop test mode on critical error -# --- END OF FILE test_mode_manager.py --- \ No newline at end of file +# --- END OF FILE test_mode_manager.py --- diff --git a/ui.py b/ui.py index f2d0708..120cd1a 100644 --- a/ui.py +++ b/ui.py @@ -51,7 +51,7 @@ class ControlPanel(ttk.Frame): # Method within ControlPanel class in ui.py def init_ui(self): """Initializes and arranges the user interface widgets within the frame. - Uses a more compact layout.""" + Uses a compact layout with Size/Palette and Contrast/Brightness on same rows.""" log_prefix = "[UI Setup]" logging.debug( f"{log_prefix} Starting init_ui widget creation (compact layout)..." @@ -73,13 +73,13 @@ class ControlPanel(ttk.Frame): 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 + row=sar_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=2 ) logging.debug(f"{log_prefix} Test Image checkbox created.") - sar_row += 1 + sar_row += 1 # Now sar_row is 1 - # SAR Size - self.sar_size_label = ttk.Label(self.sar_params_frame, text="SAR Size:") + # --- Row 1: Size and Palette --- + 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 ) @@ -87,9 +87,8 @@ class ControlPanel(ttk.Frame): self.sar_params_frame, values=config.SAR_SIZE_FACTORS, state="readonly", - width=5, + width=6, ) - # Set initial value based on AppState (via app reference) try: initial_factor = ( config.SAR_WIDTH // self.app.state.sar_display_width @@ -98,87 +97,82 @@ class ControlPanel(ttk.Frame): ) initial_size_str = f"1:{initial_factor}" if initial_size_str not in config.SAR_SIZE_FACTORS: - initial_size_str = ( - config.DEFAULT_SAR_SIZE - ) # Fallback if calculated factor is not in list + initial_size_str = config.DEFAULT_SAR_SIZE self.sar_size_combo.set(initial_size_str) - except Exception: # Catch potential errors accessing state during init + except Exception as e: + logging.warning(f"{log_prefix} Error getting initial SAR size from state: {e}. Using default.") self.sar_size_combo.set(config.DEFAULT_SAR_SIZE) - self.sar_size_combo.grid( - row=sar_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1 + row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1 ) self.sar_size_combo.bind("<>", self.app.update_sar_size) - logging.debug(f"{log_prefix} SAR Size combobox created.") - sar_row += 1 - # Contrast Scale - 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_scale = ttk.Scale( - self.sar_params_frame, - orient=tk.HORIZONTAL, - length=150, - from_=0.1, - to=3.0, - # Set initial value from AppState - value=self.app.state.sar_contrast, - command=self.app.update_contrast, - ) - self.contrast_scale.grid( - row=sar_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1 - ) - logging.debug(f"{log_prefix} Contrast scale created.") - sar_row += 1 - - # Brightness Scale - self.brightness_label = ttk.Label(self.sar_params_frame, text="Brightness:") - self.brightness_label.grid( - row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1 - ) - self.brightness_scale = ttk.Scale( - self.sar_params_frame, - orient=tk.HORIZONTAL, - length=150, - from_=-100, - to=100, - # Set initial value from AppState - value=self.app.state.sar_brightness, - command=self.app.update_brightness, - ) - self.brightness_scale.grid( - row=sar_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1 - ) - logging.debug(f"{log_prefix} Brightness scale created.") - sar_row += 1 - - # Palette Combobox self.palette_label = ttk.Label(self.sar_params_frame, text="Palette:") - self.palette_label.grid(row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1) + 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, ) - # Set initial value from AppState self.palette_combo.set(self.app.state.sar_palette) self.palette_combo.grid( - row=sar_row, column=1, sticky=tk.EW, padx=(2, 5), pady=1 + row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1 ) self.palette_combo.bind("<>", self.app.update_sar_palette) - logging.debug(f"{log_prefix} Palette combobox created.") + logging.debug(f"{log_prefix} Size and Palette controls created on row {sar_row}.") + sar_row += 1 # Now sar_row is 2 + # --- Row 2: Contrast and Brightness --- + 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 # Pad left of label + ) + self.contrast_scale = ttk.Scale( + self.sar_params_frame, + orient=tk.HORIZONTAL, + from_=0.1, + to=3.0, + value=self.app.state.sar_contrast, + command=self.app.update_contrast, + ) + # Grid in column 1, add right padding for spacing before next label + self.contrast_scale.grid( + row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1 + ) + logging.debug(f"{log_prefix} Contrast scale created.") + + self.brightness_label = ttk.Label(self.sar_params_frame, text="Brightness:") + # Grid in column 2, less left padding + self.brightness_label.grid( + row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1 # Space before label + ) + self.brightness_scale = ttk.Scale( + self.sar_params_frame, + orient=tk.HORIZONTAL, + from_=-100, + to=100, + value=self.app.state.sar_brightness, + command=self.app.update_brightness, + ) + # Grid in column 3, add right padding + self.brightness_scale.grid( + row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1 # Space after scale + ) + logging.debug(f"{log_prefix} Brightness scale created.") + logging.debug(f"{log_prefix} Contrast and Brightness controls created on row {sar_row}.") + # sar_row += 1 # No more SAR controls after this + + # Configure column weights for expansion (already done previously, still correct) self.sar_params_frame.columnconfigure(1, weight=1) + self.sar_params_frame.columnconfigure(3, weight=1) # --- SAR Info Frame --- + # (No changes needed in this frame) logging.debug(f"{log_prefix} Creating SAR Info frame...") self.sar_info_frame = ttk.Labelframe(self, text="SAR Info", padding=5) self.sar_info_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2) - - # SAR Info Labels (Initial text set here, updated by App.update_status) + # ... (rest of the labels remain the same) ... self.sar_center_label = ttk.Label( self.sar_info_frame, text="Image Ref: Lat=N/A, Lon=N/A" ) @@ -187,19 +181,17 @@ class ControlPanel(ttk.Frame): self.sar_info_frame, text="Image Orient: N/A" ) self.sar_orientation_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) - self.sar_size_label = ttk.Label( self.sar_info_frame, text="Image Size: N/A" ) self.sar_size_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) - self.mouse_latlon_label = ttk.Label( self.sar_info_frame, text="Mouse : Lat=N/A, Lon=N/A" ) self.mouse_latlon_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) self.dropped_label = ttk.Label( self.sar_info_frame, text="Dropped (Q): SAR=0, MFD=0, Tk=0, Mouse=0" - ) # Initial text + ) self.dropped_label.pack(side=tk.TOP, anchor=tk.W, padx=5, pady=1) self.incomplete_label = ttk.Label( self.sar_info_frame, text="Incomplete (RX): SAR=0, MFD=0" @@ -208,10 +200,11 @@ class ControlPanel(ttk.Frame): logging.debug(f"{log_prefix} SAR Info labels created.") # --- MFD Parameters Frame --- + # (No changes needed in this frame) logging.debug(f"{log_prefix} Creating MFD Parameters frame...") 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) - + # ... (rest of the MFD setup remains the same) ... mfd_row = 0 mfd_categories_ui_setup = { "Occlusion": {"label_text": "Occlusion:"}, @@ -230,15 +223,14 @@ class ControlPanel(ttk.Frame): intensity_var_name = ( f"mfd_{internal_name.replace(' ', '_').lower()}_intensity_var" ) - # Read initial intensity from AppState - initial_intensity = config.DEFAULT_MFD_INTENSITY # Default fallback + initial_intensity = config.DEFAULT_MFD_INTENSITY try: initial_intensity = self.app.state.mfd_params["categories"][ internal_name ]["intensity"] - except Exception: + except Exception as e: logging.error( - f"{log_prefix} Could not get initial intensity for {internal_name} from AppState." + f"{log_prefix} Could not get initial intensity for {internal_name} from AppState: {e}" ) intensity_var = tk.IntVar(value=initial_intensity) @@ -270,12 +262,9 @@ class ControlPanel(ttk.Frame): self.mfd_params_frame, text="", width=3, relief=tk.SUNKEN, borderwidth=1 ) try: - # --- CORRECTION HERE --- - # Access mfd_params through self.app.state initial_bgr = self.app.state.mfd_params["categories"][internal_name][ "color" ] - # --- END CORRECTION --- initial_hex = "#{:02x}{:02x}{:02x}".format( initial_bgr[2], initial_bgr[1], initial_bgr[0] ) @@ -300,13 +289,12 @@ class ControlPanel(ttk.Frame): logging.debug(f"{log_prefix} Creating Raw Map intensity slider...") raw_map_label = ttk.Label(self.mfd_params_frame, text="Raw Map:") raw_map_label.grid(row=mfd_row, column=0, sticky=tk.W, padx=(5, 1), pady=1) - # Read initial value from AppState - initial_raw_intensity = config.DEFAULT_MFD_RAW_MAP_INTENSITY # Default fallback + initial_raw_intensity = config.DEFAULT_MFD_RAW_MAP_INTENSITY try: initial_raw_intensity = self.app.state.mfd_params["raw_map_intensity"] - except Exception: + except Exception as e: logging.error( - f"{log_prefix} Could not get initial raw map intensity from AppState." + f"{log_prefix} Could not get initial raw map intensity from AppState: {e}" ) self.mfd_raw_map_intensity_var = tk.IntVar(value=initial_raw_intensity) @@ -327,7 +315,7 @@ class ControlPanel(ttk.Frame): self.mfd_params_frame.columnconfigure(1, weight=1) logging.debug( - f"{log_prefix} init_ui widget creation complete (compact layout)." + f"{log_prefix} init_ui widget creation complete." ) def set_mouse_coordinates(self, latitude_str, longitude_str): @@ -400,19 +388,12 @@ class ControlPanel(ttk.Frame): """Updates the SAR image size label.""" log_prefix = "[UI Update]" text_to_display = f"Image Size: {size_text}" - logging.debug( - f"{log_prefix} Setting SAR size label to: '{text_to_display}'" - ) + logging.debug(f"{log_prefix} Setting SAR size label to: '{text_to_display}'") try: # Check widget exists - if ( - hasattr(self, "sar_size_label") - and self.sar_size_label.winfo_exists() - ): + if hasattr(self, "sar_size_label") and self.sar_size_label.winfo_exists(): self.sar_size_label.config(text=text_to_display) - logging.debug( - f"{log_prefix} SAR size label updated successfully." - ) + logging.debug(f"{log_prefix} SAR size label updated successfully.") else: logging.warning( f"{log_prefix} SAR size label widget does not exist or is destroyed." @@ -422,10 +403,8 @@ class ControlPanel(ttk.Frame): f"{log_prefix} Could not update SAR size label (TclError: {e})" ) except Exception as e: - logging.exception( - f"{log_prefix} Unexpected error updating SAR size label:" - ) - + logging.exception(f"{log_prefix} Unexpected error updating SAR size label:") + def update_mfd_color_display(self, category_name, color_bgr_tuple): """Updates the background color of the specified MFD category's display label.""" log_prefix = "[UI Update]" # Use update prefix diff --git a/utils.py b/utils.py index c14e303..102d1be 100644 --- a/utils.py +++ b/utils.py @@ -13,28 +13,34 @@ Uses standardized logging prefixes. Drop counts are now managed within AppState. import queue import logging import math -import os # Aggiunto -import datetime # Aggiunto per timestamp -import sys # Aggiunto per platform check -import subprocess # Aggiunto per lanciare processi -import shutil # Aggiunto per trovare eseguibili (opzionale) +import os # Aggiunto +import datetime # Aggiunto per timestamp +import sys # Aggiunto per platform check +import subprocess # Aggiunto per lanciare processi +import shutil # Aggiunto per trovare eseguibili (opzionale) # Importa le librerie KML e GEO, gestendo l'ImportError try: import simplekml + _simplekml_available = True except ImportError: simplekml = None _simplekml_available = False - logging.warning("[Utils KML] Library 'simplekml' not found. KML generation disabled. (pip install simplekml)") + logging.warning( + "[Utils KML] Library 'simplekml' not found. KML generation disabled. (pip install simplekml)" + ) try: import pyproj + _pyproj_available = True except ImportError: pyproj = None _pyproj_available = False - logging.warning("[Utils KML] Library 'pyproj' not found. KML generation requires it for corner calculation. (pip install pyproj)") + logging.warning( + "[Utils KML] Library 'pyproj' not found. KML generation requires it for corner calculation. (pip install pyproj)" + ) # Removed: threading (Lock is now in AppState) @@ -216,6 +222,7 @@ def decimal_to_dms(decimal_degrees, is_latitude): ) return "Error DMS" # Return specific error string + def _calculate_geo_corners_for_kml(geo_info_radians): """ Helper interno per calcolare i corner geografici (gradi) da geo_info (radianti). @@ -223,22 +230,23 @@ def _calculate_geo_corners_for_kml(geo_info_radians): Richiede pyproj. Restituisce lista di tuple (lon, lat) in gradi o None. """ - if not _pyproj_available: return None + if not _pyproj_available: + return None log_prefix = "[Utils KML Calc]" try: geod = pyproj.Geod(ellps="WGS84") # Estrai dati necessari (gestisci KeyError) - center_lat_rad = geo_info_radians['lat'] - center_lon_rad = geo_info_radians['lon'] - orient_rad = geo_info_radians['orientation'] - ref_x = geo_info_radians['ref_x'] - ref_y = geo_info_radians['ref_y'] - scale_x = geo_info_radians['scale_x'] - scale_y = geo_info_radians['scale_y'] - width = geo_info_radians['width_px'] - height = geo_info_radians['height_px'] - - orient_rad = -orient_rad #inverse angle + center_lat_rad = geo_info_radians["lat"] + center_lon_rad = geo_info_radians["lon"] + orient_rad = geo_info_radians["orientation"] + ref_x = geo_info_radians["ref_x"] + ref_y = geo_info_radians["ref_y"] + scale_x = geo_info_radians["scale_x"] + scale_y = geo_info_radians["scale_y"] + width = geo_info_radians["width_px"] + height = geo_info_radians["height_px"] + + orient_rad = -orient_rad # inverse angle if not (scale_x > 0 and scale_y > 0 and width > 0 and height > 0): logging.error(f"{log_prefix} Invalid scale/dimensions in geo_info.") @@ -249,7 +257,7 @@ def _calculate_geo_corners_for_kml(geo_info_radians): (0 - ref_x, ref_y - 0), (width - 1 - ref_x, ref_y - 0), (width - 1 - ref_x, ref_y - (height - 1)), - (0 - ref_x, ref_y - (height - 1)) + (0 - ref_x, ref_y - (height - 1)), ] corners_meters = [(dx * scale_x, dy * scale_y) for dx, dy in corners_pixel] @@ -273,8 +281,10 @@ def _calculate_geo_corners_for_kml(geo_info_radians): distance_m = math.sqrt(dx_m_rot**2 + dy_m_rot**2) azimuth_rad = math.atan2(dx_m_rot, dy_m_rot) azimuth_deg = math.degrees(azimuth_rad) - endlon, endlat, _ = geod.fwd(center_lon_deg, center_lat_deg, azimuth_deg, distance_m) - sar_corners_geo_deg.append((endlon, endlat)) # (lon, lat) + endlon, endlat, _ = geod.fwd( + center_lon_deg, center_lat_deg, azimuth_deg, distance_m + ) + sar_corners_geo_deg.append((endlon, endlat)) # (lon, lat) if len(sar_corners_geo_deg) == 4: return sar_corners_geo_deg @@ -303,10 +313,14 @@ def generate_sar_kml(geo_info_radians, output_path) -> bool: """ log_prefix = "[Utils KML Gen]" if not _simplekml_available or not _pyproj_available: - logging.error(f"{log_prefix} Cannot generate KML: simplekml or pyproj library missing.") + logging.error( + f"{log_prefix} Cannot generate KML: simplekml or pyproj library missing." + ) return False if not geo_info_radians or not geo_info_radians.get("valid", False): - logging.warning(f"{log_prefix} Cannot generate KML: Invalid or missing GeoInfo.") + logging.warning( + f"{log_prefix} Cannot generate KML: Invalid or missing GeoInfo." + ) return False try: @@ -314,17 +328,23 @@ def generate_sar_kml(geo_info_radians, output_path) -> bool: corners_deg = _calculate_geo_corners_for_kml(geo_info_radians) if corners_deg is None: logging.error(f"{log_prefix} Failed to calculate SAR corners for KML.") - return False # Errore già loggato nella funzione helper + return False # Errore già loggato nella funzione helper # Estrai centro e orientamento (converti in gradi per KML) - center_lon_deg = math.degrees(geo_info_radians['lon']) - center_lat_deg = math.degrees(geo_info_radians['lat']) - orientation_deg = math.degrees(geo_info_radians['orientation']) # KML usa gradi + center_lon_deg = math.degrees(geo_info_radians["lon"]) + center_lat_deg = math.degrees(geo_info_radians["lat"]) + orientation_deg = math.degrees(geo_info_radians["orientation"]) # KML usa gradi # Calcola dimensione approssimativa per l'altitudine della vista - width_km = (geo_info_radians.get('scale_x', 1) * geo_info_radians.get('width_px', 1)) / 1000.0 - height_km = (geo_info_radians.get('scale_y', 1) * geo_info_radians.get('height_px', 1)) / 1000.0 - view_altitude_m = max(width_km, height_km) * 2000 # Altitudine vista = 2 * dimensione max in metri + width_km = ( + geo_info_radians.get("scale_x", 1) * geo_info_radians.get("width_px", 1) + ) / 1000.0 + height_km = ( + geo_info_radians.get("scale_y", 1) * geo_info_radians.get("height_px", 1) + ) / 1000.0 + view_altitude_m = ( + max(width_km, height_km) * 2000 + ) # Altitudine vista = 2 * dimensione max in metri # Crea oggetto KML kml = simplekml.Kml(name=f"SAR Image {datetime.datetime.now():%Y%m%d_%H%M%S}") @@ -332,9 +352,13 @@ def generate_sar_kml(geo_info_radians, output_path) -> bool: # Aggiungi LookAt per centrare la vista kml.document.lookat.longitude = center_lon_deg kml.document.lookat.latitude = center_lat_deg - kml.document.lookat.range = view_altitude_m # Distanza in metri dalla coordinata - kml.document.lookat.tilt = 45 # Angolo di vista (0=diretto verso il basso) - kml.document.lookat.heading = orientation_deg # Orientamento della camera (0=Nord) + kml.document.lookat.range = ( + view_altitude_m # Distanza in metri dalla coordinata + ) + kml.document.lookat.tilt = 45 # Angolo di vista (0=diretto verso il basso) + kml.document.lookat.heading = ( + orientation_deg # Orientamento della camera (0=Nord) + ) # Aggiungi un segnaposto al centro # placemark = kml.newpoint(name="SAR Center", coords=[(center_lon_deg, center_lat_deg)]) @@ -344,9 +368,11 @@ def generate_sar_kml(geo_info_radians, output_path) -> bool: # L'altitudine è opzionale, la mettiamo a 0 rispetto al suolo. outer_boundary = [(lon, lat, 0) for lon, lat in corners_deg] pol = kml.newpolygon(name="SAR Footprint", outerboundaryis=outer_boundary) - pol.style.linestyle.color = simplekml.Color.red # Colore linea - pol.style.linestyle.width = 2 # Spessore linea - pol.style.polystyle.color = simplekml.Color.changealphaint(100, simplekml.Color.red) # Rosso semi-trasparente per riempimento + pol.style.linestyle.color = simplekml.Color.red # Colore linea + pol.style.linestyle.width = 2 # Spessore linea + pol.style.polystyle.color = simplekml.Color.changealphaint( + 100, simplekml.Color.red + ) # Rosso semi-trasparente per riempimento # Salva il file KML logging.debug(f"{log_prefix} Saving KML to: {output_path}") @@ -372,32 +398,43 @@ def launch_google_earth(kml_path): logging.error(f"{log_prefix} Cannot launch: KML file not found at {kml_path}") return - logging.info(f"{log_prefix} Attempting to launch default KML handler for: {kml_path}") + logging.info( + f"{log_prefix} Attempting to launch default KML handler for: {kml_path}" + ) try: if sys.platform == "win32": - os.startfile(kml_path) # Metodo standard Windows per aprire un file con l'app associata - elif sys.platform == "darwin": # macOS - subprocess.run(['open', kml_path], check=True) - else: # Linux e altri Unix-like + os.startfile( + kml_path + ) # Metodo standard Windows per aprire un file con l'app associata + elif sys.platform == "darwin": # macOS + subprocess.run(["open", kml_path], check=True) + else: # Linux e altri Unix-like # Tenta di trovare google-earth-pro nel PATH - google_earth_cmd = shutil.which('google-earth-pro') + google_earth_cmd = shutil.which("google-earth-pro") if google_earth_cmd: subprocess.Popen([google_earth_cmd, kml_path]) - logging.debug(f"{log_prefix} Launched using found command: {google_earth_cmd}") + logging.debug( + f"{log_prefix} Launched using found command: {google_earth_cmd}" + ) else: # Fallback: usa xdg-open che usa l'associazione MIME - logging.debug(f"{log_prefix} 'google-earth-pro' not in PATH, using 'xdg-open'...") - subprocess.run(['xdg-open', kml_path], check=True) + logging.debug( + f"{log_prefix} 'google-earth-pro' not in PATH, using 'xdg-open'..." + ) + subprocess.run(["xdg-open", kml_path], check=True) logging.info(f"{log_prefix} Launch command issued for {kml_path}.") except FileNotFoundError: - # Questo può accadere su Linux se né google-earth-pro né xdg-open sono trovati - logging.error(f"{log_prefix} Launch command failed: Command not found (is Google Earth Pro installed and in PATH, or xdg-utils installed?)") + # Questo può accadere su Linux se né google-earth-pro né xdg-open sono trovati + logging.error( + f"{log_prefix} Launch command failed: Command not found (is Google Earth Pro installed and in PATH, or xdg-utils installed?)" + ) except subprocess.CalledProcessError as e: - # Errore da 'open' o 'xdg-open' - logging.error(f"{log_prefix} Error launching KML handler: {e}") + # Errore da 'open' o 'xdg-open' + logging.error(f"{log_prefix} Error launching KML handler: {e}") except Exception as e: logging.exception(f"{log_prefix} Unexpected error launching Google Earth:") + # --- END OF FILE utils.py ---