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

1332
app.py

File diff suppressed because it is too large Load Diff

View File

@ -165,14 +165,16 @@ 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
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

View File

@ -30,8 +30,10 @@ from image_processing import (
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,65 +98,89 @@ 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.")
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.")
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.")
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.")
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.")
logging.error(
f"{log_prefix} Failed to convert input SAR to uint8: {e}. Aborting."
)
return
else:
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
original_orient_rad = (
geo_info.get("orientation", 0.0) if is_geo_valid else 0.0
)
orient_rad = -orient_rad # invert angle
# 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
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
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}")
@ -185,7 +213,7 @@ class ImagePipeline:
# 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
border_color = [0, 0, 0] if img.ndim == 3 else 0
# Perform affine warp
rotated_img = cv2.warpAffine(
img,
@ -193,7 +221,7 @@ class ImagePipeline:
(w, h), # Output size same as input
flags=cv2.INTER_LINEAR, # Linear interpolation
borderMode=cv2.BORDER_CONSTANT,
borderValue=border_color # Fill borders with black
borderValue=border_color, # Fill borders with black
)
logging.debug(f"{log_prefix} Rotation successful.")
return rotated_img
@ -202,7 +230,9 @@ class ImagePipeline:
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]:
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.
@ -216,8 +246,10 @@ class ImagePipeline:
"""
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}).")
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
@ -226,15 +258,18 @@ class ImagePipeline:
return img
# Perform resize using the function from image_processing
resized_img = resize_image(img, target_w, target_h) # This function handles logging
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).")
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):
"""
Applies the MFD LUT from AppState to the indices in AppState and queues the result.
@ -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...")
@ -270,10 +307,13 @@ class ImagePipeline:
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:
@ -287,16 +327,19 @@ class ImagePipeline:
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 = "-","-"
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.
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(

View File

@ -72,16 +72,22 @@ class MapDisplayWindow:
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}).")
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.")
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):
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,12 +120,20 @@ 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
@ -123,38 +144,58 @@ 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
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
@ -163,29 +204,39 @@ class MapDisplayWindow:
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)
# 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}.")
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.")
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 ---

View File

