1558 lines
73 KiB
Python
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 ---
|