# display.py """ THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. Manages the display of MFD and SAR images using OpenCV windows. Handles window creation, positioning, image updates (called from main thread), mouse event callbacks for the SAR window, and window destruction during shutdown. Uses standardized logging prefixes and levels. """ # Standard library imports import logging import queue import time from typing import Optional # For type hinting # Third-party imports import cv2 import numpy as np # Local application imports from controlpanel import config # For default sizes and other constants from controlpanel.utils.utils import put_queue class DisplayManager: """ Manages MFD and SAR OpenCV display windows and related interactions. Requires image update calls (show_mfd_image, show_sar_image) to be made from the main GUI thread. Logs operations with the [DisplayMgr] prefix. """ # --- MODIFIED FUNCTION: __init__ --- def __init__( self, app, sar_queue, mouse_queue, sar_x, sar_y, mfd_x, mfd_y, initial_sar_width: Optional[int] = None, # <<< ADDED Argument initial_sar_height: Optional[int] = None, # <<< ADDED Argument ): """ Initializes the DisplayManager. Args: app (App): Reference to the main application instance. sar_queue (queue.Queue): Queue for receiving processed SAR images. mouse_queue (queue.Queue): Queue for sending raw mouse coordinates. sar_x (int): Initial X position for the SAR window. sar_y (int): Initial Y position for the SAR window. mfd_x (int): Initial X position for the MFD window. mfd_y (int): Initial Y position for the MFD window. initial_sar_width (Optional[int]): Initial width for SAR placeholder/window. Defaults to config. initial_sar_height (Optional[int]): Initial height for SAR placeholder/window. Defaults to config. """ # Define log prefix for this class instance self.log_prefix = "[DisplayMgr]" logging.debug(f"{self.log_prefix} Initializing DisplayManager...") self.app = app # Reference to main application self.sar_queue = sar_queue self.mouse_queue = mouse_queue self.sar_x = sar_x self.sar_y = sar_y self.mfd_x = mfd_x self.mfd_y = mfd_y self.sar_window_name = "SAR" self.mfd_window_name = "MFD" self.sar_window_initialized = False self.mfd_window_initialized = False self.sar_mouse_callback_set = False # Store initial dimensions from arguments or config for placeholders # Use passed values if provided, otherwise fall back to config defaults self.initial_mfd_width = ( config.INITIAL_MFD_WIDTH ) # MFD size not changed by map overlay (yet) self.initial_mfd_height = config.INITIAL_MFD_HEIGHT self.initial_sar_width = ( initial_sar_width if initial_sar_width is not None else config.INITIAL_SAR_WIDTH ) self.initial_sar_height = ( initial_sar_height if initial_sar_height is not None else config.INITIAL_SAR_HEIGHT ) logging.debug( f"{self.log_prefix} Using Initial Sizes - MFD: {self.initial_mfd_width}x{self.initial_mfd_height}, SAR: {self.initial_sar_width}x{self.initial_sar_height}" ) logging.debug(f"{self.log_prefix} DisplayManager initialization complete.") # --- END MODIFIED FUNCTION: __init__ --- # --- MODIFIED FUNCTION: initialize_display_windows --- def initialize_display_windows(self): """ Creates and displays initial placeholder windows for MFD and SAR. Forces OpenCV windows to appear at startup with default background. MUST be called from the main GUI thread. Uses the initial dimensions stored during __init__. """ # Use INFO level for significant initialization step logging.info( f"{self.log_prefix} Initializing MFD and SAR display windows with placeholders..." ) try: # MFD Placeholder - Uses self.initial_mfd_... attributes logging.debug( f"{self.log_prefix} Creating placeholder MFD image ({self.initial_mfd_height}x{self.initial_mfd_width})." ) placeholder_mfd = np.zeros( (self.initial_mfd_height, self.initial_mfd_width, 3), dtype=np.uint8 ) placeholder_mfd[:, :] = [30, 30, 30] # Dark Gray BGR logging.debug(f"{self.log_prefix} Attempting to show placeholder MFD...") # show_mfd_image logs internally self.show_mfd_image(placeholder_mfd) # Log success after the call if window became initialized if self.mfd_window_initialized: logging.info( f"{self.log_prefix} MFD window '{self.mfd_window_name}' initialized with placeholder." ) else: logging.warning( f"{self.log_prefix} MFD window initialization attempt finished, but window not marked as initialized." ) except Exception as e: # Use ERROR for failure during initialization logging.error( f"{self.log_prefix} Failed to initialize MFD window with placeholder: {e}", exc_info=True, ) try: # SAR Placeholder - Uses self.initial_sar_... attributes logging.debug( f"{self.log_prefix} Creating placeholder SAR image ({self.initial_sar_height}x{self.initial_sar_width})." ) # Ensure placeholder is BGR (3 channels) as show_sar_image might receive colorized images later placeholder_sar = np.zeros( (self.initial_sar_height, self.initial_sar_width, 3), dtype=np.uint8 ) placeholder_sar[:, :] = [60, 60, 60] # Lighter Gray BGR logging.debug(f"{self.log_prefix} Attempting to show placeholder SAR...") # show_sar_image logs internally self.show_sar_image(placeholder_sar) # Log success after the call if self.sar_window_initialized: logging.info( f"{self.log_prefix} SAR window '{self.sar_window_name}' initialized with placeholder." ) else: logging.warning( f"{self.log_prefix} SAR window initialization attempt finished, but window not marked as initialized." ) except Exception as e: # Use ERROR for failure during initialization logging.error( f"{self.log_prefix} Failed to initialize SAR window with placeholder: {e}", exc_info=True, ) # --- END MODIFIED FUNCTION: initialize_display_windows --- def show_mfd_image(self, image): """ Displays the MFD image in its OpenCV window. Creates/moves window on first call. MUST be called from the main GUI thread. Includes cv2.waitKey(1). Args: image (numpy.ndarray): The MFD image to display (BGR format). """ # Use DEBUG for start of display function logging.debug( f"{self.log_prefix} Received request to show MFD image (Shape: {getattr(image, 'shape', 'N/A')})." ) # Validate input - Use ERROR for invalid input causing failure if image is None or not isinstance(image, np.ndarray) or image.size == 0: logging.error( f"{self.log_prefix} show_mfd_image received invalid image data. Aborting display." ) return if image.ndim != 3 or image.shape[2] != 3: logging.error( f"{self.log_prefix} show_mfd_image received non-BGR image (ndim={image.ndim}, channels={image.shape[2] if image.ndim==3 else 'N/A'}). Aborting display." ) return try: window_name = self.mfd_window_name # Create and move window only once if not self.mfd_window_initialized: # DEBUG for first-time setup logging.debug( f"{self.log_prefix} First call for '{window_name}'. Creating and positioning window." ) # Create window explicitly with WINDOW_AUTOSIZE to ensure exact fit (484x484) cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE) cv2.imshow(window_name, image) try: cv2.moveWindow(window_name, self.mfd_x, self.mfd_y) self.mfd_window_initialized = ( True # Set flag AFTER successful operations ) # INFO log for successful window creation/move logging.info( f"{self.log_prefix} '{window_name}' window created and moved to ({self.mfd_x}, {self.mfd_y})." ) cv2.waitKey(1) # Allow window to draw/position except cv2.error as move_e: # WARNING if move fails but window might still exist logging.warning( f"{self.log_prefix} Could not move '{window_name}' window on initialization: {move_e}. Window might appear elsewhere." ) # Set flag true anyway, assuming imshow worked. self.mfd_window_initialized = True else: # Just update the image - DEBUG for subsequent updates logging.debug( f"{self.log_prefix} Updating existing MFD window '{window_name}'." ) cv2.imshow(window_name, image) # Essential waitKey - DEBUG for calling waitKey # logging.debug(f"{self.log_prefix} Calling waitKey(1) for MFD window.") # Less verbose cv2.waitKey(1) except cv2.error as e: # Handle cases where window might have been closed manually # Use WARNING as it's recoverable if "NULL window" in str(e) or "invalid window" in str(e): logging.warning( f"{self.log_prefix} OpenCV window '{window_name}' seems closed. Will re-initialize on next valid image." ) self.mfd_window_initialized = False # Reset flag else: # Keep EXCEPTION for other OpenCV errors logging.exception( f"{self.log_prefix} OpenCV error displaying MFD image: {e}" ) except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception( f"{self.log_prefix} Unexpected error displaying MFD image: {e}" ) def show_sar_image(self, image): """ Displays the SAR image in its OpenCV window. Handles first-time setup (creation, move, mouse callback) and subsequent updates. MUST be called from the main GUI thread. Includes cv2.waitKey(1). Args: image (numpy.ndarray): The SAR image (grayscale or BGR colorized). """ # DEBUG for start of display function logging.debug( f"{self.log_prefix} Received request to show SAR image (Shape: {getattr(image, 'shape', 'N/A')})." ) # Validate input - ERROR for invalid input if image is None or not isinstance(image, np.ndarray) or image.size == 0: logging.error( f"{self.log_prefix} show_sar_image received invalid image data. Aborting display." ) return if image.shape[0] == 0 or image.shape[1] == 0: logging.error( f"{self.log_prefix} show_sar_image received image with invalid dimensions: {image.shape}. Aborting." ) return # Basic check for supported types (grayscale or BGR) if not (image.ndim == 2 or (image.ndim == 3 and image.shape[2] == 3)): logging.error( f"{self.log_prefix} show_sar_image received unsupported image format (ndim={image.ndim}, channels={image.shape[2] if image.ndim==3 else 'N/A'}). Expected Grayscale or BGR. Aborting." ) return try: window_name = self.sar_window_name # Create, move window, and set callback only once if not self.sar_window_initialized: # DEBUG for first-time setup logging.debug( f"{self.log_prefix} First call for '{window_name}'. Creating, positioning window, and setting callback." ) # Create window explicitly with WINDOW_AUTOSIZE for exact image fit cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE) cv2.imshow(window_name, image) try: cv2.moveWindow(window_name, self.sar_x, self.sar_y) self.sar_window_initialized = True # Mark initialized # INFO for successful window creation/move logging.info( f"{self.log_prefix} '{window_name}' window created and moved to ({self.sar_x}, {self.sar_y})." ) # Set mouse callback immediately after successful initialization if not self.sar_mouse_callback_set: cv2.setMouseCallback(window_name, self.sar_mouse_callback) self.sar_mouse_callback_set = True # INFO for successful callback setup logging.info( f"{self.log_prefix} Mouse callback set for '{window_name}'." ) cv2.waitKey(1) # Allow window to draw/position except cv2.error as move_e: # WARNING if move fails logging.warning( f"{self.log_prefix} Could not move '{window_name}' window on initialization: {move_e}." ) self.sar_window_initialized = True # Assume imshow worked # Still try setting callback even if move failed if not self.sar_mouse_callback_set: try: cv2.setMouseCallback(window_name, self.sar_mouse_callback) self.sar_mouse_callback_set = True logging.info( f"{self.log_prefix} Mouse callback set for '{window_name}' (after move failed)." ) except cv2.error as cb_e: # ERROR if callback setting fails logging.error( f"{self.log_prefix} Failed to set mouse callback for '{window_name}': {cb_e}" ) else: # Just update the image - DEBUG for subsequent updates logging.debug( f"{self.log_prefix} Updating existing SAR window '{window_name}'." ) cv2.imshow(window_name, image) # Essential waitKey - DEBUG for calling waitKey # logging.debug(f"{self.log_prefix} Calling waitKey(1) for SAR window.") # Less verbose cv2.waitKey(1) except cv2.error as e: # WARNING for recoverable closed window error if "NULL window" in str(e) or "invalid window" in str(e): logging.warning( f"{self.log_prefix} OpenCV window '{self.sar_window_name}' seems closed. Will re-initialize on next valid image." ) self.sar_window_initialized = False self.sar_mouse_callback_set = False # Reset callback flag too else: # Keep EXCEPTION for other OpenCV errors logging.exception( f"{self.log_prefix} OpenCV error displaying SAR image: {e}" ) except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception( f"{self.log_prefix} Unexpected error displaying SAR image: {e}" ) # Method inside DisplayManager class in display.py def sar_mouse_callback(self, event, x, y, flags, param): """ OpenCV mouse callback for the SAR window. Processes left mouse clicks, clamps coordinates, and puts raw (x, y) onto the application's mouse_queue. Updates only on click, removing the need for rate limiting. """ # --- MODIFIED: Process only left button down events --- if event == cv2.EVENT_LBUTTONDOWN: log_prefix_cb = f"{self.log_prefix} MouseClick" logging.debug( f"{log_prefix_cb} SAR Window Left Click detected at ({x}, {y})" ) try: current_display_width = self.app.state.sar_display_width current_display_height = self.app.state.sar_display_height if current_display_width <= 0 or current_display_height <= 0: return x_clamped = max(0, min(x, current_display_width - 1)) y_clamped = max(0, min(y, current_display_height - 1)) # --- Action 1: Queue raw coords for geo calculation (original queue) --- logging.debug( f"{log_prefix_cb} Putting clamped coords ({x_clamped}, {y_clamped}) onto mouse_queue for geo calc." ) put_queue( self.app.mouse_queue, (x_clamped, y_clamped), queue_name="mouse", app_instance=self.app, ) # --- >>> START OF NEW ACTION <<< --- # --- Action 2: Queue pixel coords to update marker state (tkinter queue) --- click_command = "SAR_CLICK_UPDATE" click_payload = (x_clamped, y_clamped) logging.debug( f"{log_prefix_cb} Putting command '{click_command}' payload {click_payload} onto tkinter_queue for marker state." ) put_queue( queue_obj=self.app.tkinter_queue, item=(click_command, click_payload), queue_name="tkinter", app_instance=self.app, ) # --- >>> END OF NEW ACTION <<< --- except Exception as e: logging.exception(f"{log_prefix_cb} Error in SAR mouse callback: {e}") def update_sar_position(self, x, y): """Updates the stored position and attempts to move the SAR window.""" # DEBUG for position update request logging.debug(f"{self.log_prefix} Updating SAR window position to ({x}, {y}).") self.sar_x = x self.sar_y = y if self.sar_window_initialized: try: cv2.moveWindow(self.sar_window_name, self.sar_x, self.sar_y) # INFO for successful move logging.info( f"{self.log_prefix} Moved SAR window '{self.sar_window_name}' to ({x}, {y})" ) except cv2.error as e: # WARNING if move fails logging.warning( f"{self.log_prefix} Could not move SAR window '{self.sar_window_name}': {e}" ) else: logging.debug( f"{self.log_prefix} SAR window not initialized, cannot move yet." ) def update_mfd_position(self, x, y): """Updates the stored position and attempts to move the MFD window.""" # DEBUG for position update request logging.debug(f"{self.log_prefix} Updating MFD window position to ({x}, {y}).") self.mfd_x = x self.mfd_y = y if self.mfd_window_initialized: try: cv2.moveWindow(self.mfd_window_name, self.mfd_x, self.mfd_y) # INFO for successful move logging.info( f"{self.log_prefix} Moved MFD window '{self.mfd_window_name}' to ({x}, {y})" ) except cv2.error as e: # WARNING if move fails logging.warning( f"{self.log_prefix} Could not move MFD window '{self.mfd_window_name}': {e}" ) else: logging.debug( f"{self.log_prefix} MFD window not initialized, cannot move yet." ) def destroy_windows(self): """Explicitly destroys the managed OpenCV windows during application shutdown.""" # INFO for start of significant cleanup action logging.info( f"{self.log_prefix} Attempting to destroy managed OpenCV windows (MFD, SAR)..." ) destroyed_count = 0 # Attempt to destroy SAR window # Check name existence as fallback if init flag is somehow false if self.sar_window_initialized or hasattr(self, "sar_window_name"): try: # DEBUG for specific window destruction logging.debug( f"{self.log_prefix} Destroying OpenCV window: '{self.sar_window_name}'" ) cv2.destroyWindow(self.sar_window_name) destroyed_count += 1 self.sar_window_initialized = False # Reset flags self.sar_mouse_callback_set = False except cv2.error as e: # WARNING for known error if window already closed logging.warning( f"{self.log_prefix} Ignoring OpenCV error destroying window '{self.sar_window_name}' (may already be closed): {e}" ) except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception( f"{self.log_prefix} Unexpected error destroying window '{self.sar_window_name}': {e}" ) # Attempt to destroy MFD window if self.mfd_window_initialized or hasattr(self, "mfd_window_name"): try: # DEBUG for specific window destruction logging.debug( f"{self.log_prefix} Destroying OpenCV window: '{self.mfd_window_name}'" ) cv2.destroyWindow(self.mfd_window_name) destroyed_count += 1 self.mfd_window_initialized = False # Reset flag except cv2.error as e: # WARNING for known error logging.warning( f"{self.log_prefix} Ignoring OpenCV error destroying window '{self.mfd_window_name}' (may already be closed): {e}" ) except Exception as e: # Keep EXCEPTION for unexpected errors logging.exception( f"{self.log_prefix} Unexpected error destroying window '{self.mfd_window_name}': {e}" ) # INFO for completion of cleanup action logging.info( f"{self.log_prefix} Finished destroying managed OpenCV windows (attempted {destroyed_count})." )