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
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
133
map_display.py
133
map_display.py
@ -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 ---
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -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
165
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,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
105
utils.py
@ -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 ---
|
||||
|
||||
Loading…
Reference in New Issue
Block a user