@ -18,6 +18,7 @@ from typing import Optional, Dict, Any, Tuple, List
# Third-party imports
import numpy as np
try:
from PIL import Image
except ImportError:
@ -37,6 +38,7 @@ import cv2
import config
from app_state import AppState
from utils import put_queue
# Map specific modules that this manager orchestrates
from map_services import get_map_service, BaseMapService
from map_manager import MapTileManager
@ -44,12 +46,13 @@ from map_utils import (
get_bounding_box_from_center_size,
get_tile_ranges_for_bbox,
MapCalculationError,
calculate_meters_per_pixel
calculate_meters_per_pixel,
)
from map_display import MapDisplayWindow
# Forward declaration for type hinting App instance
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app import App
@ -61,7 +64,7 @@ class MapIntegrationManager:
self,
app_state: AppState,
tkinter_queue: queue.Queue,
app: 'App',
app: "App",
map_x: int,
map_y: int,
):
@ -74,12 +77,15 @@ class MapIntegrationManager:
self._app_state: AppState = app_state
self._tkinter_queue: queue.Queue = tkinter_queue
self._app: 'App' = app
self._app: "App" = app
# --- Dependency Checks ---
if Image is None: raise ImportError("Pillow library not found")
if mercantile is None: raise ImportError("mercantile library not found")
if pyproj is None: raise ImportError("pyproj library not found")
if Image is None:
raise ImportError("Pillow library not found")
if mercantile is None:
raise ImportError("mercantile library not found")
if pyproj is None:
raise ImportError("pyproj library not found")
# --- Initialize Attributes ---
self._map_service: Optional[BaseMapService] = None
@ -96,18 +102,22 @@ class MapIntegrationManager:
# --- Initialize Other Map Components ---
# 1. Get Map Service
service_name = getattr(config, 'MAP_SERVICE_PROVIDER', 'osm')
api_key = getattr(config, 'MAP_API_KEY', None)
service_name = getattr(config, "MAP_SERVICE_PROVIDER", "osm")
api_key = getattr(config, "MAP_API_KEY", None)
self._map_service = get_map_service(service_name, api_key)
if not self._map_service:
raise ValueError(f"Failed to get map service '{service_name}'.")
logging.debug(f"{self._log_prefix} Map service '{self._map_service.name}' loaded.")
logging.debug(
f"{self._log_prefix} Map service '{self._map_service.name}' loaded."
)
# 2. Create Tile Manager
self._map_tile_manager = MapTileManager(
map_service=self._map_service,
cache_base_dir=getattr(config, 'MAP_CACHE_DIRECTORY', None),
enable_online_fetching=getattr(config, 'ENABLE_ONLINE_MAP_FETCHING', None)
cache_base_dir=getattr(config, "MAP_CACHE_DIRECTORY", None),
enable_online_fetching=getattr(
config, "ENABLE_ONLINE_MAP_FETCHING", None
),
)
logging.debug(f"{self._log_prefix} MapTileManager created.")
@ -115,7 +125,9 @@ class MapIntegrationManager:
self._map_display_window = MapDisplayWindow(
window_name="Map Overlay", x_pos=map_x, y_pos=map_y
)
logging.debug(f"{self._log_prefix} MapDisplayWindow created at ({map_x},{map_y}).")
logging.debug(
f"{self._log_prefix} MapDisplayWindow created at ({map_x},{map_y})."
)
# 4. Trigger Initial Map Display in Background Thread
logging.debug(f"{self._log_prefix} Starting initial map display thread...")
@ -124,11 +136,15 @@ class MapIntegrationManager:
self._map_initial_display_thread = threading.Thread(
target=self._display_initial_map_area_thread,
name="InitialMapDisplayThread",
daemon=True
daemon=True,
)
self._map_initial_display_thread.start()
except (ImportError, ValueError, pyproj.exceptions.CRSError) as init_err: # Catch pyproj errors too
except (
ImportError,
ValueError,
pyproj.exceptions.CRSError,
) as init_err: # Catch pyproj errors too
logging.critical(f"{self._log_prefix} Initialization failed: {init_err}")
# Ensure components are None if init fails midway
self._geod = None
@ -137,14 +153,15 @@ class MapIntegrationManager:
self._map_display_window = None
raise # Re-raise critical errors
except Exception as e:
logging.exception(f"{self._log_prefix} Unexpected error during initialization:")
logging.exception(
f"{self._log_prefix} Unexpected error during initialization:"
)
self._geod = None
self._map_service = None
self._map_tile_manager = None
self._map_display_window = None
raise # Re-raise other unexpected errors # Re-raise other unexpected errors
def _display_initial_map_area_thread(self):
"""
(Runs in background thread) Calculates the initial map area based on default
@ -158,7 +175,9 @@ class MapIntegrationManager:
if config.SAR_CENTER_LAT == 0.0 and config.SAR_CENTER_LON == 0.0:
# ... (codice per saltare e aggiornare lo stato, come prima) ...
# ... (assicurati che questa parte sia corretta come nella risposta precedente) ...
logging.debug(f"{log_prefix} Initial map display skipped based on config defaults (0,0). Waiting for valid GeoInfo.")
logging.debug(
f"{log_prefix} Initial map display skipped based on config defaults (0,0). Waiting for valid GeoInfo."
)
if not self._app_state.shutting_down:
status_msg = "Status Unavailable" # Default
try:
@ -169,8 +188,10 @@ class MapIntegrationManager:
else:
socket_ok = False
listening_info = "Error: No Network Socket"
if hasattr(self._app, 'udp_socket') and self._app.udp_socket:
if hasattr(self._app, 'local_ip') and hasattr(self._app, 'local_port'):
if hasattr(self._app, "udp_socket") and self._app.udp_socket:
if hasattr(self._app, "local_ip") and hasattr(
self._app, "local_port"
):
listening_info = f"Listening UDP {self._app.local_ip}:{self._app.local_port}"
socket_ok = True
else:
@ -179,18 +200,26 @@ class MapIntegrationManager:
status_msg = listening_info
status_msg += " | Map Ready (Waiting for GeoData)"
except Exception as e:
logging.exception(f"{log_prefix} Unexpected error determining status message:")
status_msg = "Error Getting Status | Map Ready (Waiting for GeoData)"
logging.exception(
f"{log_prefix} Unexpected error determining status message:"
)
status_msg = (
"Error Getting Status | Map Ready (Waiting for GeoData)"
)
self._app.set_status(status_msg)
return # Esce dal thread
# Se le coordinate di default *non* sono (0,0), procedi
logging.debug(f"{log_prefix} Calculating initial map area based on non-zero config defaults...")
logging.debug(
f"{log_prefix} Calculating initial map area based on non-zero config defaults..."
)
# Check dependencies
if not (self._map_tile_manager and self._map_display_window):
logging.error(f"{log_prefix} Map components not initialized. Aborting thread.")
put_queue(self._tkinter_queue, ('SHOW_MAP', None), "tkinter", self._app)
logging.error(
f"{log_prefix} Map components not initialized. Aborting thread."
)
put_queue(self._tkinter_queue, ("SHOW_MAP", None), "tkinter", self._app)
return
if self._app_state.shutting_down:
logging.debug(f"{log_prefix} Shutdown detected. Aborting.")
@ -230,8 +259,12 @@ class MapIntegrationManager:
if map_image_pil:
logging.debug(f"{log_prefix} Initial map area stitched successfully.")
center_lat_deg = config.SAR_CENTER_LAT # Usa il default per la mappa iniziale
map_image_pil = self._draw_scale_bar(map_image_pil, center_lat_deg, zoom)
center_lat_deg = (
config.SAR_CENTER_LAT
) # Usa il default per la mappa iniziale
map_image_pil = self._draw_scale_bar(
map_image_pil, center_lat_deg, zoom
)
else:
logging.error(f"{log_prefix} Failed to stitch initial map area.")
@ -243,12 +276,20 @@ class MapIntegrationManager:
map_image_pil = None
finally:
if not self._app_state.shutting_down:
logging.debug(f"{log_prefix} Queueing SHOW_MAP command for initial map.")
put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_pil), "tkinter", self._app)
logging.debug(
f"{log_prefix} Queueing SHOW_MAP command for initial map."
)
put_queue(
self._tkinter_queue,
("SHOW_MAP", map_image_pil),
"tkinter",
self._app,
)
logging.debug(f"{log_prefix} Initial map display thread finished.")
def update_map_overlay(self, sar_normalized_uint8: np.ndarray, geo_info_radians: Dict[str, Any]):
def update_map_overlay(
self, sar_normalized_uint8: np.ndarray, geo_info_radians: Dict[str, Any]
):
"""
Calculates the map overlay based on current SAR data.
Currently fetches map tiles and draws the SAR bounding box.
@ -270,13 +311,19 @@ class MapIntegrationManager:
# +++ DETAILED COMPONENT CHECK +++
if not self._map_tile_manager:
logging.warning(f"{log_prefix} Skipping update: _map_tile_manager is not available (None or evaluates False).")
logging.warning(
f"{log_prefix} Skipping update: _map_tile_manager is not available (None or evaluates False)."
)
return
if not self._map_display_window:
logging.warning(f"{log_prefix} Skipping update: _map_display_window is not available (None or evaluates False).")
logging.warning(
f"{log_prefix} Skipping update: _map_display_window is not available (None or evaluates False)."
)
return
if not self._geod:
logging.warning(f"{log_prefix} Skipping update: _geod (geodetic calculator) is not available (None or evaluates False).")
logging.warning(
f"{log_prefix} Skipping update: _geod (geodetic calculator) is not available (None or evaluates False)."
)
return
# +++ END DETAILED CHECK +++
@ -286,11 +333,15 @@ class MapIntegrationManager:
return
# Check libraries (redundant if init succeeded, but safe)
if Image is None or mercantile is None or pyproj is None:
logging.error(f"{log_prefix} Skipping update: Missing required map libraries.")
logging.error(
f"{log_prefix} Skipping update: Missing required map libraries."
)
return
# Log start of calculation
logging.debug(f"{log_prefix} Starting map overlay calculation (Phase 1: BBox)...")
logging.debug(
f"{log_prefix} Starting map overlay calculation (Phase 1: BBox)..."
)
# Initialize variables to store map images
map_image_with_overlay: Optional[Image.Image] = None
stitched_map_image: Optional[Image.Image] = None # Store the base map
@ -298,15 +349,17 @@ class MapIntegrationManager:
try:
# --- Calculate SAR Footprint Parameters ---
# Extract center coordinates (convert back to degrees for utility functions)
center_lat_deg = math.degrees(geo_info_radians.get('lat', 0.0))
center_lon_deg = math.degrees(geo_info_radians.get('lon', 0.0))
center_lat_deg = math.degrees(geo_info_radians.get("lat", 0.0))
center_lon_deg = math.degrees(geo_info_radians.get("lon", 0.0))
# Extract scale and dimensions to calculate size
scale_x = geo_info_radians.get('scale_x', 0.0)
width_px = geo_info_radians.get('width_px', 0)
scale_x = geo_info_radians.get("scale_x", 0.0)
width_px = geo_info_radians.get("width_px", 0)
# Calculate size in KM, using default from config as fallback
if scale_x > 0 and width_px > 0:
size_km = (scale_x * width_px) / 1000.0
logging.debug(f"{log_prefix} Calculated approximate size based on scale_x * width_px: {size_km:.2f} km")
logging.debug(
f"{log_prefix} Calculated approximate size based on scale_x * width_px: {size_km:.2f} km"
)
else:
logging.error(
f"{log_prefix} Invalid scale_x ({scale_x}) or width_px ({width_px}) in received GeoInfo. "
@ -318,8 +371,12 @@ class MapIntegrationManager:
# --- Fetch and Stitch Base Map ---
# 1. Calculate Geographic Bounding Box for fetching tiles
logging.debug(f"{log_prefix} Calculating map tile BBox for center ({center_lat_deg:.4f},{center_lon_deg:.4f}), size {size_km*1.2:.1f}km.")
fetch_bbox = get_bounding_box_from_center_size(center_lat_deg, center_lon_deg, size_km * 1.2)
logging.debug(
f"{log_prefix} Calculating map tile BBox for center ({center_lat_deg:.4f},{center_lon_deg:.4f}), size {size_km*1.2:.1f}km."
)
fetch_bbox = get_bounding_box_from_center_size(
center_lat_deg, center_lon_deg, size_km * 1.2
)
if fetch_bbox is None:
raise MapCalculationError("Tile Bounding Box calculation failed.")
@ -331,77 +388,124 @@ class MapIntegrationManager:
# --- Check shutdown flag before potentially long operation ---
if self._app_state.shutting_down:
logging.debug(f"{log_prefix} Shutdown detected before stitching base map.")
logging.debug(
f"{log_prefix} Shutdown detected before stitching base map."
)
return
# 3. Stitch Background Map Image using MapTileManager
logging.debug(f"{log_prefix} Stitching base map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})...")
stitched_map_image = self._map_tile_manager.stitch_map_image(zoom, tile_ranges[0], tile_ranges[1])
logging.debug(
f"{log_prefix} Stitching base map tiles (Zoom: {zoom}, X: {tile_ranges[0]}, Y: {tile_ranges[1]})..."
)
stitched_map_image = self._map_tile_manager.stitch_map_image(
zoom, tile_ranges[0], tile_ranges[1]
)
# --- Validate Stitched Image and Log ---
if stitched_map_image is None:
logging.error(f"{log_prefix} MapTileManager.stitch_map_image returned None. Cannot proceed.")
logging.error(
f"{log_prefix} MapTileManager.stitch_map_image returned None. Cannot proceed."
)
map_image_with_overlay = None
raise MapCalculationError("Failed to stitch base map image.")
else:
logging.debug(f"{log_prefix} Base map stitched successfully (PIL Size: {stitched_map_image.size}, Mode: {stitched_map_image.mode}).")
logging.debug(
f"{log_prefix} Base map stitched successfully (PIL Size: {stitched_map_image.size}, Mode: {stitched_map_image.mode})."
)
map_image_with_overlay = stitched_map_image.copy()
# --- Check shutdown flag after stitching ---
if self._app_state.shutting_down:
logging.debug(f"{log_prefix} Shutdown detected after stitching base map.")
logging.debug(
f"{log_prefix} Shutdown detected after stitching base map."
)
return
# --- Calculate and Draw SAR Bounding Box ---
# 4. Calculate Geographic Coordinates of SAR Corners
logging.debug(f"{log_prefix} Calculating SAR corner geographic coordinates...")
logging.debug(
f"{log_prefix} Calculating SAR corner geographic coordinates..."
)
sar_corners_deg = self._calculate_sar_corners_geo(geo_info_radians)
if sar_corners_deg is None:
raise MapCalculationError("SAR corner geographic coordinate calculation failed.")
raise MapCalculationError(
"SAR corner geographic coordinate calculation failed."
)
# 5. Convert SAR Corner Geographic Coords to Pixel Coords on Stitched Map
logging.debug(f"{log_prefix} Converting SAR corners to map pixel coordinates...")
top_left_tile = mercantile.Tile(x=tile_ranges[0][0], y=tile_ranges[1][0], z=zoom)
logging.debug(
f"{log_prefix} Converting SAR corners to map pixel coordinates..."
)
top_left_tile = mercantile.Tile(
x=tile_ranges[0][0], y=tile_ranges[1][0], z=zoom
)
map_display_bounds = mercantile.bounds(top_left_tile)
sar_corners_pixels = self._geo_coords_to_map_pixels(
coords_deg=sar_corners_deg,
map_bounds=map_display_bounds,
map_tile_ranges=tile_ranges,
zoom=zoom,
stitched_map_shape=map_image_with_overlay.size[::-1]
stitched_map_shape=map_image_with_overlay.size[::-1],
)
if sar_corners_pixels is None:
raise MapCalculationError("SAR corner to map pixel conversion failed.")
# 6. Draw the SAR Bounding Box Polygon on the map image copy
logging.debug(f"{log_prefix} Drawing SAR bounding box polygon on map image...")
logging.debug(
f"{log_prefix} Drawing SAR bounding box polygon on map image..."
)
try:
map_cv = cv2.cvtColor(np.array(map_image_with_overlay), cv2.COLOR_RGB2BGR)
map_cv = cv2.cvtColor(
np.array(map_image_with_overlay), cv2.COLOR_RGB2BGR
)
pts = np.array(sar_corners_pixels, np.int32).reshape((-1, 1, 2))
cv2.polylines(map_cv, [pts], isClosed=True, color=(0, 0, 255), thickness=2)
map_image_with_overlay = Image.fromarray(cv2.cvtColor(map_cv, cv2.COLOR_BGR2RGB))
logging.debug(f"{log_prefix} SAR bounding box drawn successfully on map.")
cv2.polylines(
map_cv, [pts], isClosed=True, color=(0, 0, 255), thickness=2
)
map_image_with_overlay = Image.fromarray(
cv2.cvtColor(map_cv, cv2.COLOR_BGR2RGB)
)
logging.debug(
f"{log_prefix} SAR bounding box drawn successfully on map."
)
except Exception as draw_err:
logging.exception(f"{log_prefix} Error drawing SAR bounding box on map:")
logging.exception(
f"{log_prefix} Error drawing SAR bounding box on map:"
)
map_image_with_overlay = stitched_map_image
logging.warning(f"{log_prefix} Proceeding with map display without SAR bounding box due to drawing error.")
logging.warning(
f"{log_prefix} Proceeding with map display without SAR bounding box due to drawing error."
)
current_center_lat_deg = math.degrees(geo_info_radians['lat']) # Usa la lat attuale
map_image_with_overlay = self._draw_scale_bar(map_image_with_overlay, current_center_lat_deg, zoom)
current_center_lat_deg = math.degrees(
geo_info_radians["lat"]
) # Usa la lat attuale
map_image_with_overlay = self._draw_scale_bar(
map_image_with_overlay, current_center_lat_deg, zoom
)
except MapCalculationError as e:
logging.error(f"{log_prefix} Map overlay calculation failed: {e}")
map_image_with_overlay = stitched_map_image
except Exception as e:
logging.exception(f"{log_prefix} Unexpected error during map overlay update:")
logging.exception(
f"{log_prefix} Unexpected error during map overlay update:"
)
map_image_with_overlay = stitched_map_image
finally:
# --- Queue Result for Main Thread Display ---
if not self._app_state.shutting_down:
payload_type = type(map_image_with_overlay)
payload_size = getattr(map_image_with_overlay, 'size', 'N/A')
logging.debug(f"{log_prefix} Queueing SHOW_MAP command for updated map overlay. Payload Type: {payload_type}, Size: {payload_size}")
put_queue(self._tkinter_queue, ('SHOW_MAP', map_image_with_overlay), "tkinter", self._app)
payload_size = getattr(map_image_with_overlay, "size", "N/A")
logging.debug(
f"{log_prefix} Queueing SHOW_MAP command for updated map overlay. Payload Type: {payload_type}, Size: {payload_size}"
)
put_queue(
self._tkinter_queue,
("SHOW_MAP", map_image_with_overlay),
"tkinter",
self._app,
)
else:
logging.debug(f"{log_prefix} Skipping queue put due to shutdown.")
@ -421,9 +525,13 @@ class MapIntegrationManager:
# Update app status only *after* the initial map load attempt completes
self._update_app_status_after_map_load(map_image_pil is not None)
except Exception as e:
logging.exception(f"{log_prefix} Error calling MapDisplayWindow.show_map:")
logging.exception(
f"{log_prefix} Error calling MapDisplayWindow.show_map:"
)
else:
logging.warning(f"{log_prefix} Map display window not available. Cannot display map.")
logging.warning(
f"{log_prefix} Map display window not available. Cannot display map."
)
def _update_app_status_after_map_load(self, success: bool):
"""Updates the main application status after the initial map load attempt."""
@ -431,23 +539,40 @@ class MapIntegrationManager:
try:
# Check if the status bar still shows the loading message
# Access status bar via self._app reference
if hasattr(self._app, 'statusbar') and "Loading initial map" in self._app.statusbar.cget("text"):
if hasattr(
self._app, "statusbar"
) and "Loading initial map" in self._app.statusbar.cget("text"):
# Determine the final status based on success and current mode
if success:
current_mode = self._app.state.test_mode_active # Check current mode
status_msg = "Ready (Test Mode)" if current_mode else \
("Ready (Local Mode)" if config.USE_LOCAL_IMAGES else \
(f"Listening UDP {self._app.local_ip}:{self._app.local_port}" if self._app.udp_socket else "Error: No Socket"))
current_mode = (
self._app.state.test_mode_active
) # Check current mode
status_msg = (
"Ready (Test Mode)"
if current_mode
else (
"Ready (Local Mode)"
if config.USE_LOCAL_IMAGES
else (
f"Listening UDP {self._app.local_ip}:{self._app.local_port}"
if self._app.udp_socket
else "Error: No Socket"
)
)
)
else:
status_msg = "Error Loading Map"
logging.debug(f"{log_prefix} Initial map load finished (Success: {success}). Setting App status to: '{status_msg}'")
logging.debug(
f"{log_prefix} Initial map load finished (Success: {success}). Setting App status to: '{status_msg}'"
)
self._app.set_status(status_msg) # Use App's method for thread safety
#else: # Status already updated by something else, do nothing
# else: # Status already updated by something else, do nothing
# logging.debug(f"{log_prefix} Skipping status update, map loading message not present.")
except Exception as e:
logging.warning(f"{log_prefix} Error checking/updating app status after map load: {e}")
logging.warning(
f"{log_prefix} Error checking/updating app status after map load: {e}"
)
def shutdown(self):
"""Cleans up map-related resources, like closing the display window."""
@ -491,20 +616,22 @@ class MapIntegrationManager:
logging.debug(f"{log_prefix} Calculating SAR corner geographic coordinates...")
if not self._geod:
logging.error(f"{log_prefix} Geodetic calculator (pyproj.Geod) not initialized.")
logging.error(
f"{log_prefix} Geodetic calculator (pyproj.Geod) not initialized."
)
return None
try:
# Extract necessary info (ensure keys exist and values are valid)
center_lat_rad = geo_info['lat']
center_lon_rad = geo_info['lon']
orient_rad = geo_info['orientation']
ref_x = geo_info['ref_x']
ref_y = geo_info['ref_y']
scale_x = geo_info['scale_x'] # meters/pixel
scale_y = geo_info['scale_y'] # meters/pixel
width = geo_info['width_px']
height = geo_info['height_px']
center_lat_rad = geo_info["lat"]
center_lon_rad = geo_info["lon"]
orient_rad = geo_info["orientation"]
ref_x = geo_info["ref_x"]
ref_y = geo_info["ref_y"]
scale_x = geo_info["scale_x"] # meters/pixel
scale_y = geo_info["scale_y"] # meters/pixel
width = geo_info["width_px"]
height = geo_info["height_px"]
if not (scale_x > 0 and scale_y > 0 and width > 0 and height > 0):
logging.error(f"{log_prefix} Invalid scale or dimensions in geo_info.")
@ -516,7 +643,7 @@ class MapIntegrationManager:
(0 - ref_x, ref_y - 0), # Top-Left (dx, dy relative to ref, y inverted)
(width - 1 - ref_x, ref_y - 0), # Top-Right
(width - 1 - ref_x, ref_y - (height - 1)), # Bottom-Right
(0 - ref_x, ref_y - (height - 1)) # Bottom-Left
(0 - ref_x, ref_y - (height - 1)), # Bottom-Left
]
# 2. Convert pixel offsets to meter offsets
@ -537,11 +664,14 @@ class MapIntegrationManager:
rot_dx = dx_m * cos_o - dy_m * sin_o
rot_dy = dx_m * sin_o + dy_m * cos_o
corners_meters_rotated.append((rot_dx, rot_dy))
logging.debug(f"{log_prefix} Applied rotation ({math.degrees(orient_rad):.2f} deg) to meter offsets.")
logging.debug(
f"{log_prefix} Applied rotation ({math.degrees(orient_rad):.2f} deg) to meter offsets."
)
else:
corners_meters_rotated = corners_meters # No rotation needed
logging.debug(f"{log_prefix} Skipping rotation for meter offsets (angle near zero).")
logging.debug(
f"{log_prefix} Skipping rotation for meter offsets (angle near zero)."
)
# 4. Calculate geographic coordinates of corners using pyproj.Geod.fwd
# This requires calculating distance and azimuth from the center to each rotated meter offset.
@ -560,17 +690,25 @@ class MapIntegrationManager:
center_lat_deg = math.degrees(center_lat_rad)
# Calculate the destination point
endlon, endlat, _ = self._geod.fwd(center_lon_deg, center_lat_deg, azimuth_deg, distance_m)
endlon, endlat, _ = self._geod.fwd(
center_lon_deg, center_lat_deg, azimuth_deg, distance_m
)
# Append (lon, lat) tuple in degrees
sar_corners_geo_deg.append((endlon, endlat))
logging.debug(f"{log_prefix} Calculated corner: Dist={distance_m:.1f}m, Az={azimuth_deg:.2f}deg -> Lon={endlon:.6f}, Lat={endlat:.6f}")
logging.debug(
f"{log_prefix} Calculated corner: Dist={distance_m:.1f}m, Az={azimuth_deg:.2f}deg -> Lon={endlon:.6f}, Lat={endlat:.6f}"
)
if len(sar_corners_geo_deg) != 4:
logging.error(f"{log_prefix} Failed to calculate all 4 corner coordinates.")
logging.error(
f"{log_prefix} Failed to calculate all 4 corner coordinates."
)
return None
logging.debug(f"{log_prefix} Successfully calculated 4 SAR corner geographic coordinates.")
logging.debug(
f"{log_prefix} Successfully calculated 4 SAR corner geographic coordinates."
)
return sar_corners_geo_deg
except KeyError as ke:
@ -587,7 +725,7 @@ class MapIntegrationManager:
map_bounds: mercantile.LngLatBbox,
map_tile_ranges: Tuple[Tuple[int, int], Tuple[int, int]],
zoom: int,
stitched_map_shape: Tuple[int, int] # (height, width)
stitched_map_shape: Tuple[int, int], # (height, width)
) -> Optional[List[Tuple[int, int]]]:
"""
Converts a list of geographic coordinates (lon, lat degrees) to pixel
@ -607,13 +745,21 @@ class MapIntegrationManager:
top-left of the stitched map image. Returns None on error.
"""
log_prefix = f"{self._log_prefix} Geo to Pixel"
logging.debug(f"{log_prefix} Converting {len(coords_deg)} geo coordinates to map pixels...")
logging.debug(
f"{log_prefix} Converting {len(coords_deg)} geo coordinates to map pixels..."
)
if mercantile is None:
logging.error(f"{log_prefix} Mercantile library not available.")
return None
if not stitched_map_shape or stitched_map_shape[0] <= 0 or stitched_map_shape[1] <= 0:
logging.error(f"{log_prefix} Invalid stitched map shape: {stitched_map_shape}")
if (
not stitched_map_shape
or stitched_map_shape[0] <= 0
or stitched_map_shape[1] <= 0
):
logging.error(
f"{log_prefix} Invalid stitched map shape: {stitched_map_shape}"
)
return None
pixel_coords = []
@ -644,17 +790,25 @@ class MapIntegrationManager:
# 1. Find the projected meter coordinates (Web Mercator) of the top-left corner of the stitched area.
tl_tile_mercator_bounds = mercantile.xy_bounds(min_tile_x, min_tile_y, zoom)
map_origin_x_mercator = tl_tile_mercator_bounds.left
map_origin_y_mercator = tl_tile_mercator_bounds.top # Top has higher Y in Mercator
map_origin_y_mercator = (
tl_tile_mercator_bounds.top
) # Top has higher Y in Mercator
# 2. Calculate the total span of the stitched map in Mercator meters
max_tile_x = map_tile_ranges[0][1]
max_tile_y = map_tile_ranges[1][1]
br_tile_mercator_bounds = mercantile.xy_bounds(max_tile_x, max_tile_y, zoom)
map_total_width_mercator = br_tile_mercator_bounds.right - map_origin_x_mercator
map_total_height_mercator = map_origin_y_mercator - br_tile_mercator_bounds.bottom # Top Y > Bottom Y
map_total_width_mercator = (
br_tile_mercator_bounds.right - map_origin_x_mercator
)
map_total_height_mercator = (
map_origin_y_mercator - br_tile_mercator_bounds.bottom
) # Top Y > Bottom Y
if map_total_width_mercator <= 0 or map_total_height_mercator <=0:
logging.error(f"{log_prefix} Invalid map span in Mercator coordinates calculated.")
if map_total_width_mercator <= 0 or map_total_height_mercator <= 0:
logging.error(
f"{log_prefix} Invalid map span in Mercator coordinates calculated."
)
return None
# 3. For each input geographic coordinate:
@ -664,51 +818,91 @@ class MapIntegrationManager:
# b. Calculate the coordinate relative to the map's top-left origin in Mercator meters
relative_x_mercator = point_x_mercator - map_origin_x_mercator
relative_y_mercator = map_origin_y_mercator - point_y_mercator # Invert Y difference
relative_y_mercator = (
map_origin_y_mercator - point_y_mercator
) # Invert Y difference
# c. Scale the relative Mercator coordinates to pixel coordinates based on the total map span and pixel dimensions
pixel_x = int(round((relative_x_mercator / map_total_width_mercator) * map_width_px))
pixel_y = int(round((relative_y_mercator / map_total_height_mercator) * map_height_px))
pixel_x = int(
round(
(relative_x_mercator / map_total_width_mercator) * map_width_px
)
)
pixel_y = int(
round(
(relative_y_mercator / map_total_height_mercator)
* map_height_px
)
)
# Clamp pixel coordinates to be within the stitched map bounds
pixel_x_clamped = max(0, min(pixel_x, map_width_px - 1))
pixel_y_clamped = max(0, min(pixel_y, map_height_px - 1))
if pixel_x != pixel_x_clamped or pixel_y != pixel_y_clamped:
logging.warning(f"{log_prefix} Clamped pixel coords for ({lon:.4f},{lat:.4f}): ({pixel_x},{pixel_y}) -> ({pixel_x_clamped},{pixel_y_clamped})")
logging.warning(
f"{log_prefix} Clamped pixel coords for ({lon:.4f},{lat:.4f}): ({pixel_x},{pixel_y}) -> ({pixel_x_clamped},{pixel_y_clamped})"
)
pixel_coords.append((pixel_x_clamped, pixel_y_clamped))
logging.debug(f"{log_prefix} Converted ({lon:.4f},{lat:.4f}) -> MercatorRel({relative_x_mercator:.1f},{relative_y_mercator:.1f}) -> Pixel({pixel_x_clamped},{pixel_y_clamped})")
logging.debug(
f"{log_prefix} Converted ({lon:.4f},{lat:.4f}) -> MercatorRel({relative_x_mercator:.1f},{relative_y_mercator:.1f}) -> Pixel({pixel_x_clamped},{pixel_y_clamped})"
)
logging.debug(f"{log_prefix} Successfully converted {len(pixel_coords)} coordinates to map pixels.")
logging.debug(
f"{log_prefix} Successfully converted {len(pixel_coords)} coordinates to map pixels."
)
return pixel_coords
except Exception as e:
logging.exception(f"{log_prefix} Error converting geo coordinates to map pixels:")
logging.exception(
f"{log_prefix} Error converting geo coordinates to map pixels:"
)
return None
def _draw_scale_bar(self, image_pil: Image.Image, latitude_deg: float, zoom: int) -> Image.Image:
def _draw_scale_bar(
self, image_pil: Image.Image, latitude_deg: float, zoom: int
) -> Image.Image:
"""Draws a simple scale bar onto the map image."""
log_prefix = f"{self._log_prefix} ScaleBar"
if image_pil is None: return None # Non fare nulla se l'immagine non c'è
if image_pil is None:
return None # Non fare nulla se l'immagine non c'è
try:
# 1. Calculate meters/pixel
meters_per_pixel = calculate_meters_per_pixel(latitude_deg, zoom)
if meters_per_pixel is None or meters_per_pixel <= 0:
logging.warning(f"{log_prefix} Invalid meters_per_pixel ({meters_per_pixel}). Cannot draw scale bar.")
logging.warning(
f"{log_prefix} Invalid meters_per_pixel ({meters_per_pixel}). Cannot draw scale bar."
)
return image_pil # Restituisci l'immagine originale
# 2. Choose scale distance based on meters/pixel or image width
# Obiettivo: barra lunga ~100-150 pixel
img_w, img_h = image_pil.size
target_bar_px = max(50, min(150, img_w // 4)) # Lunghezza desiderata in pixel (adattiva)
target_bar_px = max(
50, min(150, img_w // 4)
) # Lunghezza desiderata in pixel (adattiva)
# Trova una distanza "tonda" (1, 2, 5, 10, 20, 50, 100... km)
# che corrisponda a circa target_bar_px
possible_distances_km = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]
possible_distances_km = [
0.1,
0.2,
0.5,
1,
2,
5,
10,
20,
50,
100,
200,
500,
1000,
]
best_dist_km = 1
min_diff = float('inf')
min_diff = float("inf")
for dist_km in possible_distances_km:
dist_m = dist_km * 1000.0
@ -723,7 +917,9 @@ class MapIntegrationManager:
scale_bar_pixels = int(round(scale_distance_meters / meters_per_pixel))
if scale_bar_pixels < 10: # Troppo piccola per essere utile
logging.warning(f"{log_prefix} Calculated scale bar length too small ({scale_bar_pixels}px). Skipping draw.")
logging.warning(
f"{log_prefix} Calculated scale bar length too small ({scale_bar_pixels}px). Skipping draw."
)
return image_pil
# 3. Prepare for drawing (Convert to OpenCV BGR)
@ -741,22 +937,56 @@ class MapIntegrationManager:
color = (0, 0, 0) # Nero
# 5. Draw the scale bar line
cv2.line(map_cv, (bar_x_start, bar_y), (bar_x_start + scale_bar_pixels, bar_y), color, bar_thickness)
cv2.line(
map_cv,
(bar_x_start, bar_y),
(bar_x_start + scale_bar_pixels, bar_y),
color,
bar_thickness,
)
# Draw small ticks at the ends
cv2.line(map_cv, (bar_x_start, bar_y - 3), (bar_x_start, bar_y + 3), color, bar_thickness)
cv2.line(map_cv, (bar_x_start + scale_bar_pixels, bar_y - 3), (bar_x_start + scale_bar_pixels, bar_y + 3), color, bar_thickness)
cv2.line(
map_cv,
(bar_x_start, bar_y - 3),
(bar_x_start, bar_y + 3),
color,
bar_thickness,
)
cv2.line(
map_cv,
(bar_x_start + scale_bar_pixels, bar_y - 3),
(bar_x_start + scale_bar_pixels, bar_y + 3),
color,
bar_thickness,
)
# 6. Draw the text label
label = f"{scale_distance_km} km" if scale_distance_km >= 1 else f"{int(scale_distance_meters)} m"
label = (
f"{scale_distance_km} km"
if scale_distance_km >= 1
else f"{int(scale_distance_meters)} m"
)
text_size, _ = cv2.getTextSize(label, font, font_scale, font_thickness)
text_x = bar_x_start + (scale_bar_pixels // 2) - (text_size[0] // 2)
text_y = bar_y + text_offset_y - 5 # Posiziona sopra la barra
cv2.putText(map_cv, label, (text_x, text_y), font, font_scale, color, font_thickness, cv2.LINE_AA)
cv2.putText(
map_cv,
label,
(text_x, text_y),
font,
font_scale,
color,
font_thickness,
cv2.LINE_AA,
)
# 7. Convert back to PIL RGB
image_pil_with_scale = Image.fromarray(cv2.cvtColor(map_cv, cv2.COLOR_BGR2RGB))
logging.debug(f"{log_prefix} Scale bar drawn ({label}, {scale_bar_pixels}px).")
image_pil_with_scale = Image.fromarray(
cv2.cvtColor(map_cv, cv2.COLOR_BGR2RGB)
)
logging.debug(
f"{log_prefix} Scale bar drawn ({label}, {scale_bar_pixels}px)."
)
return image_pil_with_scale
except Exception as e:

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
@ -224,10 +225,13 @@ def calculate_meters_per_pixel(latitude_deg: float, zoom: int) -> Optional[float
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

@ -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
@ -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)
@ -136,14 +140,20 @@ class TestModeManager:
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).")
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)
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.")
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
@ -151,7 +161,7 @@ class TestModeManager:
low=min_val,
high=max_val + 1, # randint is exclusive of high
size=sar_shape,
dtype=np.uint16 # Explicit fallback type
dtype=np.uint16, # Explicit fallback type
)
# Generate integers if type allows
@ -160,22 +170,31 @@ class TestModeManager:
low=min_val,
high=max_val + 1, # numpy randint is exclusive of high
size=sar_shape,
dtype=config.SAR_DATA_TYPE
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,16 +204,24 @@ 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.")
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
@ -202,7 +229,9 @@ class TestModeManager:
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.")
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):
@ -219,10 +248,16 @@ 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.")
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
@ -230,7 +265,9 @@ class TestModeManager:
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.")
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):
@ -242,10 +279,14 @@ 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
@ -255,10 +296,14 @@ 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
@ -281,17 +326,23 @@ class TestModeManager:
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.")
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,12 +393,13 @@ class TestModeManager:
except IndexError as idx_err:
# Handle potential index errors if LUT/indices dimensions mismatch
min_idx, max_idx = "-","-"
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
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}. "
@ -369,19 +433,27 @@ 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.")
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.")
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...")
@ -389,13 +461,21 @@ class TestModeManager:
# --- 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})...")
@ -412,30 +492,43 @@ class TestModeManager:
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).")
# 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 ---

165
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,9 +403,7 @@ 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."""

105
utils.py
View File

@ -22,19 +22,25 @@ 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']
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
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,7 +281,9 @@ 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)
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:
@ -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:
@ -317,14 +331,20 @@ def generate_sar_kml(geo_info_radians, output_path) -> bool:
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.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.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)])
@ -346,7 +370,9 @@ def generate_sar_kml(geo_info_radians, output_path) -> bool:
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.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
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)
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?)")
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}")
except Exception as e:
logging.exception(f"{log_prefix} Unexpected error launching Google Earth:")
# --- END OF FILE utils.py ---