add war sar metadata view

This commit is contained in:
VALLONGOL 2025-04-15 14:10:43 +02:00
parent 93833289fd
commit c0b40803cf
3 changed files with 951 additions and 672 deletions

View File

@ -38,6 +38,7 @@ 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
@ -53,6 +54,7 @@ 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.")
@ -78,7 +80,7 @@ from utils import (
generate_lookat_and_point_kml,
_simplekml_available,
_pyproj_available,
format_ctypes_structure
format_ctypes_structure,
)
from network import create_udp_socket, close_udp_socket
from receiver import UdpReceiver
@ -93,6 +95,7 @@ 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:
@ -113,6 +116,7 @@ if map_libs_found:
from map_utils import MapCalculationError
from map_display import MapDisplayWindow
from map_integration import MapIntegrationManager
MAP_MODULES_LOADED = True
except ImportError as map_import_err:
logging.warning(
@ -144,7 +148,7 @@ class ControlPanelApp:
self.root.title("Control Panel")
try:
# Determine script directory safely
if getattr(sys, 'frozen', False): # Running as compiled executable
if getattr(sys, "frozen", False): # Running as compiled executable
script_dir = os.path.dirname(sys.executable)
elif "__file__" in locals() or "__file__" in globals(): # Running as script
script_dir = os.path.dirname(os.path.abspath(__file__))
@ -208,7 +212,7 @@ class ControlPanelApp:
# Initialize ControlPanel UI (UIPanel class from ui.py)
self.control_panel = UIPanel(self.container_frame, self)
# Grid the control panel into the container's first column
self.control_panel.grid(row=0, column=0, sticky='nsew')
self.control_panel.grid(row=0, column=0, sticky="nsew")
# --- Create Metadata Frame Structure (as attribute of self) ---
# Create metadata frame as child of the container_frame
@ -226,10 +230,10 @@ class ControlPanelApp:
self.metadata_display_text = tk.Text(
self.metadata_text_frame,
wrap=tk.NONE,
state='disabled',
state="disabled",
height=8,
yscrollcommand=self.metadata_scrollbar.set,
font=("Courier New", 8)
font=("Courier New", 8),
)
# Configure scrollbar
self.metadata_scrollbar.config(command=self.metadata_display_text.yview)
@ -237,9 +241,7 @@ class ControlPanelApp:
self.metadata_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.metadata_display_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Set initial placeholder text using the method on self
self.set_metadata_display(
"Enable 'Show SAR Metadata' checkbox to view data..."
)
self.set_metadata_display("Enable 'Show SAR Metadata' checkbox to view data...")
# NOTE: metadata_frame is CREATED but NOT GRIDDED here initially
logging.debug(
f"{log_prefix} Metadata Display frame structure created but not gridded."
@ -248,20 +250,15 @@ class ControlPanelApp:
# --- Initialize Sub-systems ---
# Calculate positions for external windows
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
)
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
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
)
map_x, map_y = self._calculate_map_position(screen_w, initial_sar_w, map_max_w)
# Initialize Display Manager
self.display_manager = DisplayManager(
@ -362,9 +359,11 @@ class ControlPanelApp:
return
try:
statusbar = getattr(self, "statusbar", None)
if (statusbar
if (
statusbar
and isinstance(statusbar, tk.Widget)
and statusbar.winfo_exists()):
and statusbar.winfo_exists()
):
current_text: str = statusbar.cget("text")
# Preserve info after the first '|'
parts = current_text.split("|", 1) # Split only once
@ -373,7 +372,7 @@ class ControlPanelApp:
suffix = f" | {parts[1].strip()}" # Reconstruct suffix
final_text = f"{new_status_prefix}{suffix}"
# Call the specific method of the StatusBar class if available
if hasattr(statusbar, 'set_status_text'):
if hasattr(statusbar, "set_status_text"):
statusbar.set_status_text(final_text)
else: # Fallback
statusbar.config(text=final_text)
@ -462,9 +461,9 @@ class ControlPanelApp:
# Create a single channel grayscale ramp
gray_ramp = np.arange(256, dtype=np.uint8)[:, np.newaxis]
# Convert grayscale ramp to 3-channel BGR
self.state.mfd_lut = cv2.cvtColor(
gray_ramp, cv2.COLOR_GRAY2BGR
)[:, 0, :] # Remove added dimension
self.state.mfd_lut = cv2.cvtColor(gray_ramp, cv2.COLOR_GRAY2BGR)[
:, 0, :
] # Remove added dimension
except Exception as fb_e:
logging.critical(f"[MFD LUT Update] Fallback LUT failed: {fb_e}")
# Ultimate fallback: all black
@ -474,17 +473,21 @@ class ControlPanelApp:
def update_image_mode(self):
"""Callback for the Test Image checkbox."""
log_prefix = "[App Mode Switch]"
if (not hasattr(self, "state")
if (
not hasattr(self, "state")
or not hasattr(self, "test_mode_manager")
or self.state.shutting_down):
or self.state.shutting_down
):
return
try:
cp = getattr(self, "control_panel", None)
var = getattr(cp, "test_image_var", None) if cp else None
# Determine the requested state from the checkbox variable
is_test_req = var.get() == 1 if (
var and isinstance(var, tk.Variable)
) else self.state.test_mode_active
is_test_req = (
var.get() == 1
if (var and isinstance(var, tk.Variable))
else self.state.test_mode_active
)
# Only act if the state is actually changing
if is_test_req != self.state.test_mode_active:
@ -552,7 +555,9 @@ class ControlPanelApp:
# Trigger reprocessing/redisplay of SAR image
self._trigger_sar_update()
except ValueError:
logging.warning(f"[App CB SAR Contrast] Invalid contrast value: {value_str}")
logging.warning(
f"[App CB SAR Contrast] Invalid contrast value: {value_str}"
)
except Exception as e:
logging.exception(f"[App CB SAR Contrast] Error updating contrast: {e}")
@ -570,7 +575,9 @@ class ControlPanelApp:
# Trigger reprocessing/redisplay of SAR image
self._trigger_sar_update()
except ValueError:
logging.warning(f"[App CB SAR Brightness] Invalid brightness value: {value_str}")
logging.warning(
f"[App CB SAR Brightness] Invalid brightness value: {value_str}"
)
except Exception as e:
logging.exception(f"[App CB SAR Brightness] Error updating brightness: {e}")
@ -609,13 +616,17 @@ class ControlPanelApp:
# Clamp value to valid range 0-255
clamped_value = np.clip(intensity_value, 0, 255)
# Update the intensity in the MFD parameters dictionary
self.state.mfd_params["categories"][category_name]["intensity"] = clamped_value
self.state.mfd_params["categories"][category_name][
"intensity"
] = clamped_value
# Recalculate the MFD LUT based on the change
self.update_mfd_lut()
# Trigger reprocessing/redisplay of MFD image
self._trigger_mfd_update()
else:
logging.warning(f"[App CB MFD Intensity] Unknown category: {category_name}")
logging.warning(
f"[App CB MFD Intensity] Unknown category: {category_name}"
)
except Exception as e:
logging.exception(
f"[App CB MFD Intensity] Error updating intensity for '{category_name}': {e}"
@ -633,7 +644,9 @@ class ControlPanelApp:
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}"
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
)
@ -641,14 +654,20 @@ class ControlPanelApp:
if color_code and color_code[0]:
rgb = color_code[0]
# Convert RGB to BGR tuple, clamping values
new_bgr = tuple(np.clip(int(c), 0, 255) for c in (rgb[2], rgb[1], rgb[0]))
new_bgr = tuple(
np.clip(int(c), 0, 255) for c in (rgb[2], rgb[1], rgb[0])
)
# Update state
self.state.mfd_params["categories"][category_name]["color"] = new_bgr
self.update_mfd_lut()
# Schedule UI update for the color preview
cp = getattr(self, "control_panel", None)
if (self.root and self.root.winfo_exists() and cp
and hasattr(cp, "update_mfd_color_display")):
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
)
@ -719,9 +738,7 @@ class ControlPanelApp:
# Trigger a map redraw using recomposition (faster)
self.trigger_map_redraw(full_update=False)
except Exception as e:
logging.exception(
f"{log_prefix} Error handling alpha slider release: {e}"
)
logging.exception(f"{log_prefix} Error handling alpha slider release: {e}")
def toggle_sar_recording(self):
"""Callback for the Record SAR checkbox."""
@ -823,11 +840,17 @@ class ControlPanelApp:
coords_text = cp.map_mouse_coords_var.get()
source_desc = "Map Mouse"
else:
logging.warning(f"{log_prefix} Unknown coordinate source: {coord_source}")
logging.warning(
f"{log_prefix} Unknown coordinate source: {coord_source}"
)
return
if (not coords_text or "N/A" in coords_text
or "Error" in coords_text or "Invalid" in coords_text):
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
@ -836,8 +859,8 @@ class ControlPanelApp:
if lon_sep in coords_text:
parts = coords_text.split(lon_sep, 1)
lat_dms_str = None
if 'Lat=' in parts[0]:
lat_dms_str = parts[0].split('=', 1)[1].strip()
if "Lat=" in parts[0]:
lat_dms_str = parts[0].split("=", 1)[1].strip()
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)
@ -854,10 +877,14 @@ class ControlPanelApp:
open_google_maps(lat_deg, lon_deg)
except ValueError as ve:
logging.error(f"{log_prefix} Parsing error for {source_desc}: {ve} (Text: '{coords_text}')")
logging.error(
f"{log_prefix} Parsing error for {source_desc}: {ve} (Text: '{coords_text}')"
)
self.set_status(f"Error parsing coords for {source_desc}.")
except Exception as e:
logging.exception(f"{log_prefix} Error opening Google Maps for {source_desc}:")
logging.exception(
f"{log_prefix} Error opening Google Maps for {source_desc}:"
)
self.set_status(f"Error opening map for {source_desc}.")
def go_to_google_earth(self, coord_source: str):
@ -896,12 +923,18 @@ class ControlPanelApp:
source_desc = "Map Mouse"
placemark_name = "Mouse on Map"
else:
logging.warning(f"{log_prefix} Unknown coordinate source: {coord_source}")
logging.warning(
f"{log_prefix} Unknown coordinate source: {coord_source}"
)
return
# Validate coordinates text
if (not coords_text or "N/A" in coords_text
or "Error" in coords_text or "Invalid" in coords_text):
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
@ -910,8 +943,8 @@ class ControlPanelApp:
if lon_sep in coords_text:
parts = coords_text.split(lon_sep, 1)
lat_dms_str = None
if 'Lat=' in parts[0]:
lat_dms_str = parts[0].split('=', 1)[1].strip()
if "Lat=" in parts[0]:
lat_dms_str = parts[0].split("=", 1)[1].strip()
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)
@ -933,7 +966,7 @@ class ControlPanelApp:
latitude_deg=lat_deg,
longitude_deg=lon_deg,
placemark_name=placemark_name,
placemark_desc=f"Source: {source_desc}\nCoords: {coords_text}"
placemark_desc=f"Source: {source_desc}\nCoords: {coords_text}",
)
# Launch Google Earth if KML generated
@ -978,7 +1011,7 @@ class ControlPanelApp:
source_map = {
"SAR Center": ("SAR Center", control_panel_ref.sar_center_coords_var),
"SAR Mouse": ("Mouse on SAR", control_panel_ref.mouse_coords_var),
"Map Mouse": ("Mouse on Map", control_panel_ref.map_mouse_coords_var)
"Map Mouse": ("Mouse on Map", control_panel_ref.map_mouse_coords_var),
}
# Iterate through sources, parse coordinates, add valid points
@ -988,17 +1021,29 @@ class ControlPanelApp:
logging.debug(
f"{log_prefix} Processing {internal_name} (KML: {kml_name}) - Text: '{coords_text}'"
)
if (coords_text and "N/A" not in coords_text
and "Error" not in coords_text and "Invalid" not in coords_text):
if (
coords_text
and "N/A" not in coords_text
and "Error" not in coords_text
and "Invalid" not in coords_text
):
try:
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
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)
lon_deg = dms_string_to_decimal(lon_dms_str, is_latitude=False)
lat_deg = dms_string_to_decimal(
lat_dms_str, is_latitude=True
)
lon_deg = dms_string_to_decimal(
lon_dms_str, is_latitude=False
)
else:
raise ValueError("Could not split Lat/Lon parts")
else:
@ -1006,7 +1051,12 @@ class ControlPanelApp:
if lat_deg is not None and lon_deg is not None:
points_to_plot.append(
(lat_deg, lon_deg, kml_name, f"Source: {internal_name}\nCoords: {coords_text}")
(
lat_deg,
lon_deg,
kml_name,
f"Source: {internal_name}\nCoords: {coords_text}",
)
)
logging.debug(
f"{log_prefix} Added valid point: {kml_name} ({lat_deg:.6f}, {lon_deg:.6f})"
@ -1092,7 +1142,7 @@ class ControlPanelApp:
# 1. Grid the metadata frame into the container (column 1)
logging.debug(f"{log_prefix} Gridding metadata frame...")
metadata_frame.grid(
row=0, column=1, sticky='nsew', padx=(5, 5), pady=(0,0)
row=0, column=1, sticky="nsew", padx=(5, 5), pady=(0, 0)
)
# 2. Configure column weight (give equal weight)
container.columnconfigure(1, weight=1)
@ -1131,7 +1181,6 @@ class ControlPanelApp:
except Exception as e:
logging.exception(f"{log_prefix} Error toggling metadata display: {e}")
# --- Initialization Helper Methods ---
def _get_screen_dimensions(self) -> Tuple[int, int]:
"""Gets primary screen dimensions using screeninfo."""
@ -1141,13 +1190,19 @@ class ControlPanelApp:
if not monitors:
raise screeninfo.ScreenInfoError("No monitors detected.")
screen = monitors[0]
logging.debug(f"{log_prefix} Detected Screen: {screen.width}x{screen.height}")
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.")
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]:
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
@ -1158,8 +1213,10 @@ class ControlPanelApp:
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):
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})."
@ -1236,7 +1293,7 @@ class ControlPanelApp:
except Exception as receiver_init_e:
logging.critical(
f"{log_prefix} Failed to initialize UdpReceiver: {receiver_init_e}",
exc_info=True
exc_info=True,
)
self.set_status("Error: Receiver Init Failed")
close_udp_socket(self.udp_socket)
@ -1332,8 +1389,12 @@ class ControlPanelApp:
)
try:
# Placeholder: Implement actual loading from config.MFD_IMAGE_PATH
mfd_path = getattr(config, "MFD_IMAGE_PATH", "local_mfd_indices.png") # Example
logging.warning(f"{log_prefix} Local MFD image loading NYI ({mfd_path}). Using random.")
mfd_path = getattr(
config, "MFD_IMAGE_PATH", "local_mfd_indices.png"
) # Example
logging.warning(
f"{log_prefix} Local MFD image loading NYI ({mfd_path}). Using random."
)
# loaded_indices = load_image(mfd_path, np.uint8) # Hypothetical load
# self.state.local_mfd_image_data_indices = loaded_indices if loaded_indices ... else default_indices
self.state.local_mfd_image_data_indices = default_indices
@ -1380,7 +1441,9 @@ class ControlPanelApp:
# Process MFD if data available
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.state.current_mfd_indices = (
self.state.local_mfd_image_data_indices.copy()
)
self.image_pipeline.process_mfd_for_display()
# Process SAR if data available
if self.state.local_sar_image_data_raw is not None:
@ -1414,7 +1477,8 @@ class ControlPanelApp:
# Network mode status
status = (
f"Listening UDP {self.local_ip}:{self.local_port}"
if self.udp_socket else "Error: No Socket"
if self.udp_socket
else "Error: No Socket"
)
self.set_status(status)
@ -1481,7 +1545,9 @@ class ControlPanelApp:
if config.USE_LOCAL_IMAGES: # Local Mode Restore
# Display local MFD if available
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.state.current_mfd_indices = (
self.state.local_mfd_image_data_indices.copy()
)
if hasattr(self, "image_pipeline"):
self.image_pipeline.process_mfd_for_display()
# Display local SAR if available
@ -1496,7 +1562,8 @@ class ControlPanelApp:
# Set status based on socket state
status = (
f"Listening UDP {self.local_ip}:{self.local_port}"
if self.udp_socket else "Error: No UDP Socket"
if self.udp_socket
else "Error: No UDP Socket"
)
self.set_status(status)
@ -1535,12 +1602,14 @@ class ControlPanelApp:
# Create MFD placeholder (dark gray)
ph_mfd = np.full(
(config.INITIAL_MFD_HEIGHT, config.INITIAL_MFD_WIDTH, 3),
30, dtype=np.uint8
30,
dtype=np.uint8,
)
# Create SAR placeholder (lighter gray, uses current display size)
ph_sar = np.full(
(self.state.sar_display_height, self.state.sar_display_width, 3),
60, dtype=np.uint8
60,
dtype=np.uint8,
)
# Put placeholders onto respective display queues
put_queue(self.mfd_queue, ph_mfd, "mfd", self)
@ -1571,9 +1640,9 @@ class ControlPanelApp:
# Simple redraw requested (e.g., alpha, toggle, marker change)
# Check if data required for fast recomposition is available
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
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
)
if can_recompose:
@ -1634,7 +1703,9 @@ class ControlPanelApp:
# Check if libraries needed for KML were loaded
if not _simplekml_available or not _pyproj_available:
# Log this only once maybe? Or check flag? For now, log each time.
logging.warning("[App KML] Skipping KML generation: simplekml or pyproj missing.")
logging.warning(
"[App KML] Skipping KML generation: simplekml or pyproj missing."
)
return
try:
kml_dir = config.KML_OUTPUT_DIRECTORY
@ -1647,9 +1718,7 @@ class ControlPanelApp:
success = generate_sar_kml(geo_info, fp)
if success:
# Call utility function to clean up old KML files
cleanup_old_kml_files(
config.KML_OUTPUT_DIRECTORY, config.MAX_KML_FILES
)
cleanup_old_kml_files(config.KML_OUTPUT_DIRECTORY, config.MAX_KML_FILES)
# Optionally launch Google Earth
if config.AUTO_LAUNCH_GOOGLE_EARTH:
launch_google_earth(fp) # Use utility function
@ -1779,7 +1848,9 @@ class ControlPanelApp:
# Call manager method to update map and overlay
mgr.update_map_overlay(sar, geo)
except Exception as e:
logging.exception(f"[App Trigger Map Update] Error calling manager: {e}")
logging.exception(
f"[App Trigger Map Update] Error calling manager: {e}"
)
# --- Periodic Update Scheduling ---
def schedule_periodic_updates(self):
@ -1920,9 +1991,11 @@ class ControlPanelApp:
if not cp:
return
# Unpack payload or use default "N/A"
lat_s, lon_s = payload if (
payload and isinstance(payload, tuple) and len(payload) == 2
) else ("N/A", "N/A")
lat_s, lon_s = (
payload
if (payload and isinstance(payload, tuple) and len(payload) == 2)
else ("N/A", "N/A")
)
try:
# Call method on UI object to update the text
cp.set_mouse_coordinates(lat_s, lon_s)
@ -1937,16 +2010,18 @@ class ControlPanelApp:
# Convert map pixel coords to geo coords using MapIntegrationManager
mgr = getattr(self, "map_integration_manager", None)
if mgr:
geo_coords = mgr.get_geo_coords_from_map_pixel(
payload[0], payload[1]
)
geo_coords = mgr.get_geo_coords_from_map_pixel(payload[0], payload[1])
# If conversion successful, format to DMS
if geo_coords:
lat_s_calc = decimal_to_dms(geo_coords[0], True)
lon_s_calc = decimal_to_dms(geo_coords[1], False)
# Check for formatting errors
if ("Error" not in lat_s_calc and "Invalid" not in lat_s_calc and
"Error" not in lon_s_calc and "Invalid" not in lon_s_calc):
if (
"Error" not in lat_s_calc
and "Invalid" not in lat_s_calc
and "Error" not in lon_s_calc
and "Invalid" not in lon_s_calc
):
lat_s, lon_s = lat_s_calc, lon_s_calc
else:
# Use error string if DMS formatting failed
@ -2115,10 +2190,17 @@ class ControlPanelApp:
# 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"])
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:
@ -2162,14 +2244,22 @@ class ControlPanelApp:
final_lon_deg: float = math.degrees(ref_lon_rad) + lon_offset_deg
# Validate calculated coordinates and format to DMS
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
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_calc = decimal_to_dms(final_lat_deg, True)
lon_s_calc = decimal_to_dms(final_lon_deg, False)
# Check for formatting errors
if ("Error" not in lat_s_calc and "Invalid" not in lat_s_calc and
"Error" not in lon_s_calc and "Invalid" not in lon_s_calc):
if (
"Error" not in lat_s_calc
and "Invalid" not in lat_s_calc
and "Error" not in lon_s_calc
and "Invalid" not in lon_s_calc
):
lat_s, lon_s = lat_s_calc, lon_s_calc
else:
lat_s, lon_s = "Error DMS", "Error DMS"
@ -2219,10 +2309,10 @@ class ControlPanelApp:
try:
# Ensure the widget still exists before configuring
if text_widget.winfo_exists():
text_widget.config(state='normal') # Enable editing
text_widget.delete('1.0', tk.END) # Clear existing content
text_widget.insert('1.0', text) # Insert new text
text_widget.config(state='disabled')# Disable editing
text_widget.config(state="normal") # Enable editing
text_widget.delete("1.0", tk.END) # Clear existing content
text_widget.insert("1.0", text) # Insert new text
text_widget.config(state="disabled") # Disable editing
except Exception as e:
logging.warning(f"[App UI Update] Error setting metadata display text: {e}")
@ -2242,18 +2332,28 @@ class ControlPanelApp:
stats = self.state.get_statistics()
try:
# Determine current mode
mode = "Test" if self.state.test_mode_active else (
"Local" if config.USE_LOCAL_IMAGES else "Network"
mode = (
"Test"
if self.state.test_mode_active
else ("Local" if config.USE_LOCAL_IMAGES else "Network")
)
# Check if map is active
map_on = " MapOn" if (
config.ENABLE_MAP_OVERLAY and
hasattr(self, "map_integration_manager") and
self.map_integration_manager
) else ""
map_on = (
" MapOn"
if (
config.ENABLE_MAP_OVERLAY
and hasattr(self, "map_integration_manager")
and self.map_integration_manager
)
else ""
)
# Format FPS strings
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"
sar_fps = (
f"SAR:{self.state.sar_fps:.1f}fps"
if self.state.sar_fps > 0
else "SAR:N/A"
)
# Construct status prefix
status_prefix = f"Status: {mode}{map_on} | {mfd_fps} | {sar_fps}"
@ -2321,9 +2421,11 @@ class ControlPanelApp:
if self.udp_thread.is_alive():
logging.warning("[App Shutdown] UDP thread did not join cleanly.")
# Shutdown worker pool
pool = getattr(
self.udp_receiver, "executor", None
) if hasattr(self, "udp_receiver") else None
pool = (
getattr(self.udp_receiver, "executor", None)
if hasattr(self, "udp_receiver")
else None
)
if pool:
logging.debug("[App Shutdown] Shutting down ThreadPoolExecutor...")
pool.shutdown(wait=False, cancel_futures=True)
@ -2367,7 +2469,8 @@ if __name__ == "__main__":
"Control Panel",
config.TKINTER_MIN_WIDTH,
config.TKINTER_MIN_HEIGHT,
10, 10 # Initial position, App constructor calculates final
10,
10, # Initial position, App constructor calculates final
)
# Instantiate the main application class
app_instance = ControlPanelApp(root) # Pass root window
@ -2381,12 +2484,14 @@ if __name__ == "__main__":
exit_code = exit_e.code if isinstance(exit_e.code, int) else 1
log_level = logging.INFO if exit_code == 0 else logging.WARNING
# Log the exit code without re-raising SystemExit
logging.log(log_level, f"[App Main] Application exited via sys.exit({exit_code}).")
logging.log(
log_level, f"[App Main] Application exited via sys.exit({exit_code})."
)
except ImportError as imp_err:
# Handle critical import errors during startup
logging.critical(
f"[App Main] CRITICAL IMPORT ERROR: {imp_err}. Application cannot start.",
exc_info=True
exc_info=True,
)
print(
f"\nCRITICAL ERROR: Missing required library - {imp_err}\n"
@ -2396,10 +2501,11 @@ if __name__ == "__main__":
except Exception as e:
# Catch any other unhandled exceptions during startup or main loop
logging.critical(
"[App Main] UNHANDLED EXCEPTION during startup or main loop:",
exc_info=True
"[App Main] UNHANDLED EXCEPTION during startup or main loop:", exc_info=True
)
print(
"\nFATAL ERROR: An unhandled exception occurred. Check logs for details.\n"
)
print("\nFATAL ERROR: An unhandled exception occurred. Check logs for details.\n")
sys.exit(1)
finally:
# Ensure logging is shut down properly on any exit path

170
ui.py
View File

@ -10,7 +10,8 @@ Defines the user interface components for the Control Panel application,
including the main control panel area, status bar, map parameters, info display
with Google Maps/Earth links, and helper functions for window creation.
The main ControlPanel frame is designed to be placed within a container managed
by the main application.
by the main application. The metadata display components are now created and
managed directly by the main application (ControlPanelApp).
"""
# Standard library imports
@ -20,7 +21,8 @@ from typing import TYPE_CHECKING, Dict, Tuple, Optional
# Third-party imports
import tkinter as tk
from tkinter import ttk
# Import ScrolledText only if needed within this module (currently not)
# Removed ScrolledText import as it's no longer created here
# from tkinter.scrolledtext import ScrolledText
# Local application imports
@ -38,6 +40,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
information displays, statistics labels, and interaction buttons.
This frame is typically placed in the main application window's container.
"""
def __init__(self, parent: tk.Widget, app: "ControlPanelApp", *args, **kwargs):
"""Initializes the ControlPanel frame."""
log_prefix = "[UI Setup]"
@ -57,16 +60,14 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.sar_lon_shift_var = tk.StringVar(
value=f"{self.app.state.sar_lon_shift_deg:.6f}"
)
self.dropped_stats_var = tk.StringVar(
value="Drop (Q): S=0, M=0, Tk=0, Mo=0"
)
self.incomplete_stats_var = tk.StringVar(
value="Incmpl (RX): S=0, M=0"
)
self.dropped_stats_var = tk.StringVar(value="Drop (Q): S=0, M=0, Tk=0, Mo=0")
self.incomplete_stats_var = tk.StringVar(value="Incmpl (RX): S=0, M=0")
# Checkbox variable for metadata toggle (still needed here)
self.show_meta_var = tk.BooleanVar(value=self.app.state.display_sar_metadata)
# --- References to UI widgets ---
self.mfd_color_labels: Dict[str, tk.Label] = {}
# References to metadata widgets are REMOVED from here
# References to metadata widgets are REMOVED from this class
# --- Initialize UI structure ---
self.init_ui() # Call the UI building method
@ -100,9 +101,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
)
# Record SAR Checkbox
self.record_sar_var = tk.BooleanVar(
value=config.DEFAULT_SAR_RECORDING_ENABLED
)
self.record_sar_var = tk.BooleanVar(value=config.DEFAULT_SAR_RECORDING_ENABLED)
self.record_sar_check = ttk.Checkbutton(
self.sar_params_frame,
text="Record SAR",
@ -144,9 +143,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
# SAR Palette Combobox
self.palette_label = ttk.Label(self.sar_params_frame, text="Palette:")
self.palette_label.grid(
row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1
)
self.palette_label.grid(row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1)
self.palette_combo = ttk.Combobox(
self.sar_params_frame,
values=config.COLOR_PALETTES,
@ -195,14 +192,14 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1
)
# SAR Metadata Checkbox
# SAR Metadata Checkbox (Still created here as it belongs logically with SAR params)
sar_row += 1
self.show_meta_var = tk.BooleanVar(value=self.app.state.display_sar_metadata)
# self.show_meta_var is already created in __init__
self.show_meta_check = ttk.Checkbutton(
self.sar_params_frame,
text="Show SAR Metadata",
variable=self.show_meta_var,
command=self.app.toggle_sar_metadata_display # Link to app callback
command=self.app.toggle_sar_metadata_display, # Link to app callback
)
self.show_meta_check.grid(
row=sar_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=(5, 2)
@ -216,14 +213,20 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.mfd_params_frame = ttk.Labelframe(self, text="MFD Parameters", padding=5)
self.mfd_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2)
mfd_categories_ordered = [ # Order for display
"Occlusion", "Cat A", "Cat B", "Cat C", "Cat C1", "Cat C2", "Cat C3",
mfd_categories_ordered = [
"Occlusion",
"Cat A",
"Cat B",
"Cat C",
"Cat C1",
"Cat C2",
"Cat C3",
]
num_categories = len(mfd_categories_ordered)
for index, name in enumerate(mfd_categories_ordered):
row = index // 2 # Two categories per row
col_offset = 0 if (index % 2 == 0) else 4 # Offset for second column
row = index // 2
col_offset = 0 if (index % 2 == 0) else 4
# Category Label
cat_label = ttk.Label(self.mfd_params_frame, text=f"{name}:")
@ -233,12 +236,12 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
# Intensity Slider Variable and Widget
intensity_var = tk.IntVar(value=config.DEFAULT_MFD_INTENSITY)
try: # Set initial value from state
try:
intensity_var.set(
self.app.state.mfd_params["categories"][name]["intensity"]
)
except Exception:
pass # Ignore if state not ready or key missing
pass
intensity_scale = ttk.Scale(
self.mfd_params_frame,
orient=tk.HORIZONTAL,
@ -246,7 +249,6 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
from_=0,
to=255,
variable=intensity_var,
# Use lambda to pass name and var correctly to callback
command=lambda v, n=name, var=intensity_var: (
self.app.update_mfd_category_intensity(n, var.get())
),
@ -268,41 +270,39 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
# Color Preview Label
color_label = tk.Label(
self.mfd_params_frame,
text="",
width=3,
relief=tk.SUNKEN,
borderwidth=1
self.mfd_params_frame, text="", width=3, relief=tk.SUNKEN, borderwidth=1
)
try: # Set initial color from state
try:
bgr = self.app.state.mfd_params["categories"][name]["color"]
hex_color = f"#{bgr[2]:02x}{bgr[1]:02x}{bgr[0]:02x}"
color_label.config(background=hex_color)
except Exception:
color_label.config(background="grey") # Fallback
color_label.config(background="grey")
color_label.grid(
row=row, column=3 + col_offset, sticky=tk.W, padx=(1, 5), pady=1
)
self.mfd_color_labels[name] = color_label # Store label reference
self.mfd_color_labels[name] = color_label
# Raw Map Intensity Slider
last_cat_row = (num_categories - 1) // 2
# Determine position based on number of categories
raw_map_col_offset = 4 if (num_categories % 2 != 0) else 0
raw_map_row = last_cat_row if (num_categories % 2 != 0) else last_cat_row + 1
raw_map_label = ttk.Label(self.mfd_params_frame, text="Raw Map:")
raw_map_label.grid(
row=raw_map_row, column=0 + raw_map_col_offset, sticky=tk.W, padx=(5, 1), pady=1
row=raw_map_row,
column=0 + raw_map_col_offset,
sticky=tk.W,
padx=(5, 1),
pady=1,
)
raw_map_intensity_var = tk.IntVar(value=config.DEFAULT_MFD_RAW_MAP_INTENSITY)
try: # Set initial value from state
try:
raw_map_intensity_var.set(self.app.state.mfd_params["raw_map_intensity"])
except Exception:
pass
# Store var reference if needed elsewhere
self.mfd_raw_map_intensity_var = raw_map_intensity_var
self.mfd_raw_map_intensity_var = raw_map_intensity_var # Keep reference
raw_map_scale = ttk.Scale(
self.mfd_params_frame,
@ -318,14 +318,14 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
raw_map_scale.grid(
row=raw_map_row,
column=1 + raw_map_col_offset,
columnspan=3, # Span 3 cols after label
columnspan=3,
sticky=tk.EW,
padx=(1, 5),
pady=1,
)
# Configure MFD frame column weights
self.mfd_params_frame.columnconfigure(1, weight=1) # Allow sliders to expand
self.mfd_params_frame.columnconfigure(5, weight=1) # Allow sliders on right
self.mfd_params_frame.columnconfigure(1, weight=1)
self.mfd_params_frame.columnconfigure(5, weight=1)
# --- 3. Map Parameters Frame ---
self.map_params_frame = ttk.Labelframe(self, text="Map Parameters", padding=5)
@ -342,9 +342,9 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.map_params_frame,
values=config.MAP_SIZE_FACTORS,
state="readonly",
width=6
width=6,
)
self.map_size_combo.set(config.DEFAULT_MAP_SIZE) # Set default initially
self.map_size_combo.set(config.DEFAULT_MAP_SIZE)
self.map_size_combo.grid(
row=map_row, column=1, sticky=tk.EW, padx=(2, 10), pady=1
)
@ -354,7 +354,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.save_map_button = ttk.Button(
self.map_params_frame,
text="Save Map View",
command=self.app.save_current_map_view
command=self.app.save_current_map_view,
)
self.save_map_button.grid(
row=map_row, column=2, columnspan=4, sticky=tk.E, padx=5, pady=1
@ -380,9 +380,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
# SAR Overlay Alpha Slider
self.alpha_label = ttk.Label(self.map_params_frame, text="SAR Overlay Alpha:")
self.alpha_label.grid(
row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1
)
self.alpha_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1)
self.sar_overlay_alpha_var = tk.DoubleVar(
value=self.app.state.map_sar_overlay_alpha
)
@ -393,7 +391,6 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
to=1.0,
variable=self.sar_overlay_alpha_var,
)
# Trigger update only on release for performance
self.alpha_scale.bind("<ButtonRelease-1>", self.app.on_alpha_slider_release)
self.alpha_scale.grid(
row=map_row, column=1, columnspan=5, sticky=tk.EW, padx=(0, 5), pady=1
@ -403,44 +400,35 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
# SAR Shift Inputs and Apply Button
shift_label = ttk.Label(self.map_params_frame, text="SAR Shift (deg):")
shift_label.grid(
row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1
)
shift_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1)
lat_label = ttk.Label(self.map_params_frame, text="Lat:")
lat_label.grid(row=map_row, column=1, sticky=tk.W, padx=(0, 0), pady=1)
self.lat_shift_entry = ttk.Entry(
self.map_params_frame,
textvariable=self.sar_lat_shift_var,
width=10
self.map_params_frame, textvariable=self.sar_lat_shift_var, width=10
)
self.lat_shift_entry.grid(
row=map_row, column=2, sticky=tk.W, padx=(0, 5), pady=1
)
lon_label = ttk.Label(self.map_params_frame, text="Lon:")
lon_label.grid(row=map_row, column=3, sticky=tk.W, padx=(5, 0), pady=1)
self.lon_shift_entry = ttk.Entry(
self.map_params_frame,
textvariable=self.sar_lon_shift_var,
width=10
self.map_params_frame, textvariable=self.sar_lon_shift_var, width=10
)
self.lon_shift_entry.grid(
row=map_row, column=4, sticky=tk.W, padx=(0, 5), pady=1
)
self.apply_shift_button = ttk.Button(
self.map_params_frame,
text="Apply Shift",
command=self.app.apply_sar_overlay_shift
command=self.app.apply_sar_overlay_shift,
)
self.apply_shift_button.grid(
row=map_row, column=5, sticky=tk.E, padx=(5, 5), pady=1
)
# Configure Map frame column weights
self.map_params_frame.columnconfigure(2, weight=1) # Allow Lat entry expand
self.map_params_frame.columnconfigure(4, weight=1) # Allow Lon entry expand
self.map_params_frame.columnconfigure(2, weight=1)
self.map_params_frame.columnconfigure(4, weight=1)
# --- 4. Info Display Frame ---
self.info_display_frame = ttk.Labelframe(self, text="Info Display", padding=5)
@ -456,7 +444,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
textvariable=self.sar_center_coords_var,
state="readonly",
width=35
width=35,
)
self.sar_center_entry.grid(
row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1
@ -465,7 +453,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
text="Go",
width=button_width,
command=lambda: self.app.go_to_google_maps("sar_center")
command=lambda: self.app.go_to_google_maps("sar_center"),
)
self.ref_gmaps_button.grid(
row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1
@ -474,7 +462,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
text="GE",
width=button_width,
command=lambda: self.app.go_to_google_earth("sar_center")
command=lambda: self.app.go_to_google_earth("sar_center"),
)
self.ref_gearth_button.grid(
row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1
@ -488,20 +476,18 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
textvariable=self.sar_orientation_var,
state="readonly",
width=15
width=15,
)
self.sar_orientation_entry.grid(
row=info_row, column=1, sticky=tk.EW, padx=(0, 5), pady=1
)
size_label = ttk.Label(self.info_display_frame, text="Image Size:")
size_label.grid(
row=info_row, column=2, sticky=tk.W, padx=(10, 2), pady=1
)
size_label.grid(row=info_row, column=2, sticky=tk.W, padx=(10, 2), pady=1)
self.sar_size_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.sar_size_km_var,
state="readonly",
width=25
width=25,
)
self.sar_size_entry.grid(
row=info_row, column=3, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1
@ -515,7 +501,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
textvariable=self.mouse_coords_var,
state="readonly",
width=35
width=35,
)
self.mouse_latlon_entry.grid(
row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1
@ -524,7 +510,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
text="Go",
width=button_width,
command=lambda: self.app.go_to_google_maps("sar_mouse")
command=lambda: self.app.go_to_google_maps("sar_mouse"),
)
self.sar_mouse_gmaps_button.grid(
row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1
@ -533,7 +519,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
text="GE",
width=button_width,
command=lambda: self.app.go_to_google_earth("sar_mouse")
command=lambda: self.app.go_to_google_earth("sar_mouse"),
)
self.sar_mouse_gearth_button.grid(
row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1
@ -547,7 +533,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
textvariable=self.map_mouse_coords_var,
state="readonly",
width=35
width=35,
)
self.map_mouse_latlon_entry.grid(
row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1
@ -556,7 +542,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
text="Go",
width=button_width,
command=lambda: self.app.go_to_google_maps("map_mouse")
command=lambda: self.app.go_to_google_maps("map_mouse"),
)
self.map_mouse_gmaps_button.grid(
row=info_row, column=4, sticky=tk.E, padx=(0, 1), pady=1
@ -565,7 +551,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
text="GE",
width=button_width,
command=lambda: self.app.go_to_google_earth("map_mouse")
command=lambda: self.app.go_to_google_earth("map_mouse"),
)
self.map_mouse_gearth_button.grid(
row=info_row, column=5, sticky=tk.E, padx=(0, 5), pady=1
@ -574,14 +560,12 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
# --- Row 4: Drop & Incomplete Stats ---
dropped_label_text = ttk.Label(self.info_display_frame, text="Stats Drop:")
dropped_label_text.grid(
row=info_row, column=0, sticky=tk.W, padx=5, pady=1
)
dropped_label_text.grid(row=info_row, column=0, sticky=tk.W, padx=5, pady=1)
self.dropped_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.dropped_stats_var,
state="readonly",
width=30
width=30,
)
self.dropped_entry.grid(
row=info_row, column=1, sticky=tk.EW, padx=(0, 5), pady=1
@ -594,7 +578,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
self.info_display_frame,
textvariable=self.incomplete_stats_var,
state="readonly",
width=15
width=15,
)
self.incomplete_entry.grid(
row=info_row, column=3, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1
@ -603,17 +587,10 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
# --- Row 5: "GE All" Button ---
self.ge_all_button = ttk.Button(
self.info_display_frame,
text="GE All",
command=self.app.go_to_all_gearth # Link to app callback
self.info_display_frame, text="GE All", command=self.app.go_to_all_gearth
)
self.ge_all_button.grid(
row=info_row,
column=0, # Start column 0
columnspan=6, # Span all 6 columns
sticky=tk.EW, # Expand horizontally
padx=5,
pady=(5, 5) # Padding top and bottom
row=info_row, column=0, columnspan=6, sticky=tk.EW, padx=5, pady=(5, 5)
)
# Configure column weights for Info Display frame
@ -624,12 +601,11 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
logging.debug(f"{log_prefix} Info Display frame created.")
# --- 5. Metadata Display Frame (Creation REMOVED from here) ---
# The structure is now created in ControlPanelApp.__init__
# This is now created and managed in ControlPanelApp
# --- End of init_ui ---
logging.debug(f"{log_prefix} init_ui widget creation complete.")
# --- UI Update Methods ---
def set_sar_center_coords(self, latitude_str: str, longitude_str: str):
"""Updates the SAR Center coordinates display."""
@ -698,6 +674,7 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
f"[UI Update] Error updating MFD color for {category_name}: {e}"
)
# --- StatusBar Class ---
class StatusBar(ttk.Label):
"""Represents the status bar at the bottom of the main window."""
@ -712,7 +689,7 @@ class StatusBar(ttk.Label):
anchor=tk.W, # Anchor text to the West (left)
padding=(5, 2), # Add some internal padding
*args,
**kwargs
**kwargs,
)
# Packed by the main application layout manager
@ -726,6 +703,7 @@ class StatusBar(ttk.Label):
# Ignore errors during status update, especially during shutdown
pass
# --- Window Creation Helper ---
def create_main_window(
title: str, min_width: int, min_height: int, x_pos: int, y_pos: int
@ -740,9 +718,10 @@ def create_main_window(
# Set minimum size constraint
root.minsize(min_width, min_height)
# Set initial position (may be overridden by OS window manager)
# This is less critical now as the app constructor sets the final position
root.geometry(f"+{x_pos}+{y_pos}")
logging.debug(
f"{log_prefix} Main Tkinter root window created (requested pos: {x_pos},{y_pos})."
f"{log_prefix} Main Tkinter root window created (initial requested pos: {x_pos},{y_pos})."
)
return root
except Exception as e:
@ -752,4 +731,5 @@ def create_main_window(
# Re-raise the exception to halt execution if window creation fails
raise
# --- END OF FILE ui.py ---

761
utils.py

File diff suppressed because it is too large Load Diff