add war sar metadata view
This commit is contained in:
parent
93833289fd
commit
c0b40803cf
348
ControlPanel.py
348
ControlPanel.py
@ -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
170
ui.py
@ -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 ---
|
||||
Loading…
Reference in New Issue
Block a user