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:
parent
2dd7c4715c
commit
0c93133448
16
config.py
16
config.py
@ -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
|
||||
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -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}"
|
||||
|
||||
163
map_display.py
163
map_display.py
@ -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
16
map_utils.py
16
map_utils.py
@ -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 ---
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
167
ui.py
@ -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
137
utils.py
@ -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 ---
|
||||
|
||||
Loading…
Reference in New Issue
Block a user