rivista disposizione dei controlli per sar parameters, controllato gestione rotazione secondo specifiche con negazione, sistemato swtich dimensione sar con mappa attiva

This commit is contained in:
VALLONGOL 2025-04-09 09:50:22 +02:00
parent 2dd7c4715c
commit 0c93133448
11 changed files with 2670 additions and 1639 deletions

2392
app.py

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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 ---
# --- END OF FILE image_pipeline.py ---

View File

@ -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}"

View File

@ -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 ---

File diff suppressed because it is too large Load Diff

View File

@ -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 ---

View File

@ -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(

View File

@ -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 ---
# --- END OF FILE test_mode_manager.py ---

167
ui.py
View File

@ -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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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

137
utils.py
View File

@ -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 ---