225 lines
12 KiB
Python
225 lines
12 KiB
Python
# --- 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 ---
|