SXXXXXXX_ControlPanel/ControlPanel.py
VALLONGOL f0c49a7934 fix overlay function
add shift sar map
2025-04-14 15:55:12 +02:00

1558 lines
73 KiB
Python

# --- START OF FILE ControlPanel.py ---
# ControlPanel.py (Previously app.py)
"""
THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED.
Main application module for the Control Panel application.
Orchestrates UI, display, network reception, image processing pipeline,
test mode management, map integration, image recording, and state management.
Initializes all sub-modules and manages the main application lifecycle.
"""
# --- Standard library imports ---
import threading
import time
import queue
import os
import logging
import math
import sys
import socket
from typing import Optional, Tuple, Any, Dict, TYPE_CHECKING, Union
import datetime
import tkinter.filedialog as fd # For save file dialog
from pathlib import Path # For map saving
# --- Third-party imports ---
import tkinter as tk
from tkinter import ttk
from tkinter import colorchooser
import numpy as np
import cv2
import screeninfo
# --- PIL Import and Type Definition ---
try:
from PIL import Image, ImageTk
ImageType = Image.Image # type: ignore
except ImportError:
ImageType = Any # Fallback type hint
Image = None
ImageTk = None
logging.critical(
"[App Init] Pillow library not found. Map/Image functionality will fail."
)
# --- Configuration Import ---
import config
# --- Logging Setup ---
try:
from logging_config import setup_logging
setup_logging()
except ImportError:
print("ERROR: logging_config.py not found. Using basic logging.")
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s",
)
# --- Application Modules Import ---
from ui import ControlPanel as UIPanel, StatusBar, create_main_window
from display import DisplayManager
from utils import (
put_queue,
clear_queue,
decimal_to_dms,
dms_string_to_decimal, # Added dms_string_to_decimal
generate_sar_kml,
launch_google_earth,
cleanup_old_kml_files,
open_google_maps,
)
from network import create_udp_socket, close_udp_socket
from receiver import UdpReceiver
from app_state import AppState
from test_mode_manager import TestModeManager
from image_pipeline import ImagePipeline
from image_processing import load_image, normalize_image, apply_color_palette
from image_recorder import ImageRecorder
# --- Map related imports (Conditional) ---
map_libs_found = True
try:
import mercantile
import pyproj
if Image is None and ImageType is not Any:
raise ImportError("Pillow failed import")
except ImportError as map_lib_err:
logging.warning(
f"[App Init] Core map lib import failed ({map_lib_err}). Map disabled."
)
map_libs_found = False
BaseMapService = None
MapTileManager = None
MapDisplayWindow = None
MapIntegrationManager = None
MapCalculationError = Exception
if map_libs_found:
try:
from map_services import get_map_service, BaseMapService
from map_manager import MapTileManager
from map_utils import MapCalculationError # Only import needed exception?
from map_display import MapDisplayWindow
from map_integration import MapIntegrationManager
MAP_MODULES_LOADED = True
except ImportError as map_import_err:
logging.warning(
f"[App Init] Specific map module import failed ({map_import_err}). Map disabled."
)
MAP_MODULES_LOADED = False
BaseMapService = None
MapTileManager = None
MapDisplayWindow = None
MapIntegrationManager = None
MapCalculationError = Exception
else:
MAP_MODULES_LOADED = False
# --- Main Application Class ---
class ControlPanelApp:
"""
Main application class. Manages UI, display, processing, network, state,
and orchestrates various managers.
"""
# --- Status Update Method ---
def set_status(self, message: str):
"""Safely updates the main status message prefix in the status bar via after_idle."""
log_prefix = "[App Set Status]"
if not hasattr(self, "state") or self.state.shutting_down:
return
new_status_prefix = f"Status: {message}"
# logging.info(...) # Reduce verbosity
def _update():
if not hasattr(self, "state") or self.state.shutting_down:
return
try:
statusbar = getattr(self, "statusbar", None)
if (
statusbar
and isinstance(statusbar, tk.Widget)
and statusbar.winfo_exists()
):
current_text: str = statusbar.cget("text")
parts = current_text.split("|")
suffix = ""
if len(parts) > 1:
suffix_parts = [p.strip() for p in parts[1:] if p.strip()]
suffix = (
" | " + " | ".join(suffix_parts) if suffix_parts else ""
)
final_text = f"{new_status_prefix}{suffix}"
statusbar.set_status_text(final_text)
except Exception as e:
logging.exception(f"{log_prefix} Error updating status bar text:")
try:
if hasattr(self, "root") and self.root and self.root.winfo_exists():
self.root.after_idle(_update)
except Exception as e:
logging.warning(f"{log_prefix} Error scheduling status update: {e}")
# --- LUT Generation Methods ---
def update_brightness_contrast_lut(self):
"""Recalculates the SAR B/C LUT based on AppState and stores it back."""
log_prefix = "[App Update SAR LUT]"
if not hasattr(self, "state"):
return
try:
contrast = max(0.01, self.state.sar_contrast)
brightness = self.state.sar_brightness
lut_values = np.arange(256, dtype=np.float32)
adjusted = (lut_values * contrast) + brightness
self.state.brightness_contrast_lut = np.clip(
np.round(adjusted), 0, 255
).astype(np.uint8)
except Exception as e:
logging.exception(f"{log_prefix} Error calculating SAR B/C LUT:")
self.state.brightness_contrast_lut = np.arange(256, dtype=np.uint8)
def update_mfd_lut(self):
"""Recalculates the MFD LUT based on AppState parameters and stores it back."""
log_prefix = "[MFD LUT Update]"
if not hasattr(self, "state"):
return
try:
params = self.state.mfd_params
raw_map_factor = params["raw_map_intensity"] / 255.0
pixel_map = params["pixel_to_category"]
categories = params["categories"]
new_lut = np.zeros((256, 3), dtype=np.uint8)
for idx in range(256):
cat_name = pixel_map.get(idx)
if cat_name:
cat_data = categories[cat_name]
bgr = cat_data["color"]
intensity = cat_data["intensity"] / 255.0
new_lut[idx, 0] = np.clip(
int(round(float(bgr[0]) * intensity)), 0, 255
)
new_lut[idx, 1] = np.clip(
int(round(float(bgr[1]) * intensity)), 0, 255
)
new_lut[idx, 2] = np.clip(
int(round(float(bgr[2]) * intensity)), 0, 255
)
elif 32 <= idx <= 255:
raw_intensity = (float(idx) - 32.0) * (255.0 / 223.0)
final_gray = int(
round(np.clip(raw_intensity * raw_map_factor, 0, 255))
)
new_lut[idx, :] = final_gray
self.state.mfd_lut = new_lut
except Exception as e:
logging.critical(f"{log_prefix} CRITICAL MFD LUT error:", exc_info=True)
self._apply_fallback_mfd_lut()
def _apply_fallback_mfd_lut(self):
"""Applies a simple grayscale ramp as a fallback MFD LUT."""
if hasattr(self, "state"):
try:
self.state.mfd_lut = cv2.cvtColor(
np.arange(256, dtype=np.uint8)[:, np.newaxis], cv2.COLOR_GRAY2BGR
)[:, 0, :]
except Exception as fb_e:
logging.critical(f"[MFD LUT Update] Fallback LUT failed: {fb_e}")
self.state.mfd_lut = np.zeros((256, 3), dtype=np.uint8)
# --- Existing UI Callbacks (Mostly Unchanged Logic, adapted to new UI/State) ---
def update_image_mode(self):
log_prefix = "[App Mode Switch]"
if (
not hasattr(self, "state")
or not hasattr(self, "test_mode_manager")
or self.state.shutting_down
):
return
try:
cp = getattr(self, "control_panel", None)
var = getattr(cp, "test_image_var", None) if cp else None
is_test_req = (
var.get() == 1
if (var and isinstance(var, tk.Variable))
else self.state.test_mode_active
)
if is_test_req != self.state.test_mode_active:
self.state.test_mode_active = is_test_req
if is_test_req:
if self.test_mode_manager.activate():
self.activate_test_mode_ui_actions()
else:
self._revert_test_mode_ui()
else:
self.test_mode_manager.deactivate()
self.deactivate_test_mode_ui_actions()
self.state.reset_statistics()
self.update_status()
except Exception as e:
logging.exception(f"{log_prefix} Error:")
def update_sar_size(self, event=None):
log_prefix = "[App CB SAR Size]"
if not hasattr(self, "state") or self.state.shutting_down:
return
try:
cp = getattr(self, "control_panel", None)
combo = getattr(cp, "sar_size_combo", None) if cp else None
if not combo:
return
size_str = combo.get()
factor = 1
if size_str and ":" in size_str:
try:
factor = int(size_str.split(":")[1])
factor = max(1, factor)
except:
factor = 1
w = max(1, config.SAR_WIDTH // factor)
h = max(1, config.SAR_HEIGHT // factor)
if w != self.state.sar_display_width or h != self.state.sar_display_height:
self.state.update_sar_display_size(w, h)
self._trigger_sar_update()
except Exception as e:
logging.exception(f"{log_prefix} Error:")
def update_contrast(self, value_str: str):
if self.state.shutting_down:
return
try:
contrast = float(value_str)
self.state.update_sar_parameters(contrast=contrast)
self.update_brightness_contrast_lut()
self._trigger_sar_update()
except Exception as e:
logging.exception(f"[App CB SAR Contrast] Error: {e}")
def update_brightness(self, value_str: str):
if self.state.shutting_down:
return
try:
brightness = int(float(value_str))
self.state.update_sar_parameters(brightness=brightness)
self.update_brightness_contrast_lut()
self._trigger_sar_update()
except Exception as e:
logging.exception(f"[App CB SAR Brightness] Error: {e}")
def update_sar_palette(self, event=None):
if self.state.shutting_down:
return
try:
cp = getattr(self, "control_panel", None)
combo = getattr(cp, "palette_combo", None) if cp else None
if not combo:
return
palette = combo.get()
if palette in config.COLOR_PALETTES:
self.state.update_sar_parameters(palette=palette)
self._trigger_sar_update()
else:
combo.set(self.state.sar_palette)
except Exception as e:
logging.exception(f"[App CB SAR Palette] Error: {e}")
def update_mfd_category_intensity(self, category_name: str, intensity_value: int):
if self.state.shutting_down:
return
try:
if category_name in self.state.mfd_params["categories"]:
self.state.mfd_params["categories"][category_name]["intensity"] = (
np.clip(intensity_value, 0, 255)
)
self.update_mfd_lut()
self._trigger_mfd_update()
except Exception as e:
logging.exception(
f"[App CB MFD Intensity] Error for '{category_name}': {e}"
)
def choose_mfd_category_color(self, category_name: str):
if self.state.shutting_down:
return
if category_name not in self.state.mfd_params["categories"]:
return
try:
initial_bgr = self.state.mfd_params["categories"][category_name]["color"]
initial_hex = (
f"#{initial_bgr[2]:02x}{initial_bgr[1]:02x}{initial_bgr[0]:02x}"
)
color_code = colorchooser.askcolor(
title=f"Select Color for {category_name}", initialcolor=initial_hex
)
if color_code and color_code[0]:
rgb = color_code[0]
new_bgr = tuple(
np.clip(int(c), 0, 255) for c in (rgb[2], rgb[1], rgb[0])
)
self.state.mfd_params["categories"][category_name]["color"] = new_bgr
self.update_mfd_lut()
cp = getattr(self, "control_panel", None)
if (
self.root
and self.root.winfo_exists()
and cp
and hasattr(cp, "update_mfd_color_display")
):
self.root.after_idle(
cp.update_mfd_color_display, category_name, new_bgr
)
self._trigger_mfd_update()
except Exception as e:
logging.exception(f"[App CB MFD Color] Error for '{category_name}': {e}")
def update_mfd_raw_map_intensity(self, intensity_value: int):
if self.state.shutting_down:
return
try:
self.state.mfd_params["raw_map_intensity"] = np.clip(
intensity_value, 0, 255
)
self.update_mfd_lut()
self._trigger_mfd_update()
except Exception as e:
logging.exception(f"[App CB MFD RawMap] Error: {e}")
def update_map_size(self, event=None):
if not hasattr(self, "state") or self.state.shutting_down:
return
try:
cp = getattr(self, "control_panel", None)
combo = getattr(cp, "map_size_combo", None) if cp else None
if not combo:
return
self.state.update_map_scale_factor(combo.get())
self.trigger_map_redraw() # Redraw immediately with new scale factor
except Exception as e:
logging.exception(f"[App CB Map Size] Error: {e}")
def toggle_sar_overlay(self):
if not hasattr(self, "state") or self.state.shutting_down:
return
try:
cp = getattr(self, "control_panel", None)
var = getattr(cp, "sar_overlay_var", None) if cp else None
if not var or not isinstance(var, tk.Variable):
return
self.state.update_map_overlay_params(enabled=var.get())
self.trigger_map_redraw() # Redraw immediately
except Exception as e:
logging.exception(f"[App CB Overlay Toggle] Error: {e}")
def update_sar_overlay_alpha(
self, value_str: str
): # Only for immediate feedback if needed
if self.state.shutting_down:
return
try:
float(value_str) # Just validate
except ValueError:
logging.warning(f"[App CB SAR Alpha] Invalid alpha value: {value_str}")
def on_alpha_slider_release(self, event=None):
"""Handles ButtonRelease on alpha slider: updates state and triggers map recomposition."""
log_prefix = "[App CB SAR Alpha Release]"
if not hasattr(self, "state") or self.state.shutting_down:
return
try:
cp = getattr(self, "control_panel", None)
var = getattr(cp, "sar_overlay_alpha_var", None) if cp else None
if not var or not isinstance(var, tk.Variable):
return
final_alpha = var.get()
logging.info(f"{log_prefix} Alpha value set to: {final_alpha:.3f}")
self.state.update_map_overlay_params(alpha=final_alpha)
# Trigger a recomposition, not a full update
self.trigger_map_redraw(full_update=False)
except Exception as e:
logging.exception(f"{log_prefix} Error: {e}")
def toggle_sar_recording(self):
if not hasattr(self, "state") or self.state.shutting_down:
return
try:
cp = getattr(self, "control_panel", None)
var = getattr(cp, "record_sar_var", None) if cp else None
if not var or not isinstance(var, tk.Variable):
return
self.state.update_sar_recording_enabled(enabled=var.get())
except Exception as e:
logging.exception(f"[App CB Record Toggle] Error: {e}")
# --- NEW UI Callbacks ---
def apply_sar_overlay_shift(self):
"""Reads shift values from UI, validates, updates AppState, triggers full map update."""
log_prefix = "[App CB Apply Shift]"
if not hasattr(self, "state") or self.state.shutting_down:
return
cp = getattr(self, "control_panel", None)
if not cp:
return
lat_str = cp.sar_lat_shift_var.get()
lon_str = cp.sar_lon_shift_var.get()
try:
lat_shift = float(lat_str)
lon_shift = float(lon_str)
# Add more validation if needed (e.g., range)
self.state.update_sar_overlay_shift(lat_shift, lon_shift)
self.trigger_map_redraw(full_update=True) # Full update needed for shift
self.set_status("Applied SAR overlay shift.")
except ValueError as ve:
logging.error(f"{log_prefix} Invalid shift value: {ve}")
self.set_status(f"Error: Invalid shift - {ve}")
except Exception as e:
logging.exception(f"{log_prefix} Error: {e}")
self.set_status("Error applying shift.")
def save_current_map_view(self):
"""Opens save dialog and calls MapIntegrationManager to save the current map view."""
log_prefix = "[App CB Save Map]"
if not hasattr(self, "state") or self.state.shutting_down:
return
mgr = getattr(self, "map_integration_manager", None)
if not mgr:
self.set_status("Error: Map components not ready.")
return
if self.state.last_composed_map_pil is None:
self.set_status("Error: No map view to save.")
return
try:
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
init_fn = f"map_view_{ts}.png"
file_path = fd.asksaveasfilename(
title="Save Map View As",
initialfile=init_fn,
defaultextension=".png",
filetypes=[("PNG files", "*.png"), ("All files", "*.*")],
)
if file_path:
save_dir = os.path.dirname(file_path)
save_fn = Path(file_path).stem
mgr.save_map_view_to_file(
directory=save_dir, filename=save_fn
) # Status updated by manager
else:
self.set_status("Save map view cancelled.")
except Exception as e:
logging.exception(f"{log_prefix} Error: {e}")
self.set_status("Error saving map view.")
def go_to_google_maps(self, coord_source: str):
"""Opens Google Maps centered on coordinates from the specified UI source."""
log_prefix = "[App CB Go Gmaps]"
if not hasattr(self, "state") or self.state.shutting_down:
return
cp = getattr(self, "control_panel", None)
if not cp:
return
coords_text: Optional[str] = None
source_desc = coord_source # Use ID as description default
lat_deg: Optional[float] = None
lon_deg: Optional[float] = None
try:
if coord_source == "sar_center":
coords_text = cp.sar_center_coords_var.get()
source_desc = "SAR Center"
elif coord_source == "sar_mouse":
coords_text = cp.mouse_coords_var.get()
source_desc = "SAR Mouse"
elif coord_source == "map_mouse":
coords_text = cp.map_mouse_coords_var.get()
source_desc = "Map Mouse"
else:
return # Unknown source
if (
not coords_text
or "N/A" in coords_text
or "Error" in coords_text
or "Invalid" in coords_text
):
self.set_status(f"Error: No valid coordinates for {source_desc}.")
return
# Parse the combined string "Lat=..., Lon=..." using the DMS parser
lon_sep = ", Lon="
if lon_sep in coords_text:
parts = coords_text.split(lon_sep, 1)
lat_dms_str = (
parts[0].split("=", 1)[1].strip() if "Lat=" in parts[0] else None
)
lon_dms_str = parts[1].strip()
if lat_dms_str and lon_dms_str:
lat_deg = dms_string_to_decimal(
lat_dms_str, is_latitude=True
) # Use new util
lon_deg = dms_string_to_decimal(
lon_dms_str, is_latitude=False
) # Use new util
else:
raise ValueError("Could not split Lat/Lon parts")
else:
raise ValueError("Separator ', Lon=' not found")
if lat_deg is None or lon_deg is None:
self.set_status(f"Error: Cannot parse coordinates for {source_desc}.")
return
open_google_maps(lat_deg, lon_deg) # Call util function
except Exception as e:
logging.exception(f"{log_prefix} Error for {source_desc}: {e}")
self.set_status(f"Error opening map for {source_desc}.")
# --- Initialization Helper Methods ---
def _get_screen_dimensions(self) -> Tuple[int, int]:
"""Gets primary screen dimensions using screeninfo."""
log_prefix = "[App Init]"
try:
monitors = screeninfo.get_monitors()
if not monitors: raise screeninfo.ScreenInfoError("No monitors detected.")
screen = monitors[0] # Assume primary is the first
logging.debug(f"{log_prefix} Detected Screen: {screen.width}x{screen.height}")
return screen.width, screen.height
except Exception as e:
logging.warning(f"{log_prefix} Screen info error: {e}. Using default 1920x1080.")
return 1920, 1080
def _calculate_initial_sar_size(self, desired_factor_if_map: int = 4) -> Tuple[int, int]:
"""Calculates initial SAR display size based on config and map state."""
log_prefix = "[App Init]"
initial_w = self.state.sar_display_width; initial_h = self.state.sar_display_height
map_enabled_and_loaded = config.ENABLE_MAP_OVERLAY and MAP_MODULES_LOADED
if map_enabled_and_loaded:
forced_factor = max(1, desired_factor_if_map)
initial_w = config.SAR_WIDTH // forced_factor
initial_h = config.SAR_HEIGHT // forced_factor
# Update state immediately if map forces a different initial size
if initial_w != self.state.sar_display_width or initial_h != self.state.sar_display_height:
self.state.update_sar_display_size(initial_w, initial_h)
logging.info(f"{log_prefix} Map active, using SAR size 1:{forced_factor} ({initial_w}x{initial_h}).")
else:
logging.debug(f"{log_prefix} Using initial SAR size from state: {initial_w}x{initial_h}.")
return initial_w, initial_h
def _calculate_tkinter_position(self, screen_h: int) -> Tuple[int, int]:
"""Calculates the initial X, Y position for the Tkinter control panel window."""
x = 10; y = config.INITIAL_MFD_HEIGHT + 40 # Position below MFD placeholder
# Adjust if it goes off screen
if y + config.TKINTER_MIN_HEIGHT > screen_h:
y = max(10, screen_h - config.TKINTER_MIN_HEIGHT - 10)
return x, y
def _calculate_mfd_position(self) -> Tuple[int, int]:
"""Calculates the initial X, Y position for the MFD display window."""
# Align with Tkinter window's left edge, near the top
x = self.tkinter_x # Assumes tkinter_x is calculated first
y = 10
return x, y
def _calculate_sar_position(self, screen_w: int, initial_sar_w: int) -> Tuple[int, int]:
"""Calculates the initial X, Y position for the SAR display window."""
# Position to the right of Tkinter window + padding
x = self.tkinter_x + config.TKINTER_MIN_WIDTH + 20 # Assumes tkinter_x calculated
y = 10 # Align with top
# Adjust if it goes off screen
if x + initial_sar_w > screen_w: x = max(10, screen_w - initial_sar_w - 10)
return x, y
def _calculate_map_position(self, screen_w: int, current_sar_w: int, max_map_width: int) -> Tuple[int, int]:
"""Calculates the initial X, Y position for the Map display window."""
# Position to the right of SAR window + padding
x = self.sar_x + current_sar_w + 20 # Assumes sar_x calculated
y = 10 # Align with top
# Adjust if it goes off screen
if x + max_map_width > screen_w: x = max(10, screen_w - max_map_width - 10)
return x, y
def _setup_network_receiver(self):
"""Creates and starts the UDP socket and receiver thread."""
log_prefix = "[App Init Network]"
logging.info(f"{log_prefix} Attempting network receiver on {self.local_ip}:{self.local_port}")
self.udp_socket = create_udp_socket(self.local_ip, self.local_port)
if self.udp_socket:
try:
recorder_instance = getattr(self, "image_recorder", None)
self.udp_receiver = UdpReceiver(
app=self, udp_socket=self.udp_socket,
set_new_sar_image_callback=self.handle_new_sar_data,
set_new_mfd_indices_image_callback=self.handle_new_mfd_data,
image_recorder=recorder_instance,
)
self.udp_thread = threading.Thread(target=self.udp_receiver.receive_udp_data, name="UDPReceiverThread", daemon=True)
self.udp_thread.start()
self.set_status(f"Listening UDP {self.local_ip}:{self.local_port}")
except Exception as receiver_init_e:
logging.critical(f"{log_prefix} Failed to initialize UdpReceiver: {receiver_init_e}", exc_info=True)
self.set_status("Error: Receiver Init Failed"); close_udp_socket(self.udp_socket); self.udp_socket = None
else: self.set_status("Error: UDP Socket Failed")
def _start_initial_image_loader(self):
"""Starts a background thread to load local/test images if needed."""
log_prefix = "[App Init]"
should_load = config.USE_LOCAL_IMAGES or config.ENABLE_TEST_MODE
if should_load:
logging.debug(f"{log_prefix} Starting initial image loading thread...")
image_loading_thread = threading.Thread(target=self.load_initial_images, name="ImageLoaderThread", daemon=True)
image_loading_thread.start()
else: # If not loading, schedule initial display setup immediately
if self.root and self.root.winfo_exists(): self.root.after_idle(self._set_initial_display_from_loaded_data)
def _update_initial_ui_display(self):
"""Sets the initial text for UI info Entry widgets based on default AppState."""
# This version is already updated in the previous response part
log_prefix = "[App Init]"
logging.debug(f"{log_prefix} Setting initial UI info display...")
control_panel_ref = getattr(self, "control_panel", None)
if not control_panel_ref: return
try:
control_panel_ref.set_sar_center_coords("N/A", "N/A")
control_panel_ref.set_sar_orientation("N/A")
control_panel_ref.set_sar_size_km("N/A")
control_panel_ref.set_mouse_coordinates("N/A", "N/A")
control_panel_ref.set_map_mouse_coordinates("N/A", "N/A")
initial_stats = self.state.get_statistics()
drop_txt = f"Drop(Q): S={initial_stats['dropped_sar_q']},M={initial_stats['dropped_mfd_q']},Tk={initial_stats['dropped_tk_q']},Mo={initial_stats['dropped_mouse_q']}"
incmpl_txt = f"Incmpl(RX): S={initial_stats['incomplete_sar_rx']},M={initial_stats['incomplete_mfd_rx']}"
control_panel_ref.set_statistics_display(drop_txt, incmpl_txt)
logging.debug(f"{log_prefix} Initial UI info display set.")
except tk.TclError as e: logging.warning(f"{log_prefix} Error setting initial UI display (TclError): {e}")
except Exception as e: logging.exception(f"{log_prefix} Unexpected error setting initial UI display:")
def load_initial_images(self):
"""(Runs in background thread) Loads initial local/test images into AppState."""
log_prefix = "[App Image Loader]"
if self.state.shutting_down: return
if self.root and self.root.winfo_exists(): self.root.after_idle(self.set_status, "Loading initial images...")
try:
if config.ENABLE_TEST_MODE or self.state.test_mode_active:
if hasattr(self, "test_mode_manager") and self.test_mode_manager: self.test_mode_manager._ensure_test_images()
if config.USE_LOCAL_IMAGES:
self._load_local_mfd_image(); self._load_local_sar_image()
if self.root and self.root.winfo_exists(): self.root.after_idle(self._set_initial_display_from_loaded_data)
except Exception as e:
logging.exception(f"{log_prefix} Error during initial image loading:")
if self.root and self.root.winfo_exists(): self.root.after_idle(self.set_status, "Error Loading Images")
finally: logging.info(f"{log_prefix} Initial image loading thread finished.")
def _load_local_mfd_image(self):
"""Loads local MFD image data (indices) into AppState."""
log_prefix = "[App Image Loader]"; default_indices = np.random.randint(0, 256, (config.MFD_HEIGHT, config.MFD_WIDTH), dtype=np.uint8)
try: self.state.local_mfd_image_data_indices = default_indices # Placeholder: Actual loading NYI
except Exception as e: logging.exception(f"{log_prefix} Error loading local MFD image:"); self.state.local_mfd_image_data_indices = default_indices
def _load_local_sar_image(self):
"""Loads local SAR image data (raw) into AppState."""
log_prefix = "[App Image Loader]"; default_raw_data = np.zeros((config.SAR_HEIGHT, config.SAR_WIDTH), dtype=config.SAR_DATA_TYPE)
try: loaded_raw_data = load_image(config.SAR_IMAGE_PATH, config.SAR_DATA_TYPE); self.state.local_sar_image_data_raw = loaded_raw_data if loaded_raw_data is not None and loaded_raw_data.size > 0 else default_raw_data
except Exception as e: logging.exception(f"{log_prefix} Error loading local SAR raw data:"); self.state.local_sar_image_data_raw = default_raw_data
def _set_initial_display_from_loaded_data(self):
"""(Runs in main thread) Sets initial display based on loaded data/mode."""
log_prefix = "[App Init Display]"
if self.state.shutting_down: return
is_test = self.state.test_mode_active; is_local = config.USE_LOCAL_IMAGES
if not is_test and is_local:
if self.state.local_mfd_image_data_indices is not None:
if hasattr(self, "image_pipeline") and self.image_pipeline: self.state.current_mfd_indices = self.state.local_mfd_image_data_indices.copy(); self.image_pipeline.process_mfd_for_display()
if self.state.local_sar_image_data_raw is not None: self.set_initial_sar_image(self.state.local_sar_image_data_raw)
else: self.set_initial_sar_image(None) # Show black
elif is_test: pass # Handled by TestModeManager
else: self._show_network_placeholders()
# Set final status (check if map still loading)
map_loading = False; mgr = getattr(self, "map_integration_manager", None)
if mgr: thread_attr = getattr(mgr, "_map_initial_display_thread", None)
if mgr and thread_attr and isinstance(thread_attr, threading.Thread): map_loading = thread_attr.is_alive()
if not map_loading:
status = "";
if is_test: status="Ready (Test Mode)"
elif is_local: status="Ready (Local Mode)"
else: status=f"Listening UDP {self.local_ip}:{self.local_port}" if self.udp_socket else "Error: No Socket"
self.set_status(status)
def set_initial_sar_image(self, raw_image_data: Optional[np.ndarray]):
"""Processes raw SAR data (or None), updates state, resets UI, triggers display."""
log_prefix = "[App Init SAR Image]"; normalized = None
if self.state.shutting_down: return
if raw_image_data is not None and raw_image_data.size > 0:
try: normalized = normalize_image(raw_image_data, target_type=np.uint8)
except Exception as e: logging.exception(f"{log_prefix} Error normalizing initial SAR:")
if normalized is None: # Use black image if normalization failed or no data
if self.state.current_sar_normalized is None: self.state.current_sar_normalized = np.zeros((config.SAR_HEIGHT,config.SAR_WIDTH), dtype=np.uint8)
self.state.current_sar_normalized.fill(0)
else: self.state.current_sar_normalized = normalized
self.state.current_sar_geo_info["valid"] = False; self._reset_ui_geo_info()
if hasattr(self,"image_pipeline") and self.image_pipeline: self.image_pipeline.process_sar_for_display()
# --- Mode Switching UI Actions ---
def activate_test_mode_ui_actions(self):
log_prefix = "[App Test Activate]"; self.set_status("Activating Test Mode..."); self._reset_ui_geo_info()
clear_queue(self.mfd_queue); clear_queue(self.sar_queue); self.set_status("Ready (Test Mode)")
def deactivate_test_mode_ui_actions(self):
log_prefix = "[App Test Deactivate]"; self.set_status("Activating Normal Mode..."); clear_queue(self.mfd_queue); clear_queue(self.sar_queue); self._reset_ui_geo_info()
if config.USE_LOCAL_IMAGES: # Local Mode Restore
if self.state.local_mfd_image_data_indices is not None: self.state.current_mfd_indices = self.state.local_mfd_image_data_indices.copy(); self.image_pipeline.process_mfd_for_display()
if self.state.local_sar_image_data_raw is not None: self.set_initial_sar_image(self.state.local_sar_image_data_raw)
else: self.set_initial_sar_image(None)
self.set_status("Ready (Local Mode)")
else: # Network Mode Restore
self._show_network_placeholders()
status = f"Listening UDP {self.local_ip}:{self.local_port}" if self.udp_socket else "Error: No UDP Socket"
self.set_status(status)
def _reset_ui_geo_info(self):
"""Schedules UI reset for geo-related Entry widgets on the main thread."""
log_prefix = "[App UI Reset]"; cp = getattr(self, "control_panel", None)
if self.root and self.root.winfo_exists() and cp:
self.root.after_idle(lambda: cp.set_sar_orientation("N/A"))
self.root.after_idle(lambda: cp.set_mouse_coordinates("N/A", "N/A"))
self.root.after_idle(lambda: cp.set_map_mouse_coordinates("N/A", "N/A"))
self.root.after_idle(lambda: cp.set_sar_center_coords("N/A", "N/A"))
self.root.after_idle(lambda: cp.set_sar_size_km("N/A"))
def _revert_test_mode_ui(self):
"""Tries to uncheck the test mode checkbox in the UI and resets the state flag."""
log_prefix = "[App Mode Switch]"; logging.warning(f"{log_prefix} Reverting Test Mode UI/state...")
cp = getattr(self, "control_panel", None); var = getattr(cp, "test_image_var", None) if cp else None
if self.root and self.root.winfo_exists() and var:
try: self.root.after_idle(var.set, 0)
except Exception as e: logging.warning(f"{log_prefix} Failed to schedule uncheck: {e}")
if hasattr(self, "state"): self.state.test_mode_active = False
def _show_network_placeholders(self):
"""Queues placeholder images for MFD and SAR displays."""
log_prefix = "[App Placeholders]"
try:
ph_mfd = np.full((config.INITIAL_MFD_HEIGHT, config.INITIAL_MFD_WIDTH, 3), 30, dtype=np.uint8)
ph_sar = np.full((self.state.sar_display_height, self.state.sar_display_width, 3), 60, dtype=np.uint8)
put_queue(self.mfd_queue, ph_mfd, "mfd", self); put_queue(self.sar_queue, ph_sar, "sar", self)
except Exception as e: logging.exception(f"{log_prefix} Error creating/queueing placeholders:")
# --- Trigger map redraw ---
def trigger_map_redraw(self, full_update: bool = False):
"""
Requests a map redraw. Uses cached data for simple redraws (alpha/toggle)
or triggers a full update if needed or requested.
"""
log_prefix = "[App Trigger Map Redraw]"
if self.state.shutting_down: return
mgr = getattr(self, "map_integration_manager", None)
if not (config.ENABLE_MAP_OVERLAY and mgr):
logging.debug(f"{log_prefix} Map disabled or manager not available.")
return
if full_update:
# Requested full update (e.g., after shift change)
logging.debug(f"{log_prefix} Triggering full map update (explicit request)...")
self._trigger_map_update_from_sar()
else:
# Simple redraw requested (e.g., alpha/toggle change)
# Check if we have the necessary cached data for recomposition
can_recompose = (
self.state.last_processed_sar_for_overlay is not None and
self.state.last_sar_warp_matrix is not None and
self.state.last_map_image_pil is not None # Need base map too
# Corner pixels are needed only if drawing the box as fallback in recompose
)
if can_recompose:
# Data exists, queue the simple redraw command
logging.debug(f"{log_prefix} Queueing simple REDRAW_MAP command (cache valid)...")
put_queue(self.tkinter_queue, ("REDRAW_MAP", None), "tkinter", self)
else:
# Data for recomposition is missing, force a full update instead
logging.warning(f"{log_prefix} Recomposition cache invalid/missing. Triggering full map update instead of simple redraw...")
self._trigger_map_update_from_sar()
# --- Initialization ---
def __init__(self, root: tk.Tk):
"""Initializes the main application components and state."""
log_prefix = "[App Init]"
logging.debug(f"{log_prefix} Starting initialization...")
self.root = root
self.root.title("Control Panel")
try: # Set Icon
script_dir = (
os.path.dirname(__file__) if "__file__" in locals() else os.getcwd()
)
icon_path = os.path.join(script_dir, "ControlPanel.ico")
if os.path.exists(icon_path):
self.root.iconbitmap(default=icon_path)
except Exception as icon_e:
logging.warning(f"{log_prefix} Icon error: {icon_e}")
self.root.minsize(config.TKINTER_MIN_WIDTH, config.TKINTER_MIN_HEIGHT)
self.state = AppState()
self.sar_queue = queue.Queue(config.DEFAULT_SAR_QUEUE)
self.mouse_queue = queue.Queue(config.DEFAULT_MOUSE_QUEUE)
self.tkinter_queue = queue.Queue(config.DEFAULT_TKINTER_QUEUE)
self.mfd_queue = queue.Queue(config.DEFAULT_MFD_QUEUE)
# Window Placement
screen_w, screen_h = self._get_screen_dimensions()
initial_sar_w, initial_sar_h = self._calculate_initial_sar_size()
self.tkinter_x, self.tkinter_y = self._calculate_tkinter_position(screen_h)
self.mfd_x, self.mfd_y = self._calculate_mfd_position()
self.sar_x, self.sar_y = self._calculate_sar_position(screen_w, initial_sar_w)
map_max_w = (
config.MAX_MAP_DISPLAY_WIDTH
if not MapDisplayWindow
else getattr(
MapDisplayWindow, "MAX_DISPLAY_WIDTH", config.MAX_MAP_DISPLAY_WIDTH
)
)
map_x, map_y = self._calculate_map_position(screen_w, initial_sar_w, map_max_w)
self.root.geometry(f"+{self.tkinter_x}+{self.tkinter_y}")
# Initialize Sub-systems
self.statusbar = StatusBar(self.root)
self.control_panel = UIPanel(self.root, self)
self.update_brightness_contrast_lut()
self.update_mfd_lut()
self.display_manager = DisplayManager(
self,
self.sar_queue,
self.mouse_queue,
self.sar_x,
self.sar_y,
self.mfd_x,
self.mfd_y,
self.state.sar_display_width,
self.state.sar_display_height,
)
try:
self.display_manager.initialize_display_windows()
except Exception as e:
self.set_status("Error: Display Init Failed")
logging.critical(f"{log_prefix} Display init failed: {e}", exc_info=True)
self.image_pipeline = ImagePipeline(
self.state, self.sar_queue, self.mfd_queue, self
)
self.test_mode_manager = TestModeManager(
self.state, self.root, self.sar_queue, self.mfd_queue, self
)
self.map_integration_manager = None
if config.ENABLE_MAP_OVERLAY: # Map Manager Init
if MAP_MODULES_LOADED and MapIntegrationManager:
try:
self.map_integration_manager = MapIntegrationManager(
self.state, self.tkinter_queue, self, map_x, map_y
)
except Exception as map_e:
logging.exception(
f"{log_prefix} MapIntegrationManager init failed:"
)
self.map_integration_manager = None
self.set_status("Error: Map Init Failed")
else:
self.set_status("Error: Map Modules Missing")
self.image_recorder = None # Recorder Init
if ImageRecorder:
try:
self.image_recorder = ImageRecorder(self.state)
except Exception as rec_e:
logging.exception(f"{log_prefix} ImageRecorder init failed:")
self.image_recorder = None
self._update_initial_ui_display()
# Network Setup
self.local_ip = config.DEFAULT_SER_IP
self.local_port = config.DEFAULT_SER_PORT
self.udp_socket = None
self.udp_receiver = None
self.udp_thread = None
if not config.USE_LOCAL_IMAGES:
self._setup_network_receiver()
else:
self._start_initial_image_loader() # Load local images if network disabled
# Start loops/timers
self.process_sar_queue()
self.process_mfd_queue()
self.process_mouse_queue()
self.process_tkinter_queue()
self.schedule_periodic_updates()
self.update_image_mode()
logging.info(f"{log_prefix} Application initialization complete.")
# --- Initialization Helper Methods ---
# (_get_screen_dimensions, _calculate_initial_sar_size, _calculate_tkinter_position)
# (_calculate_mfd_position, _calculate_sar_position, _calculate_map_position)
# (_setup_network_receiver, _start_initial_image_loader, _update_initial_ui_display)
# (_load_local_mfd_image, _load_local_sar_image, _set_initial_display_from_loaded_data)
# (set_initial_sar_image, activate_test_mode_ui_actions, deactivate_test_mode_ui_actions)
# (_reset_ui_geo_info, _revert_test_mode_ui, _show_network_placeholders)
# -> Queste funzioni rimangono come nelle risposte precedenti.
# --- Network Data Handlers ---
def handle_new_sar_data(
self, normalized_image_uint8: np.ndarray, geo_info_radians: Dict[str, Any]
):
if self.state.shutting_down:
return
self.state.set_sar_data(normalized_image_uint8, geo_info_radians)
if self.root and self.root.winfo_exists():
self.root.after_idle(self._process_sar_update_on_main_thread)
def handle_new_mfd_data(self, image_indices: np.ndarray):
if self.state.shutting_down:
return
self.state.set_mfd_indices(image_indices)
if self.root and self.root.winfo_exists():
self.root.after_idle(self._process_mfd_update_on_main_thread)
# --- Main Thread Processing ---
def _process_sar_update_on_main_thread(self):
"""Processes SAR updates: UI labels, image pipeline, map update, KML, FPS."""
if self.state.shutting_down:
return
self._update_sar_ui_labels()
if hasattr(self, "image_pipeline") and self.image_pipeline:
self.image_pipeline.process_sar_for_display()
self._trigger_map_update_from_sar() # Potentially triggers full map update
geo_info = self.state.current_sar_geo_info
if geo_info and geo_info.get("valid") and config.ENABLE_KML_GENERATION:
self._handle_kml_generation(geo_info)
self._update_fps_stats("sar")
def _handle_kml_generation(self, geo_info):
"""Handles KML generation, cleanup, and optional launch."""
# (Implementation unchanged)
try:
kml_dir = config.KML_OUTPUT_DIRECTORY
os.makedirs(kml_dir, exist_ok=True)
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
fn = f"sar_footprint_{ts}.kml"
fp = os.path.join(kml_dir, fn)
success = generate_sar_kml(geo_info, fp)
if success:
cleanup_old_kml_files(config.KML_OUTPUT_DIRECTORY, config.MAX_KML_FILES)
if config.AUTO_LAUNCH_GOOGLE_EARTH:
launch_google_earth(fp)
except Exception as e:
logging.exception("[App KML] Error: {e}")
def _process_mfd_update_on_main_thread(self):
"""Processes MFD updates: image pipeline, FPS."""
if self.state.shutting_down:
return
if hasattr(self, "image_pipeline") and self.image_pipeline:
self.image_pipeline.process_mfd_for_display()
self._update_fps_stats("mfd")
# --- UI Display Update Helpers ---
def _update_sar_ui_labels(self):
"""Updates SAR related UI Entry widgets from AppState."""
# (Implementation updated in previous step)
cp = getattr(self, "control_panel", None)
if not cp or not cp.winfo_exists():
return
geo = self.state.current_sar_geo_info
lat_s, lon_s, orient_s, size_s = "N/A", "N/A", "N/A", "N/A"
is_valid = geo and geo.get("valid")
if is_valid:
try:
lat_d, lon_d = math.degrees(geo["lat"]), math.degrees(geo["lon"])
orient_d = math.degrees(geo.get("orientation", 0.0))
lat_s, lon_s = decimal_to_dms(lat_d, True), decimal_to_dms(lon_d, False)
orient_s = f"{orient_d:.2f}°"
scale_x, w = geo.get("scale_x", 0.0), geo.get("width_px", 0)
scale_y, h = geo.get("scale_y", 0.0), geo.get("height_px", 0)
if scale_x > 0 and w > 0 and scale_y > 0 and h > 0:
size_s = f"W: {(scale_x*w)/1000.0:.1f} km, H: {(scale_y*h)/1000.0:.1f} km"
except Exception:
is_valid = False
try:
cp.set_sar_center_coords(lat_s, lon_s)
cp.set_sar_orientation(orient_s)
cp.set_sar_size_km(size_s)
except Exception as e:
logging.exception(f"[App UI Update] Error: {e}")
if not is_valid:
cp.set_mouse_coordinates("N/A", "N/A")
cp.set_map_mouse_coordinates("N/A", "N/A")
def _update_fps_stats(self, img_type: str):
"""Updates FPS counters in AppState."""
# (Implementation unchanged)
now = time.time()
try:
if img_type == "sar":
self.state.sar_frame_count += 1
el = now - self.state.sar_update_time
if el >= config.LOG_UPDATE_INTERVAL:
self.state.sar_fps = self.state.sar_frame_count / el
self.state.sar_update_time = now
self.state.sar_frame_count = 0
elif img_type == "mfd":
self.state.mfd_frame_count += 1
el = now - self.state.mfd_start_time
if el >= config.LOG_UPDATE_INTERVAL:
self.state.mfd_fps = self.state.mfd_frame_count / el
self.state.mfd_start_time = now
self.state.mfd_frame_count = 0
except Exception:
pass # Ignore FPS calculation errors
# --- Trigger Methods ---
def _trigger_sar_update(self):
"""Triggers SAR reprocessing if not in test mode."""
if self.state.shutting_down or self.state.test_mode_active:
return
if hasattr(self, "image_pipeline") and self.image_pipeline:
self.image_pipeline.process_sar_for_display()
def _trigger_mfd_update(self):
"""Triggers MFD reprocessing if not in test mode."""
if self.state.shutting_down or self.state.test_mode_active:
return
if hasattr(self, "image_pipeline") and self.image_pipeline:
self.image_pipeline.process_mfd_for_display()
def _trigger_map_update_from_sar(self):
"""Triggers a full map update based on current SAR data."""
mgr = getattr(self, "map_integration_manager", None)
if self.state.shutting_down or not config.ENABLE_MAP_OVERLAY or not mgr:
return
geo = self.state.current_sar_geo_info
sar = self.state.current_sar_normalized
if geo and geo.get("valid") and sar is not None and sar.size > 0:
try:
mgr.update_map_overlay(sar, geo)
except Exception as e:
logging.exception(f"[App Trigger Map Update] Error: {e}")
# --- Periodic Update Scheduling ---
def schedule_periodic_updates(self):
"""Schedules the regular update of the status bar."""
if self.state.shutting_down:
return
self.update_status()
interval_ms = max(100, int(config.LOG_UPDATE_INTERVAL * 1000))
if self.root and self.root.winfo_exists():
self.root.after(interval_ms, self.schedule_periodic_updates)
# --- Queue Processors ---
def process_sar_queue(self):
if self.state.shutting_down:
return
img = None
try:
img = self.sar_queue.get(block=False)
self.sar_queue.task_done()
except queue.Empty:
pass
if (
img is not None
and hasattr(self, "display_manager")
and self.display_manager
):
self.display_manager.show_sar_image(img)
if not self.state.shutting_down:
self._reschedule_queue_processor(self.process_sar_queue)
def process_mfd_queue(self):
if self.state.shutting_down:
return
img = None
try:
img = self.mfd_queue.get(block=False)
self.mfd_queue.task_done()
except queue.Empty:
pass
if (
img is not None
and hasattr(self, "display_manager")
and self.display_manager
):
self.display_manager.show_mfd_image(img)
if not self.state.shutting_down:
self._reschedule_queue_processor(self.process_mfd_queue)
# --- Tkinter Queue Processor ---
def process_tkinter_queue(self):
"""Processes commands (mouse coords, map updates) from queue for UI/State."""
if self.state.shutting_down:
return
item: Optional[Tuple[str, Any]] = None
try:
item = self.tkinter_queue.get(block=False)
self.tkinter_queue.task_done()
except queue.Empty:
pass
except Exception as e:
logging.exception("[App QProc Tkinter] Error getting from queue:")
if item is not None:
try:
if isinstance(item, tuple) and len(item) == 2:
command, payload = item
# logging.debug(...) # Reduce verbosity
if command == "MOUSE_COORDS":
self._handle_sar_mouse_coords_update(payload)
elif command == "MAP_MOUSE_COORDS":
self._handle_map_mouse_coords_update(payload)
elif command == "SHOW_MAP":
self._handle_show_map_update(payload)
elif (
command == "REDRAW_MAP"
): # Handle simple redraw (e.g., alpha change)
mgr = getattr(self, "map_integration_manager", None)
if mgr:
mgr._recompose_map_overlay() # Call the recomposition method
else:
logging.warning(
"[App QProc Tkinter] REDRAW_MAP ignored: Map manager not available."
)
else:
logging.warning(
f"[App QProc Tkinter] Unknown command: {command}"
)
else:
logging.warning(
f"[App QProc Tkinter] Invalid item type: {type(item)}"
)
except Exception as e:
logging.exception("[App QProc Tkinter] Error processing item:")
if not self.state.shutting_down:
self._reschedule_queue_processor(self.process_tkinter_queue, delay=100)
def _handle_sar_mouse_coords_update(self, payload: Optional[Tuple[str, str]]):
"""Updates the SAR mouse coordinates UI Entry widget."""
cp = getattr(self, "control_panel", None)
if not cp:
return
lat_s, lon_s = (
payload
if (payload and isinstance(payload, tuple) and len(payload) == 2)
else ("N/A", "N/A")
)
try:
cp.set_mouse_coordinates(lat_s, lon_s)
except Exception as e:
logging.warning(f"[App UI Update] Error updating SAR mouse coords: {e}")
def _handle_map_mouse_coords_update(self, payload: Optional[Tuple[int, int]]):
"""Handles MAP_MOUSE_COORDS: converts pixels to geo and updates UI."""
lat_s, lon_s = ("N/A", "N/A")
cp = getattr(self, "control_panel", None)
if cp and payload and isinstance(payload, tuple) and len(payload) == 2:
mgr = getattr(self, "map_integration_manager", None)
if mgr:
geo_coords = mgr.get_geo_coords_from_map_pixel(payload[0], payload[1])
if geo_coords:
lat_s, lon_s = decimal_to_dms(geo_coords[0], True), decimal_to_dms(
geo_coords[1], False
)
try:
cp.set_map_mouse_coordinates(lat_s, lon_s)
except Exception as e:
logging.warning(f"[App UI Update] Error updating map mouse coords: {e}")
def _handle_show_map_update(self, payload: Optional[ImageType]):
"""Delegates map display to MapIntegrationManager."""
mgr = getattr(self, "map_integration_manager", None)
if mgr:
try:
mgr.display_map(payload)
except Exception as e:
logging.exception("[App QProc Tkinter] Error calling display_map:")
def _reschedule_queue_processor(self, processor_func, delay: Optional[int] = None):
"""Helper to reschedule queue processors."""
if delay is None:
delay = (
max(10, int(1000 / (config.MFD_FPS * 1.5)))
if processor_func in [self.process_sar_queue, self.process_mfd_queue]
else 100
)
try:
if self.root and self.root.winfo_exists():
self.root.after(delay, processor_func)
except Exception:
pass # Ignore errors during shutdown
# --- Mouse Coordinate Handling (SAR) ---
def process_mouse_queue(self):
"""Processes raw SAR mouse coords from queue, calculates geo coords, queues result."""
log_prefix = "[App GeoCalc]"
if self.state.shutting_down: return
raw_coords = None
try: raw_coords = self.mouse_queue.get(block=False); self.mouse_queue.task_done()
except queue.Empty: pass
except Exception as e: logging.exception(f"{log_prefix} Error getting from mouse queue:")
if isinstance(raw_coords, tuple) and len(raw_coords) == 2:
x_disp, y_disp = raw_coords
geo = self.state.current_sar_geo_info; disp_w = self.state.sar_display_width; disp_h = self.state.sar_display_height
lat_s, lon_s = "N/A", "N/A"
# Check if geo info is valid for calculation
is_geo_valid_for_calc = (geo and geo.get("valid") and disp_w > 0 and disp_h > 0 and
geo.get("width_px", 0) > 0 and geo.get("height_px", 0) > 0 and
geo.get("scale_x", 0.0) > 0 and geo.get("scale_y", 0.0) > 0 and
all(k in geo for k in ["lat", "lon", "ref_x", "ref_y", "orientation"]))
if is_geo_valid_for_calc:
try:
# Extract values
orig_w, orig_h = geo["width_px"], geo["height_px"]; scale_x, scale_y = geo["scale_x"], geo["scale_y"]
ref_x, ref_y = geo["ref_x"], geo["ref_y"]; ref_lat_rad, ref_lon_rad = geo["lat"], geo["lon"]
angle_rad = geo.get("orientation", 0.0) # Use original orientation
# Normalize display coords
nx_disp, ny_disp = x_disp / disp_w, y_disp / disp_h
# Apply Inverse Rotation if needed
nx_orig_norm, ny_orig_norm = nx_disp, ny_disp
if abs(angle_rad) > 1e-4: # If rotated significantly
arad_inv = angle_rad; cosa, sina = math.cos(arad_inv), math.sin(arad_inv); cx, cy = 0.5, 0.5
tx, ty = nx_disp - cx, ny_disp - cy; rtx, rty = tx * cosa - ty * sina, tx * sina + ty * cosa
nx_orig_norm, ny_orig_norm = rtx + cx, rty + cy
# Convert normalized back to original pixel space
orig_x = max(0.0, min(nx_orig_norm * orig_w, orig_w - 1.0))
orig_y = max(0.0, min(ny_orig_norm * orig_h, orig_h - 1.0))
# Geodetic Calculation (Simplified)
pixel_delta_x, pixel_delta_y = orig_x - ref_x, ref_y - orig_y
meters_delta_x, meters_delta_y = pixel_delta_x * scale_x, pixel_delta_y * scale_y
M_PER_DLAT, M_PER_DLON_EQ = 111132.954, 111319.488
m_per_dlon = max(abs(M_PER_DLON_EQ * math.cos(ref_lat_rad)), 1e-3)
lat_offset_deg, lon_offset_deg = meters_delta_y / M_PER_DLAT, meters_delta_x / m_per_dlon
final_lat_deg, final_lon_deg = math.degrees(ref_lat_rad) + lat_offset_deg, math.degrees(ref_lon_rad) + lon_offset_deg
# Validate and format
lat_valid = (math.isfinite(final_lat_deg) and abs(final_lat_deg) <= 90.0)
lon_valid = (math.isfinite(final_lon_deg) and abs(final_lon_deg) <= 180.0)
if lat_valid and lon_valid:
lat_s, lon_s = decimal_to_dms(final_lat_deg, True), decimal_to_dms(final_lon_deg, False)
if "Error" in lat_s or "Invalid" in lat_s or "Error" in lon_s or "Invalid" in lon_s: lat_s, lon_s = "Error DMS", "Error DMS"
else: lat_s, lon_s = "Invalid Calc", "Invalid Calc"
except KeyError as ke: logging.error(f"{log_prefix} Missing key in geo_info: {ke}"); lat_s, lon_s = "Error Key", "Error Key"
except Exception as calc_e: logging.exception(f"{log_prefix} Geo calculation error:"); lat_s, lon_s = "Calc Error", "Calc Error"
# Queue Result (even if "N/A" or error strings) using the updated method
self.put_mouse_coordinates_queue(("MOUSE_COORDS", (lat_s, lon_s)))
# Reschedule processor
if not self.state.shutting_down:
self._reschedule_queue_processor(self.process_mouse_queue, delay=50) # Use specific delay
def put_mouse_coordinates_queue(self, command_payload_tuple: Tuple[str, Tuple[str, str]]):
"""Puts processed mouse coords tuple (command, (lat_str, lon_str)) onto Tkinter queue."""
log_prefix = "[App Mouse Queue Put]"
if self.state.shutting_down: return
command, payload = command_payload_tuple
logging.debug(f"{log_prefix} Putting command '{command}' with payload {payload} onto tkinter_queue.")
# Use the utility function, passing self as app_instance
put_queue(
queue_obj=self.tkinter_queue,
item=(command, payload),
queue_name="tkinter",
app_instance=self
)
# --- Queue Processors ---
def process_sar_queue(self):
"""Gets processed SAR image from queue and displays it."""
log_prefix = "[App QProc SAR]"; image_to_display = None
if self.state.shutting_down: return
try: image_to_display = self.sar_queue.get(block=False); self.sar_queue.task_done()
except queue.Empty: pass
except Exception as e: logging.exception(f"{log_prefix} Error getting from SAR display queue:")
if image_to_display is not None:
display_mgr = getattr(self, "display_manager", None)
if display_mgr:
try: display_mgr.show_sar_image(image_to_display)
except Exception as display_e: logging.exception(f"{log_prefix} Error calling DisplayManager.show_sar_image:")
else: logging.error(f"{log_prefix} DisplayManager not available.")
if not self.state.shutting_down: self._reschedule_queue_processor(self.process_sar_queue)
def process_mfd_queue(self):
"""Gets processed MFD image from queue and displays it."""
log_prefix = "[App QProc MFD]"; image_to_display = None
if self.state.shutting_down: return
try: image_to_display = self.mfd_queue.get(block=False); self.mfd_queue.task_done()
except queue.Empty: pass
except Exception as e: logging.exception(f"{log_prefix} Error getting from MFD display queue:")
if image_to_display is not None:
display_mgr = getattr(self, "display_manager", None)
if display_mgr:
try: display_mgr.show_mfd_image(image_to_display)
except Exception as display_e: logging.exception(f"{log_prefix} Error calling DisplayManager.show_mfd_image:")
else: logging.error(f"{log_prefix} DisplayManager not available.")
if not self.state.shutting_down: self._reschedule_queue_processor(self.process_mfd_queue)
def process_tkinter_queue(self):
"""Processes commands (mouse coords, map updates) from queue for UI/State."""
log_prefix = "[App QProc Tkinter]"
if self.state.shutting_down: return
item: Optional[Tuple[str, Any]] = None
try: item = self.tkinter_queue.get(block=False); self.tkinter_queue.task_done()
except queue.Empty: pass
except Exception as e: logging.exception(f"{log_prefix} Error getting from queue:"); item = None
if item is not None:
try:
if isinstance(item, tuple) and len(item) == 2:
command, payload = item
# logging.debug(...) # Reduce verbosity
if command == "MOUSE_COORDS": self._handle_sar_mouse_coords_update(payload)
elif command == "MAP_MOUSE_COORDS": self._handle_map_mouse_coords_update(payload)
elif command == "SHOW_MAP": self._handle_show_map_update(payload)
elif command == "REDRAW_MAP": # Handle simple redraw (e.g., alpha change)
mgr = getattr(self, "map_integration_manager", None)
if mgr: mgr._recompose_map_overlay() # Call the new recomposition method
else: logging.warning(f"{log_prefix} REDRAW_MAP ignored: Map manager unavailable.")
else: logging.warning(f"{log_prefix} Unknown command: {command}")
else: logging.warning(f"{log_prefix} Invalid item type: {type(item)}")
except Exception as e: logging.exception(f"{log_prefix} Error processing item:")
if not self.state.shutting_down: self._reschedule_queue_processor(self.process_tkinter_queue, delay=100)
# --- >>> Handle Map Mouse Coords - Moved here from Part 3 <<< ---
def _handle_map_mouse_coords_update(self, payload: Optional[Tuple[int, int]]):
"""Handles MAP_MOUSE_COORDS command: converts pixels to geo and updates UI."""
log_prefix = "[App QProc Tkinter]"
lat_s, lon_s = ("N/A", "N/A"); cp = getattr(self, "control_panel", None);
if cp and payload and isinstance(payload, tuple) and len(payload)==2:
mgr = getattr(self, "map_integration_manager", None)
if mgr:
geo_coords = mgr.get_geo_coords_from_map_pixel(payload[0], payload[1])
if geo_coords: lat_s, lon_s = decimal_to_dms(geo_coords[0],True), decimal_to_dms(geo_coords[1],False)
if "Error" in lat_s or "Invalid" in lat_s or "Error" in lon_s or "Invalid" in lon_s: lat_s, lon_s = "Error DMS", "Error DMS"
try: cp.set_map_mouse_coordinates(lat_s, lon_s)
except Exception as e: logging.warning(f"[App UI Update] Error updating map mouse coords: {e}")
elif cp: # If payload invalid, still clear UI
try: cp.set_map_mouse_coordinates("N/A", "N/A")
except Exception: pass
# --- >>> Handle SAR Mouse Coords - Moved here from Part 3 <<< ---
def _handle_sar_mouse_coords_update(self, payload: Optional[Tuple[str, str]]):
"""Updates the SAR mouse coordinates UI Entry widget."""
log_prefix = "[App QProc Tkinter]"
cp = getattr(self, "control_panel", None);
if not cp: return
lat_s, lon_s = payload if (payload and isinstance(payload, tuple) and len(payload)==2) else ("N/A", "N/A")
try: cp.set_mouse_coordinates(lat_s, lon_s)
except Exception as e: logging.warning(f"{log_prefix} Error updating SAR mouse coords UI: {e}")
# --- >>> Handle Show Map Update - Moved here from Part 3 <<< ---
def _handle_show_map_update(self, payload: Optional[ImageType]):
"""Handles the SHOW_MAP/REDRAW_MAP command by delegating display to MapIntegrationManager."""
log_prefix = "[App QProc Tkinter]"
# logging.debug(...) # Reduce verbosity
mgr = getattr(self, "map_integration_manager", None)
if mgr:
try: mgr.display_map(payload)
except Exception as e: logging.exception(f"{log_prefix} Error calling map_integration_manager.display_map:")
else: logging.warning(f"{log_prefix} Received map display command but MapIntegrationManager not active.")
def _reschedule_queue_processor(self, processor_func, delay: Optional[int] = None):
"""Helper method to reschedule a queue processor function using root.after."""
if self.state.shutting_down: return # Don't reschedule if shutting down
if delay is None:
# Determine default delay based on processor type
if processor_func in [self.process_sar_queue, self.process_mfd_queue]:
target_fps = config.MFD_FPS
calculated_delay = 1000 / (target_fps * 1.5) if target_fps > 0 else 20 # Target ~1.5x display rate
delay = max(10, int(calculated_delay)) # Minimum delay 10ms
else:
delay = 100 # Default delay for other queues (tkinter, mouse)
try:
# Schedule only if root window exists
if self.root and self.root.winfo_exists():
self.root.after(delay, processor_func)
except Exception as e:
# Log error only if not shutting down
if not self.state.shutting_down:
logging.warning(f"[App Rescheduler] Error rescheduling {processor_func.__name__}: {e}")
# --- Status Update ---
def update_status(self):
"""Updates status bar text and statistics display."""
if not hasattr(self, "state") or self.state.shutting_down:
return
try: # Skip if map loading
sb = getattr(self, "statusbar", None)
if sb and sb.winfo_exists() and "Loading" in sb.cget("text"):
return
except Exception:
pass
stats = self.state.get_statistics()
try:
mode = (
"Test"
if self.state.test_mode_active
else ("Local" if config.USE_LOCAL_IMAGES else "Network")
)
map_on = (
" MapOn"
if config.ENABLE_MAP_OVERLAY
and hasattr(self, "map_integration_manager")
and self.map_integration_manager
else ""
)
mfd_fps = f"MFD:{self.state.mfd_fps:.1f}fps"
sar_fps = (
f"SAR:{self.state.sar_fps:.1f}fps"
if self.state.sar_fps > 0
else "SAR:N/A"
)
status = f"Status: {mode}{map_on} | {mfd_fps} | {sar_fps}"
drop = f"Drop(Q): S={stats['dropped_sar_q']},M={stats['dropped_mfd_q']},Tk={stats['dropped_tk_q']},Mo={stats['dropped_mouse_q']}"
incmpl = f"Incmpl(RX): S={stats['incomplete_sar_rx']},M={stats['incomplete_mfd_rx']}"
if self.root and self.root.winfo_exists():
if sb and sb.winfo_exists():
self.root.after_idle(sb.set_status_text, status)
cp = getattr(self, "control_panel", None)
if cp and cp.winfo_exists():
self.root.after_idle(cp.set_statistics_display, drop, incmpl)
except Exception:
pass # Ignore status update errors
# --- Cleanup ---
def close_app(self):
"""Performs graceful shutdown."""
if hasattr(self, "state") and self.state.shutting_down:
return
if not hasattr(self, "state"):
sys.exit(1)
logging.info("[App Shutdown] Starting shutdown sequence...")
self.state.shutting_down = True
try:
self.set_status("Closing...")
except Exception:
pass
# Shutdown components in reverse order of dependency (roughly)
if hasattr(self, "test_mode_manager"):
self.test_mode_manager.stop_timers()
if hasattr(self, "map_integration_manager"):
self.map_integration_manager.shutdown()
if hasattr(self, "image_recorder"):
self.image_recorder.shutdown()
if self.udp_socket:
close_udp_socket(self.udp_socket)
self.udp_socket = None
if self.udp_thread and self.udp_thread.is_alive():
self.udp_thread.join(timeout=0.5)
pool = (
getattr(self.udp_receiver, "executor", None)
if hasattr(self, "udp_receiver")
else None
)
if pool:
pool.shutdown(wait=False, cancel_futures=True)
if hasattr(self, "display_manager"):
self.display_manager.destroy_windows()
try:
cv2.waitKey(5)
except Exception:
pass
try:
if self.root and self.root.winfo_exists():
self.root.destroy()
except Exception as e:
logging.exception(f"[App Shutdown] Error destroying Tkinter window: {e}")
logging.info("[App Shutdown] Application close sequence finished.")
sys.exit(0)
# --- Main Execution Block ---
if __name__ == "__main__":
# (Implementation unchanged)
root = None
app_instance = None
try:
if config.ENABLE_MAP_OVERLAY and not MAP_MODULES_LOADED:
logging.critical("[App Main] Map enabled but modules failed. Cannot start.")
sys.exit(1)
root = create_main_window(
"Control Panel", config.TKINTER_MIN_WIDTH, config.TKINTER_MIN_HEIGHT, 10, 10
)
app_instance = ControlPanelApp(root)
root.protocol("WM_DELETE_WINDOW", app_instance.close_app)
root.mainloop()
except SystemExit as exit_e:
pass # Already logged by close_app or startup
except Exception as e:
logging.critical("[App Main] UNHANDLED EXCEPTION:", exc_info=True)
sys.exit(1)
finally:
logging.info("=== App End ===")
logging.shutdown()
# --- END OF FILE ControlPanel.py ---