# --- START OF FILE map_display.py --- # map_display.py """ Manages the dedicated OpenCV window for displaying the map overlay. Handles window creation, positioning, image updates, and destruction. """ # Standard library imports import logging from typing import Optional # <<< AGGIUNTO QUESTO IMPORT # Third-party imports import cv2 import numpy as np try: from PIL import Image # For converting Pillow image to NumPy except ImportError: Image = None # Local application imports 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 def __init__( self, window_name: str = "Map Overlay", x_pos: int = 100, y_pos: int = 100 ): """ Initializes the MapDisplayWindow manager. Args: window_name (str): The name for the OpenCV window. x_pos (int): Initial X position for the window. y_pos (int): Initial Y position for the window. """ self._log_prefix = "[MapDisplay]" if Image is None: logging.critical( f"{self._log_prefix} 'Pillow' library is required but not found. Map display might fail." ) # Allow init but warn heavily, functionality will be broken # raise ImportError("'Pillow' library not found.") self.window_name = window_name self.x_pos = x_pos self.y_pos = y_pos self.window_initialized = False self._current_shape = (0, 0) # Store shape to avoid unnecessary moves logging.debug( f"{self._log_prefix} Initializing window manager for '{self.window_name}'." ) def show_map(self, map_image_pil: Optional[Image.Image]): """ Displays the provided map image (Pillow format) in the OpenCV window. Handles window creation, positioning, resizing, and placeholders. Args: 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 # --- Logging Input --- if map_image_pil is None: 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}).") 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 # Check for Pillow dependency again, crucial for processing if Image is None: 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 map_to_display_bgr = None # --- Handle None Payload: Create Placeholder --- if map_image_pil is None: logging.warning(f"{log_prefix} Generating placeholder image.") 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)) # 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 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) # draw.text((10, 10), "Map Load Failed", fill=(0,0,0)) # 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}).") except Exception as ph_err: 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.") # --- Convert Valid PIL Image Payload to NumPy BGR --- 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) # Ensure it's 3-channel (handle grayscale PIL potentially) 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}).") 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}).") elif map_image_np.ndim == 3 and map_image_np.shape[2] == 4: # 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}).") else: 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:") # 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.") # --- 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...") # Calculate aspect ratio 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) 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.") except Exception as resize_err: 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}") 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.") 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}).") # 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 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.") cv2.imshow(self.window_name, map_to_display_bgr) # Essential waitKey to process OpenCV events if called outside main loop, # but likely handled by the calling queue processor loop (process_tkinter_queue). # cv2.waitKey(1) 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 else: # Log other OpenCV errors during display 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}") # --- 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}'") 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.") except cv2.error as 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}") else: logging.debug(f"{self._log_prefix} Window '{self.window_name}' was not initialized or already destroyed.") # --- END OF FILE map_display.py ---