add map elevation, using module geoelevation.

This commit is contained in:
VALLONGOL 2025-05-13 12:16:27 +02:00
parent 30be7ea111
commit dc581931a7
4 changed files with 636 additions and 489 deletions

3
.gitignore vendored
View File

@ -8,4 +8,5 @@ _build/
_dist/
sar_images/
kml_output/
_req_packages/
_req_packages/
elevation_cache/

View File

@ -1,6 +1,3 @@
# --- START OF FILE ControlPanel.py ---
# ControlPanel.py (Previously app.py)
"""
THIS SOFTWARE IS PROVIDED AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
@ -22,7 +19,7 @@ import logging
import math
import sys
import socket
from typing import Optional, Tuple, Any, Dict, TYPE_CHECKING, Union, List
from typing import Optional, Tuple, Any, Dict, TYPE_CHECKING, Union, List, Callable
import datetime
import tkinter.filedialog as fd
from pathlib import Path
@ -48,47 +45,32 @@ except ImportError:
"[App Init] Pillow library not found. Map/Image functionality will fail."
)
# --- Configuration Import ---
from controlpanel import config
# --- Logging Setup ---
# --- Absolute Imports for Application Modules ---
try:
from controlpanel import config # Config is now essential before GeoElevation logic
from controlpanel.logging_config import setup_logging
setup_logging()
except ImportError:
print("ERROR: logging_config.py not found. Using basic logging.")
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s",
from controlpanel.app_state import AppState
# GUI related imports
from controlpanel.gui.ui import ControlPanel as UIPanel, StatusBar, create_main_window
from controlpanel.gui.display import DisplayManager
# Core processing imports
from controlpanel.core.receiver import UdpReceiver
from controlpanel.core.test_mode_manager import TestModeManager
from controlpanel.core.image_pipeline import ImagePipeline
from controlpanel.core.image_recorder import ImageRecorder
# Utility imports
from controlpanel.utils.utils import (
put_queue, clear_queue, decimal_to_dms, dms_string_to_decimal,
generate_sar_kml, launch_google_earth, cleanup_kml_output_directory,
open_google_maps, generate_lookat_and_point_kml, generate_composite_kml,
_simplekml_available, _pyproj_available, format_ctypes_structure,
)
# --- Application Modules Import ---
from controlpanel.gui.ui import ControlPanel as UIPanel, StatusBar, create_main_window
from controlpanel.gui.display import DisplayManager
from controlpanel.utils.utils import (
put_queue,
clear_queue,
decimal_to_dms,
dms_string_to_decimal,
generate_sar_kml, # Rimane invariato
launch_google_earth,
# cleanup_old_kml_files, # Rimuovere o commentare il vecchio import
cleanup_kml_output_directory, # <<< NUOVO IMPORT
open_google_maps,
generate_lookat_and_point_kml,
generate_composite_kml, # Modificato precedentemente per usare questa
_simplekml_available,
_pyproj_available,
format_ctypes_structure,
)
from controlpanel.utils.network import create_udp_socket, close_udp_socket
from controlpanel.core.receiver import UdpReceiver
from controlpanel.app_state import AppState
from controlpanel.core.test_mode_manager import TestModeManager
from controlpanel.core.image_pipeline import ImagePipeline
from controlpanel.utils.image_processing import load_image, normalize_image, apply_color_palette
from controlpanel.core.image_recorder import ImageRecorder
from controlpanel.utils.network import create_udp_socket, close_udp_socket
from controlpanel.utils.image_processing import load_image, normalize_image, apply_color_palette
except ImportError as app_import_err:
print(f"FATAL ERROR: Failed to import core application modules: {app_import_err}")
logging.critical(f"FATAL ERROR: Failed to import core application modules: {app_import_err}", exc_info=True)
sys.exit(1)
# --- Map related imports (Conditional) ---
map_libs_found = True
@ -151,6 +133,97 @@ DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- End Constants ---
# --- >>> START OF NEW GEOELEVATION INTEGRATION CODE <<< ---
# Global variables for GeoElevation availability and function reference
GEOELEVATION_AVAILABLE: bool = False
get_elevation_function: Optional[Callable] = None
_geoelevation_log_prefix = "[GeoElevation Integration]"
try:
# 1. Check if the path is configured in controlpanel/config.py
configured_path_str = getattr(config, "GEOELEVATION_PROJECT_ROOT_PATH", None)
if configured_path_str and isinstance(configured_path_str, str) and configured_path_str.strip():
configured_path = Path(configured_path_str.strip())
logging.info(f"{_geoelevation_log_prefix} GEOELEVATION_PROJECT_ROOT_PATH configured to: '{configured_path_str}'")
# 2. Resolve to absolute path if relative
# Relative path is assumed to be relative to the *ControlPanel project root*
# (the directory containing the 'controlpanel' package folder).
if not configured_path.is_absolute():
# __file__ points to controlpanel/app_main.py
# controlpanel_package_dir is controlpanel/
controlpanel_package_dir = Path(__file__).parent
# project_root_dir is the parent of controlpanel/
project_root_dir = controlpanel_package_dir.parent
absolute_geoelevation_path = (project_root_dir / configured_path).resolve()
logging.debug(f"{_geoelevation_log_prefix} Resolved relative path '{configured_path}' to '{absolute_geoelevation_path}'")
configured_path = absolute_geoelevation_path
# 3. Validate the path and the 'geoelevation' subfolder
potential_module_dir = configured_path / "geoelevation" # The actual package folder
if configured_path.is_dir() and potential_module_dir.is_dir() and (potential_module_dir / "__init__.py").is_file():
logging.debug(f"{_geoelevation_log_prefix} Validated GeoElevation project root: '{configured_path}'")
logging.debug(f"{_geoelevation_log_prefix} Potential GeoElevation module directory: '{potential_module_dir}'")
# 4. Add the *parent* of the 'geoelevation' module to sys.path
# This allows `from geoelevation import ...`
# So, we add `configured_path` to sys.path.
path_to_add_to_sys = str(configured_path)
path_added_to_sys = False
if path_to_add_to_sys not in sys.path:
sys.path.insert(0, path_to_add_to_sys) # Add to beginning for priority
path_added_to_sys = True
logging.info(f"{_geoelevation_log_prefix} Added '{path_to_add_to_sys}' to sys.path for GeoElevation import.")
# 5. Attempt to import the specific function
try:
from geoelevation import get_point_elevation as ge_get_point_elevation
# from geoelevation import get_point_elevation as ge_get_point_elevation # If function is in __init__.py
GEOELEVATION_AVAILABLE = True
get_elevation_function = ge_get_point_elevation
logging.info(f"{_geoelevation_log_prefix} Module 'geoelevation.elevation_service.get_point_elevation' loaded successfully. Elevation feature ENABLED.")
# Optional: Clean up sys.path if it was modified for this import only
# This is generally good practice to avoid polluting sys.path permanently
if path_added_to_sys and path_to_add_to_sys in sys.path:
try:
sys.path.remove(path_to_add_to_sys)
logging.debug(f"{_geoelevation_log_prefix} Removed '{path_to_add_to_sys}' from sys.path after successful import.")
except ValueError:
pass # Should not happen if path_added_to_sys is True
except ImportError as e_import:
logging.warning(f"{_geoelevation_log_prefix} Failed to import 'get_point_elevation' from GeoElevation module at '{potential_module_dir}': {e_import}. Elevation feature DISABLED.")
GEOELEVATION_AVAILABLE = False
# If import failed, and we added the path, consider removing it too
if path_added_to_sys and path_to_add_to_sys in sys.path:
try:
sys.path.remove(path_to_add_to_sys)
logging.debug(f"{_geoelevation_log_prefix} Removed '{path_to_add_to_sys}' from sys.path after import failure.")
except ValueError:
pass
except Exception as e_general_import:
logging.error(f"{_geoelevation_log_prefix} An unexpected error occurred during GeoElevation import: {e_general_import}. Elevation feature DISABLED.", exc_info=True)
GEOELEVATION_AVAILABLE = False
if path_added_to_sys and path_to_add_to_sys in sys.path: # Cleanup
try: sys.path.remove(path_to_add_to_sys)
except ValueError: pass
else: # Path not valid (doesn't exist or geoelevation subfolder missing)
logging.warning(f"{_geoelevation_log_prefix} Configured GeoElevation path '{configured_path}' or its 'geoelevation' subfolder (with __init__.py) is not valid. Elevation feature DISABLED.")
GEOELEVATION_AVAILABLE = False
else: # Path not configured or empty
logging.info(f"{_geoelevation_log_prefix} GEOELEVATION_PROJECT_ROOT_PATH not configured or empty. Elevation feature DISABLED.")
GEOELEVATION_AVAILABLE = False
except Exception as e_outer_config:
# Catch any error during the config reading/path manipulation itself
logging.error(f"{_geoelevation_log_prefix} Error processing GeoElevation configuration: {e_outer_config}. Elevation feature DISABLED.", exc_info=True)
GEOELEVATION_AVAILABLE = False
# --- >>> END OF NEW GEOELEVATION INTEGRATION CODE <<< ---
# --- Main Application Class ---
class ControlPanelApp:
@ -362,8 +435,16 @@ class ControlPanelApp:
# Set initial mode (Test or Normal/Local)
self.update_image_mode()
# --- Add a new attribute for elevation request tracking ---
self.active_elevation_requests: Dict[str, threading.Thread] = {} # Key: "src_lat_lon", Value: Thread
logging.info(f"{log_prefix} Application initialization complete.")
if GEOELEVATION_AVAILABLE:
logging.info(f"{log_prefix} GeoElevation functionality is AVAILABLE.")
else:
logging.warning(f"{log_prefix} GeoElevation functionality is UNAVAILABLE.")
# --- Status Update Method ---
def set_status(self, message: str):
@ -405,6 +486,148 @@ class ControlPanelApp:
self.root.after_idle(_update)
except Exception as e:
logging.warning(f"{log_prefix} Error scheduling status update: {e}")
def request_elevation_for_point(self, source_description: str, latitude: float, longitude: float):
"""
Requests elevation for a given point asynchronously if GeoElevation is available.
A unique identifier for the request is created to manage concurrent requests.
Args:
source_description (str): A description of the point source (e.g., "SAR_Center", "SAR_Mouse_Click").
latitude (float): Latitude of the point in decimal degrees.
longitude (float): Longitude of the point in decimal degrees.
"""
_log_prefix_req = "[App Elev Req]"
if not GEOELEVATION_AVAILABLE or get_elevation_function is None:
logging.warning(f"{_log_prefix_req} GeoElevation not available. Cannot request elevation for {source_description}.")
# Optionally, update UI to indicate unavailability if this is the first time
self._update_elevation_display(source_description, "GeoElevation N/A")
return
if not (math.isfinite(latitude) and math.isfinite(longitude)):
logging.warning(f"{_log_prefix_req} Invalid coordinates for {source_description}: Lat={latitude}, Lon={longitude}. Skipping elevation request.")
self._update_elevation_display(source_description, "Invalid Coords")
return
# Create a unique request ID based on source and coordinates to prevent duplicate threads for the exact same point if clicked rapidly
# This is a simple way; more robust would involve a proper task queue with cancellation.
request_id = f"{source_description}_{latitude:.6f}_{longitude:.6f}"
# Check if a request for this exact ID is already running
if request_id in self.active_elevation_requests and self.active_elevation_requests[request_id].is_alive():
logging.debug(f"{_log_prefix_req} Elevation request for '{request_id}' is already active. Skipping.")
return
logging.info(f"{_log_prefix_req} Requesting elevation for {source_description} at ({latitude:.4f}, {longitude:.4f}). Request ID: {request_id}")
self._update_elevation_display(source_description, "Elevation: Requesting...")
# Create and start a new thread for the elevation call
elevation_thread = threading.Thread(
target=self._get_elevation_worker,
args=(request_id, source_description, latitude, longitude, get_elevation_function),
name=f"ElevationWorker-{request_id}",
daemon=True
)
self.active_elevation_requests[request_id] = elevation_thread
elevation_thread.start()
def _get_elevation_worker(
self,
request_id: str,
source_description: str,
latitude: float,
longitude: float,
elevation_func: Callable
):
"""
Worker function executed in a separate thread to call the GeoElevation service.
Sends the result or error back to the main thread via the Tkinter queue.
"""
_log_prefix_worker = f"[App Elev Work] ({request_id})"
# --- MODIFIED PAYLOAD STRUCTURE ---
result_payload: Dict[str, Any] = {
"request_id": request_id,
"source_description": source_description,
"latitude": latitude,
"longitude": longitude,
"elevation_value": None, # Stores the numeric elevation or None/NaN
"display_text": "Elev: Error", # Default display text in case of issues
"error": None
}
try:
logging.debug(f"{_log_prefix_worker} Calling GeoElevation function...")
elevation = elevation_func(latitude, longitude, show_progress_dialog=False)
logging.debug(f"{_log_prefix_worker} GeoElevation call returned: {elevation}")
if elevation is None:
result_payload["display_text"] = "Elev: N/A" # More concise
result_payload["elevation_value"] = None
elif elevation != elevation: # Check for NaN
result_payload["display_text"] = "Elev: NoData" # More concise
result_payload["elevation_value"] = float('nan') # Store NaN
else:
numeric_elevation = float(elevation)
result_payload["elevation_value"] = numeric_elevation
result_payload["display_text"] = f"{numeric_elevation:.1f} m" # Desired format
except RuntimeError as e_rt:
logging.error(f"{_log_prefix_worker} GeoElevation RuntimeError: {e_rt}")
result_payload["display_text"] = "Elev: Err(RT)"
result_payload["error"] = str(e_rt)
except Exception as e_generic:
logging.exception(f"{_log_prefix_worker} Unexpected error calling GeoElevation:")
result_payload["display_text"] = "Elev: Err(Sys)"
result_payload["error"] = str(e_generic)
finally:
if hasattr(self, 'tkinter_queue') and self.tkinter_queue is not None:
put_queue(self.tkinter_queue, ("ELEVATION_RESULT", result_payload), "tkinter", self)
if request_id in self.active_elevation_requests:
del self.active_elevation_requests[request_id]
logging.debug(f"{_log_prefix_worker} Worker finished.")
def _update_elevation_display(self, source_description: str, display_text_value: str):
"""
Updates the UI to show the elevation or status by calling the
appropriate method on the control_panel (UIPanel) instance.
Args:
source_description (str): Describes the source of the elevation point.
Should match one of config.ELEVATION_SOURCE_* constants.
display_text_value (str): The pre-formatted text to display (e.g., "200.0 m", "Elev: N/A").
"""
_log_prefix_disp = "[App Elev Disp]"
logging.debug(f"{_log_prefix_disp} For '{source_description}': {display_text_value}")
control_panel_ui = getattr(self, "control_panel", None)
if not control_panel_ui:
logging.error(f"{_log_prefix_disp} ControlPanel UI instance not found. Cannot update elevation display.")
return
try:
if source_description == config.ELEVATION_SOURCE_SAR_CENTER:
if hasattr(control_panel_ui, 'set_sar_center_elevation'):
control_panel_ui.set_sar_center_elevation(display_text_value)
else:
logging.warning(f"{_log_prefix_disp} Method 'set_sar_center_elevation' not found.")
elif source_description == config.ELEVATION_SOURCE_SAR_MOUSE:
if hasattr(control_panel_ui, 'set_sar_mouse_elevation'):
control_panel_ui.set_sar_mouse_elevation(display_text_value)
else:
logging.warning(f"{_log_prefix_disp} Method 'set_sar_mouse_elevation' not found.")
elif source_description == config.ELEVATION_SOURCE_MAP_MOUSE:
if hasattr(control_panel_ui, 'set_map_mouse_elevation'):
control_panel_ui.set_map_mouse_elevation(display_text_value)
else:
logging.warning(f"{_log_prefix_disp} Method 'set_map_mouse_elevation' not found.")
else:
logging.warning(f"{_log_prefix_disp} Unknown elevation source '{source_description}'. No specific UI label updated.")
# You might still want to update a general status if needed:
# self.statusbar.set_status_text(f"Elevation Info: [{source_description}] {display_text_value}")
except Exception as e:
logging.exception(f"{_log_prefix_disp} Error updating elevation display for '{source_description}':")
# --- LUT Generation Methods ---
def update_brightness_contrast_lut(self):
@ -1811,27 +2034,26 @@ class ControlPanelApp:
# --- UI Display Update Helpers ---
def _update_sar_ui_labels(self):
"""Updates SAR related UI Entry widgets from AppState."""
"""Updates SAR related UI Entry widgets from AppState and requests SAR center elevation."""
cp = getattr(self, "control_panel", None)
# Check if control panel exists and its window is valid
if not cp or not cp.winfo_exists():
return
# ... (codice esistente per aggiornare lat_s, lon_s, orient_s, size_s) ...
geo = self.state.current_sar_geo_info
lat_s, lon_s, orient_s, size_s = "N/A", "N/A", "N/A", "N/A"
is_valid = geo and geo.get("valid")
is_valid_geo_for_ui = geo and geo.get("valid")
lat_deg_for_elevation: Optional[float] = None
lon_deg_for_elevation: Optional[float] = None
if is_valid:
if is_valid_geo_for_ui:
try:
# Convert radians to degrees for display/formatting
lat_d = math.degrees(geo["lat"])
lon_d = math.degrees(geo["lon"])
lat_deg_for_elevation = lat_d # Store for elevation request
lon_deg_for_elevation = lon_d # Store for elevation request
# ... (resto della formattazione per lat_s, lon_s, etc.) ...
orient_d = math.degrees(geo.get("orientation", 0.0))
# Format using DMS utility
lat_s = decimal_to_dms(lat_d, True)
lon_s = decimal_to_dms(lon_d, False)
orient_s = f"{orient_d:.2f}°" # Format orientation
# Calculate size in km if possible
orient_s = f"{orient_d:.2f}°"
scale_x = geo.get("scale_x", 0.0)
width_px = geo.get("width_px", 0)
scale_y = geo.get("scale_y", 0.0)
@ -1840,25 +2062,46 @@ class ControlPanelApp:
size_w_km = (scale_x * width_px) / 1000.0
size_h_km = (scale_y * height_px) / 1000.0
size_s = f"W: {size_w_km:.1f} km, H: {size_h_km:.1f} km"
except Exception as e:
logging.warning(f"[App UI Update] Error formatting SAR geo labels: {e}")
lat_s, lon_s, orient_s, size_s = "Error", "Error", "Error", "Error"
is_valid = False # Mark as invalid if formatting fails
is_valid_geo_for_ui = False
lat_deg_for_elevation = None # Invalidate
lon_deg_for_elevation = None # Invalidate
# Update UI widgets safely using control panel methods
# Update UI widgets
# ... (chiamate a cp.set_sar_center_coords, etc.) ...
try:
cp.set_sar_center_coords(lat_s, lon_s)
cp.set_sar_orientation(orient_s)
cp.set_sar_size_km(size_s)
if cp and cp.winfo_exists():
cp.set_sar_center_coords(lat_s, lon_s)
cp.set_sar_orientation(orient_s)
cp.set_sar_size_km(size_s)
except Exception as e:
# Catch errors if UI elements don't exist or Tcl errors
logging.exception(f"[App UI Update] Error setting SAR labels: {e}")
# --- >>> START OF NEW ELEVATION REQUEST FOR SAR CENTER <<< ---
if is_valid_geo_for_ui and lat_deg_for_elevation is not None and lon_deg_for_elevation is not None:
self.request_elevation_for_point(
source_description=config.ELEVATION_SOURCE_SAR_CENTER, # Define this in config.py
latitude=lat_deg_for_elevation,
longitude=lon_deg_for_elevation
)
else:
# Clear or indicate N/A for SAR center elevation if geo is invalid
self._update_elevation_display(config.ELEVATION_SOURCE_SAR_CENTER, "GeoInfo N/A")
# --- >>> END OF NEW ELEVATION REQUEST FOR SAR CENTER <<< ---
# Clear mouse coords if GeoInfo becomes invalid
if not is_valid:
if not is_valid_geo_for_ui:
try:
cp.set_mouse_coordinates("N/A", "N/A")
cp.set_map_mouse_coordinates("N/A", "N/A")
if cp and cp.winfo_exists():
cp.set_mouse_coordinates("N/A", "N/A")
cp.set_map_mouse_coordinates("N/A", "N/A")
# Clear mouse elevation displays too
self._update_elevation_display(config.ELEVATION_SOURCE_SAR_MOUSE, "GeoInfo N/A")
self._update_elevation_display(config.ELEVATION_SOURCE_MAP_MOUSE, "GeoInfo N/A")
except Exception:
pass # Ignore errors if UI closed
@ -2043,6 +2286,8 @@ class ControlPanelApp:
self._handle_map_click_update(payload)
elif command == "SAR_METADATA_UPDATE":
self._handle_sar_metadata_update(payload)
elif command == "ELEVATION_RESULT":
self._handle_elevation_result(payload)
else:
# Log if an unknown command is received
logging.warning(f"{log_prefix} Unknown command: {command}")
@ -2110,6 +2355,32 @@ class ControlPanelApp:
cp.set_map_mouse_coordinates("N/A", "N/A")
except Exception:
pass # Ignore errors if UI closed
def _handle_elevation_result(self, payload: Dict[str, Any]):
"""
Handles the ELEVATION_RESULT command from the Tkinter queue.
Updates the UI with the elevation information.
"""
_log_prefix_res = "[App Elev Res]"
if self.state.shutting_down:
return
try:
source_desc = payload.get("source_description", "UnknownPoint")
# --- USE THE PRE-FORMATTED DISPLAY_TEXT FROM PAYLOAD ---
display_text_for_ui = payload.get("display_text", "Elev: Error")
error_details = payload.get("error")
# elevation_val = payload.get("elevation_value") # Numeric value, can be used for other logic if needed
logging.info(f"{_log_prefix_res} Received elevation result for '{source_desc}': {display_text_for_ui}")
if error_details:
logging.error(f"{_log_prefix_res} Error details for '{source_desc}': {error_details}")
self._update_elevation_display(source_desc, display_text_for_ui)
except Exception as e:
logging.exception(f"{_log_prefix_res} Error handling elevation result payload: {payload}")
self._update_elevation_display("Error", "Elev: UI Error")
def _handle_show_map_update(self, payload: Optional[ImageType]):
"""Handles the SHOW_MAP command by delegating display to MapIntegrationManager."""
@ -2144,36 +2415,101 @@ class ControlPanelApp:
)
def _handle_sar_click_update(self, payload: Optional[Tuple[int, int]]):
"""Updates the SAR click coordinates state and triggers a SAR redraw."""
"""Updates the SAR click coordinates state, triggers a SAR redraw, and requests elevation."""
log_prefix = "[App SAR Click State]"
if self.state.shutting_down:
return
# Validate payload (should be (x, y) tuple)
# ... (codice esistente per aggiornare self.state.last_sar_click_coords e self._trigger_sar_update()) ...
if payload and isinstance(payload, tuple) and len(payload) == 2:
# Store the pixel coordinates in AppState
self.state.last_sar_click_coords = payload
logging.debug(
f"{log_prefix} Updated state.last_sar_click_coords to {payload}"
)
# Trigger SAR redraw pipeline to include the marker
self._trigger_sar_update()
logging.debug(f"{log_prefix} Updated state.last_sar_click_coords to {payload}")
self._trigger_sar_update() # Redraw SAR image with marker
# --- >>> START OF NEW ELEVATION REQUEST <<< ---
# Get geographic coordinates for the clicked SAR pixel
# This reuses the logic that updates the "SAR Mouse" display
# It's a bit indirect but avoids duplicating the geo calculation here.
# Assumes process_mouse_queue will eventually update the UI stringvar
# and then we parse it back. A more direct way would be to have
# get_geo_coords_from_sar_pixel return the lat/lon directly.
# For now, let's assume we need to convert payload (pixel) to geo
geo_info = self.state.current_sar_geo_info
disp_w = self.state.sar_display_width
disp_h = self.state.sar_display_height
if (geo_info and geo_info.get("valid") and disp_w > 0 and disp_h > 0 and
geo_info.get("width_px", 0) > 0 and geo_info.get("height_px", 0) > 0):
try:
x_disp, y_disp = payload
orig_w, orig_h = geo_info["width_px"], geo_info["height_px"]
scale_x, scale_y = geo_info["scale_x"], geo_info["scale_y"]
ref_x, ref_y = geo_info["ref_x"], geo_info["ref_y"]
ref_lat_rad, ref_lon_rad = geo_info["lat"], geo_info["lon"]
# angle_rad = geo_info.get("orientation", 0.0) # Simplified calc doesn't use angle here
orig_x_f = (x_disp / disp_w) * orig_w
orig_y_f = (y_disp / disp_h) * orig_h
pixel_delta_x_f = orig_x_f - ref_x
pixel_delta_y_f = ref_y - orig_y_f
meters_delta_x = pixel_delta_x_f * scale_x
meters_delta_y = pixel_delta_y_f * scale_y
M_PER_DLAT = 111132.954
M_PER_DLON_EQ = 111319.488
m_per_dlon_val = max(abs(M_PER_DLON_EQ * math.cos(ref_lat_rad)), 1e-3)
lat_offset_deg_val = meters_delta_y / M_PER_DLAT
lon_offset_deg_val = meters_delta_x / m_per_dlon_val
final_lat_deg_val = math.degrees(ref_lat_rad) + lat_offset_deg_val
final_lon_deg_val = math.degrees(ref_lon_rad) + lon_offset_deg_val
if math.isfinite(final_lat_deg_val) and abs(final_lat_deg_val) <= 90.0 and \
math.isfinite(final_lon_deg_val) and abs(final_lon_deg_val) <= 180.0:
self.request_elevation_for_point(
source_description=config.ELEVATION_SOURCE_SAR_MOUSE, # Define this in config.py
latitude=final_lat_deg_val,
longitude=final_lon_deg_val
)
else:
logging.warning(f"{log_prefix} Calculated invalid geo-coords for SAR click, skipping elevation.")
self._update_elevation_display(config.ELEVATION_SOURCE_SAR_MOUSE, "Invalid Geo")
except Exception as e_geo_calc:
logging.exception(f"{log_prefix} Error calculating geo-coords for SAR click elevation request: {e_geo_calc}")
self._update_elevation_display(config.ELEVATION_SOURCE_SAR_MOUSE, "Geo Calc Err")
else:
logging.warning(f"{log_prefix} Insufficient data for SAR click geo-calculation, skipping elevation.")
self._update_elevation_display(config.ELEVATION_SOURCE_SAR_MOUSE, "Geo Data N/A")
# --- >>> END OF NEW ELEVATION REQUEST <<< ---
else:
logging.warning(f"{log_prefix} Received invalid payload: {payload}")
def _handle_map_click_update(self, payload: Optional[Tuple[int, int]]):
"""Updates the Map click coordinates state and triggers a Map redraw."""
"""Updates the Map click coordinates state, triggers a Map redraw, and requests elevation."""
log_prefix = "[App Map Click State]"
if self.state.shutting_down:
return
# Validate payload
# ... (codice esistente per aggiornare self.state.last_map_click_coords e self.trigger_map_redraw()) ...
if payload and isinstance(payload, tuple) and len(payload) == 2:
# Store the pixel coordinates in AppState
self.state.last_map_click_coords = payload
logging.debug(
f"{log_prefix} Updated state.last_map_click_coords to {payload}"
)
# Trigger map redraw (recomposition should pick up the new marker state)
self.trigger_map_redraw(full_update=False)
logging.debug(f"{log_prefix} Updated state.last_map_click_coords to {payload}")
self.trigger_map_redraw(full_update=False) # Redraw map with marker
# --- >>> START OF NEW ELEVATION REQUEST <<< ---
mgr = getattr(self, "map_integration_manager", None)
if mgr:
pixel_x, pixel_y = payload
geo_coords = mgr.get_geo_coords_from_map_pixel(pixel_x, pixel_y)
if geo_coords:
lat_deg, lon_deg = geo_coords
self.request_elevation_for_point(
source_description=config.ELEVATION_SOURCE_MAP_MOUSE, # Define this in config.py
latitude=lat_deg,
longitude=lon_deg
)
else:
logging.warning(f"{log_prefix} Could not get geo-coords for map click, skipping elevation.")
self._update_elevation_display(config.ELEVATION_SOURCE_MAP_MOUSE, "Geo Conv Err")
else:
logging.warning(f"{log_prefix} MapIntegrationManager not available for map click elevation request.")
self._update_elevation_display(config.ELEVATION_SOURCE_MAP_MOUSE, "Map Mgr N/A")
# --- >>> END OF NEW ELEVATION REQUEST <<< ---
else:
logging.warning(f"{log_prefix} Received invalid payload: {payload}")

View File

@ -205,4 +205,18 @@ KML_OUTPUT_DIRECTORY = "kml_output"
AUTO_LAUNCH_GOOGLE_EARTH = False
MAX_KML_FILES = 30 # Max KMLs to keep (0 or less disables cleanup)
# Path to the root directory of the GeoElevation project
# (i.e. the folder that CONTAINS the subfolder 'geoelevation')
# Leave blank or None if GeoElevation should not be used.
# Examples:
# GEOELEVATION_PROJECT_ROOT_PATH = "/Users/yourname/development/GeoElevation" (absolute)
# GEOELEVATION_PROJECT_ROOT_PATH = "../GeoElevation" (relative to the MapViewer root)
GEOELEVATION_PROJECT_ROOT_PATH = r"C:\src\____GitProjects\GeoElevation" # Or the correct path
# String constants for identifying the source of an elevation request.
# These are used internally in app_main.py to manage UI updates.
ELEVATION_SOURCE_SAR_CENTER = "SAR_Center"
ELEVATION_SOURCE_SAR_MOUSE = "SAR_Mouse"
ELEVATION_SOURCE_MAP_MOUSE = "Map_Mouse"
# --- END OF FILE config.py ---

View File

@ -62,15 +62,17 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
)
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)
self.sar_center_elevation_var = tk.StringVar(value="Elev: N/A")
self.sar_mouse_elevation_var = tk.StringVar(value="Elev: N/A")
self.map_mouse_elevation_var = tk.StringVar(value="Elev: N/A")
# --- References to UI widgets ---
self.mfd_color_labels: Dict[str, tk.Label] = {}
# References to metadata widgets are REMOVED from this class
# --- Initialize UI structure ---
self.init_ui() # Call the UI building method
self.init_ui()
logging.debug(f"{log_prefix} ControlPanel frame initialization complete.")
@ -79,510 +81,311 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
"""Initializes and arranges the user interface widgets within this frame."""
log_prefix = "[UI Setup]"
logging.debug(f"{log_prefix} Starting init_ui widget creation...")
# This frame (self) is placed by its parent (container_frame in App)
# DO NOT pack or grid self here.
# --- 1. SAR Parameters Frame ---
# ... (Questa sezione rimane invariata) ...
self.sar_params_frame = ttk.Labelframe(self, text="SAR Parameters", padding=5)
self.sar_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(5, 2))
sar_row = 0 # Row counter for SAR grid
# Test Image Checkbox
sar_row = 0
self.test_image_var = tk.IntVar(value=1 if config.ENABLE_TEST_MODE else 0)
self.test_image_check = ttk.Checkbutton(
self.sar_params_frame,
text="Test Image",
variable=self.test_image_var,
self.sar_params_frame, text="Test Image", variable=self.test_image_var,
command=self.app.update_image_mode,
)
self.test_image_check.grid(
row=sar_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2
)
# Record SAR Checkbox
self.test_image_check.grid(row=sar_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2)
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",
variable=self.record_sar_var,
self.sar_params_frame, text="Record SAR", variable=self.record_sar_var,
command=self.app.toggle_sar_recording,
)
self.record_sar_check.grid(
row=sar_row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=2
)
self.record_sar_check.grid(row=sar_row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=2)
sar_row += 1
# SAR Size Combobox
self.sar_size_label = ttk.Label(self.sar_params_frame, text="Size:")
self.sar_size_label.grid(
row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1
)
self.sar_size_label.grid(row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1)
self.sar_size_combo = ttk.Combobox(
self.sar_params_frame,
values=config.SAR_SIZE_FACTORS,
state="readonly",
width=6,
self.sar_params_frame, values=config.SAR_SIZE_FACTORS, state="readonly", width=6,
)
try: # Set initial value based on current state
try:
factor = 1
if self.app.state.sar_display_width > 0:
factor = max(1, config.SAR_WIDTH // self.app.state.sar_display_width)
sz_str = f"1:{factor}"
if sz_str in config.SAR_SIZE_FACTORS:
self.sar_size_combo.set(sz_str)
else:
self.sar_size_combo.set(config.DEFAULT_SAR_SIZE)
except Exception: # Fallback to default if error
self.sar_size_combo.set(config.DEFAULT_SAR_SIZE)
self.sar_size_combo.grid(
row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1
)
if sz_str in config.SAR_SIZE_FACTORS: self.sar_size_combo.set(sz_str)
else: self.sar_size_combo.set(config.DEFAULT_SAR_SIZE)
except Exception: self.sar_size_combo.set(config.DEFAULT_SAR_SIZE)
self.sar_size_combo.grid(row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1)
self.sar_size_combo.bind("<<ComboboxSelected>>", self.app.update_sar_size)
# 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_combo = ttk.Combobox(
self.sar_params_frame,
values=config.COLOR_PALETTES,
state="readonly",
width=8,
)
self.palette_combo.set(self.app.state.sar_palette) # Set from state
self.palette_combo.grid(
row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1
self.sar_params_frame, values=config.COLOR_PALETTES, state="readonly", width=8,
)
self.palette_combo.set(self.app.state.sar_palette)
self.palette_combo.grid(row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1)
self.palette_combo.bind("<<ComboboxSelected>>", self.app.update_sar_palette)
sar_row += 1
# SAR Contrast Slider
self.contrast_label = ttk.Label(self.sar_params_frame, text="Contrast:")
self.contrast_label.grid(
row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1
)
self.contrast_label.grid(row=sar_row, column=0, sticky=tk.W, padx=(5, 2), pady=1)
self.contrast_scale = ttk.Scale(
self.sar_params_frame,
orient=tk.HORIZONTAL,
from_=0.1,
to=3.0,
value=self.app.state.sar_contrast, # Set from state
command=self.app.update_contrast,
self.sar_params_frame, orient=tk.HORIZONTAL, from_=0.1, to=3.0,
value=self.app.state.sar_contrast, command=self.app.update_contrast,
)
self.contrast_scale.grid(
row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1
)
# SAR Brightness Slider
self.contrast_scale.grid(row=sar_row, column=1, sticky=tk.EW, padx=(0, 10), pady=1)
self.brightness_label = ttk.Label(self.sar_params_frame, text="Brightness:")
self.brightness_label.grid(
row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1
)
self.brightness_label.grid(row=sar_row, column=2, sticky=tk.W, padx=(0, 2), pady=1)
self.brightness_scale = ttk.Scale(
self.sar_params_frame,
orient=tk.HORIZONTAL,
from_=-100,
to=100,
value=self.app.state.sar_brightness, # Set from state
command=self.app.update_brightness,
self.sar_params_frame, orient=tk.HORIZONTAL, from_=-100, to=100,
value=self.app.state.sar_brightness, command=self.app.update_brightness,
)
self.brightness_scale.grid(
row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1
)
# SAR Metadata Checkbox (Still created here as it belongs logically with SAR params)
self.brightness_scale.grid(row=sar_row, column=3, sticky=tk.EW, padx=(0, 5), pady=1)
sar_row += 1
# 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
self.sar_params_frame, text="Show SAR Metadata", variable=self.show_meta_var,
command=self.app.toggle_sar_metadata_display,
)
self.show_meta_check.grid(
row=sar_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=(5, 2)
)
# Configure SAR frame column weights
self.sar_params_frame.columnconfigure(1, weight=1) # Allow sliders to expand
self.show_meta_check.grid(row=sar_row, column=0, columnspan=4, sticky=tk.W, padx=5, pady=(5, 2))
self.sar_params_frame.columnconfigure(1, weight=1)
self.sar_params_frame.columnconfigure(3, weight=1)
# --- 2. MFD Parameters Frame ---
# ... (Questa sezione rimane invariata) ...
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 = [
"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
col_offset = 0 if (index % 2 == 0) else 4
# Category Label
cat_label = ttk.Label(self.mfd_params_frame, text=f"{name}:")
cat_label.grid(
row=row, column=0 + col_offset, sticky=tk.W, padx=(5, 1), pady=1
)
# Intensity Slider Variable and Widget
cat_label.grid(row=row, column=0 + col_offset, sticky=tk.W, padx=(5,1), pady=1)
intensity_var = tk.IntVar(value=config.DEFAULT_MFD_INTENSITY)
try:
intensity_var.set(
self.app.state.mfd_params["categories"][name]["intensity"]
)
except Exception:
pass
try: intensity_var.set(self.app.state.mfd_params["categories"][name]["intensity"])
except Exception: pass
intensity_scale = ttk.Scale(
self.mfd_params_frame,
orient=tk.HORIZONTAL,
length=100,
from_=0,
to=255,
variable=intensity_var,
command=lambda v, n=name, var=intensity_var: (
self.app.update_mfd_category_intensity(n, var.get())
),
self.mfd_params_frame, orient=tk.HORIZONTAL, length=100, from_=0, to=255, variable=intensity_var,
command=lambda v, n=name, var=intensity_var: self.app.update_mfd_category_intensity(n, var.get()),
)
intensity_scale.grid(
row=row, column=1 + col_offset, sticky=tk.EW, padx=1, pady=1
)
# Color Chooser Button
intensity_scale.grid(row=row, column=1 + col_offset, sticky=tk.EW, padx=1, pady=1)
color_button = ttk.Button(
self.mfd_params_frame,
text="Color",
width=5,
command=lambda n=name: self.app.choose_mfd_category_color(n),
)
color_button.grid(
row=row, column=2 + col_offset, sticky=tk.W, padx=1, pady=1
)
# Color Preview Label
color_label = tk.Label(
self.mfd_params_frame, text="", width=3, relief=tk.SUNKEN, borderwidth=1
self.mfd_params_frame, text="Color", width=5, command=lambda n=name: self.app.choose_mfd_category_color(n),
)
color_button.grid(row=row, column=2 + col_offset, sticky=tk.W, padx=1, pady=1)
color_label = tk.Label(self.mfd_params_frame, text="", width=3, relief=tk.SUNKEN, borderwidth=1)
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")
color_label.grid(
row=row, column=3 + col_offset, sticky=tk.W, padx=(1, 5), pady=1
)
except Exception: 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
# Raw Map Intensity Slider
last_cat_row = (num_categories - 1) // 2
last_cat_row = (num_categories -1) // 2
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,
)
raw_map_label.grid(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:
raw_map_intensity_var.set(self.app.state.mfd_params["raw_map_intensity"])
except Exception:
pass
self.mfd_raw_map_intensity_var = raw_map_intensity_var # Keep reference
try: raw_map_intensity_var.set(self.app.state.mfd_params["raw_map_intensity"])
except Exception: pass
self.mfd_raw_map_intensity_var = raw_map_intensity_var
raw_map_scale = ttk.Scale(
self.mfd_params_frame,
orient=tk.HORIZONTAL,
length=100,
from_=0,
to=255,
variable=raw_map_intensity_var,
command=lambda v: self.app.update_mfd_raw_map_intensity(
raw_map_intensity_var.get()
),
self.mfd_params_frame, orient=tk.HORIZONTAL, length=100, from_=0, to=255, variable=raw_map_intensity_var,
command=lambda v: self.app.update_mfd_raw_map_intensity(raw_map_intensity_var.get()),
)
raw_map_scale.grid(
row=raw_map_row,
column=1 + raw_map_col_offset,
columnspan=3,
sticky=tk.EW,
padx=(1, 5),
pady=1,
)
# Configure MFD frame column weights
raw_map_scale.grid(row=raw_map_row, column=1 + raw_map_col_offset, columnspan=3, sticky=tk.EW, padx=(1,5), pady=1)
self.mfd_params_frame.columnconfigure(1, weight=1)
self.mfd_params_frame.columnconfigure(5, weight=1)
# --- 3. Map Parameters Frame ---
# ... (Questa sezione rimane invariata) ...
self.map_params_frame = ttk.Labelframe(self, text="Map Parameters", padding=5)
self.map_params_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2)
map_row = 0 # Row counter for Map grid
# Map Size Combobox
map_row = 0
self.map_size_label = ttk.Label(self.map_params_frame, text="Map Display Size:")
self.map_size_label.grid(
row=map_row, column=0, sticky=tk.W, padx=(5, 2), pady=1
)
self.map_size_label.grid(row=map_row, column=0, sticky=tk.W, padx=(5,2), pady=1)
self.map_size_combo = ttk.Combobox(
self.map_params_frame,
values=config.MAP_SIZE_FACTORS,
state="readonly",
width=6,
self.map_params_frame, values=config.MAP_SIZE_FACTORS, state="readonly", width=6,
)
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
)
self.map_size_combo.grid(row=map_row, column=1, sticky=tk.EW, padx=(2,10), pady=1)
self.map_size_combo.bind("<<ComboboxSelected>>", self.app.update_map_size)
# Save Map Button
self.save_map_button = ttk.Button(
self.map_params_frame,
text="Save Map View",
command=self.app.save_current_map_view,
self.map_params_frame, text="Save 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
)
self.save_map_button.grid(row=map_row, column=2, columnspan=4, sticky=tk.E, padx=5, pady=1)
map_row += 1
# SAR Overlay Checkbox
self.sar_overlay_var = tk.BooleanVar(
value=self.app.state.map_sar_overlay_enabled
)
self.sar_overlay_var = tk.BooleanVar(value=self.app.state.map_sar_overlay_enabled)
self.sar_overlay_check = ttk.Checkbutton(
self.map_params_frame,
text="Show SAR Overlay on Map",
variable=self.sar_overlay_var,
self.map_params_frame, text="Show SAR Overlay on Map", variable=self.sar_overlay_var,
command=self.app.toggle_sar_overlay,
)
self.sar_overlay_check.grid(
row=map_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2
)
self.sar_overlay_check.grid(row=map_row, column=0, columnspan=2, sticky=tk.W, padx=5, pady=2)
map_row += 1
# 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.sar_overlay_alpha_var = tk.DoubleVar(
value=self.app.state.map_sar_overlay_alpha
)
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)
self.alpha_scale = ttk.Scale(
self.map_params_frame,
orient=tk.HORIZONTAL,
from_=0.0,
to=1.0,
variable=self.sar_overlay_alpha_var,
self.map_params_frame, orient=tk.HORIZONTAL, from_=0.0, to=1.0, variable=self.sar_overlay_alpha_var,
)
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
)
self.alpha_scale.grid(row=map_row, column=1, columnspan=5, sticky=tk.EW, padx=(0,5), pady=1)
map_row += 1
# 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)
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.lat_shift_entry.grid(
row=map_row, column=2, sticky=tk.W, padx=(0, 5), pady=1
)
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)
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.lon_shift_entry.grid(
row=map_row, column=4, sticky=tk.W, padx=(0, 5), pady=1
)
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,
self.map_params_frame, text="Apply 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.apply_shift_button.grid(row=map_row, column=5, sticky=tk.E, padx=(5,5), pady=1)
self.map_params_frame.columnconfigure(2, weight=1)
self.map_params_frame.columnconfigure(4, weight=1)
# --- 4. Info Display Frame ---
# --- 4. Info Display Frame (MODIFIED) ---
self.info_display_frame = ttk.Labelframe(self, text="Info Display", padding=5)
self.info_display_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2)
info_row = 0 # Row counter for Info grid
button_width = 3 # Standard width for Go/GE buttons
info_row = 0
button_width = 3
entry_width_coords = 30 # Adjusted width for coords + elevation
entry_width_orient = 10
entry_width_size = 18
entry_width_stats_drop = 28
entry_width_stats_incmpl = 15
entry_width_elevation = 15 # Width for elevation labels
# --- Row 0: SAR Center Coords ---
# --- Row 0: SAR Center Coords & Elevation ---
ref_label = ttk.Label(self.info_display_frame, text="SAR Center:")
ref_label.grid(row=info_row, column=0, sticky=tk.W, padx=(5, 2), pady=1)
self.sar_center_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.sar_center_coords_var,
state="readonly",
width=35,
self.info_display_frame, textvariable=self.sar_center_coords_var,
state="readonly", width=entry_width_coords
)
self.sar_center_entry.grid(
row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1
self.sar_center_entry.grid(row=info_row, column=1, columnspan=2, sticky=tk.EW, padx=(0, 2), pady=1)
# --- >>> NEW: SAR Center Elevation Label <<< ---
self.sar_center_elev_label = ttk.Label(
self.info_display_frame, textvariable=self.sar_center_elevation_var,
width=entry_width_elevation, anchor=tk.W, relief=tk.SUNKEN, borderwidth=1
)
self.sar_center_elev_label.grid(row=info_row, column=3, sticky=tk.EW, padx=(2,2), pady=1)
# --- >>> END NEW <<< ---
self.ref_gmaps_button = ttk.Button(
self.info_display_frame,
text="Go",
width=button_width,
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
self.info_display_frame, text="Go", width=button_width,
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)
self.ref_gearth_button = ttk.Button(
self.info_display_frame,
text="GE",
width=button_width,
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
self.info_display_frame, text="GE", width=button_width,
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)
info_row += 1
# --- Row 1: Image Orient & Image Size ---
orient_label = ttk.Label(self.info_display_frame, text="Image Orient:")
orient_label.grid(row=info_row, column=0, sticky=tk.W, padx=5, pady=1)
self.sar_orientation_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.sar_orientation_var,
state="readonly",
width=15,
)
self.sar_orientation_entry.grid(
row=info_row, column=1, sticky=tk.EW, padx=(0, 5), pady=1
self.info_display_frame, textvariable=self.sar_orientation_var,
state="readonly", width=entry_width_orient
)
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=(5, 2), pady=1) # Adjusted column
self.sar_size_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.sar_size_km_var,
state="readonly",
width=25,
)
self.sar_size_entry.grid(
row=info_row, column=3, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1
self.info_display_frame, textvariable=self.sar_size_km_var,
state="readonly", width=entry_width_size
)
# Span across remaining columns for Go/GE alignment
self.sar_size_entry.grid(row=info_row, column=3, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1)
info_row += 1
# --- Row 2: SAR Mouse Coords ---
# --- Row 2: SAR Mouse Coords & Elevation ---
sar_mouse_label = ttk.Label(self.info_display_frame, text="SAR Mouse:")
sar_mouse_label.grid(row=info_row, column=0, sticky=tk.W, padx=5, pady=1)
self.mouse_latlon_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.mouse_coords_var,
state="readonly",
width=35,
self.info_display_frame, textvariable=self.mouse_coords_var,
state="readonly", width=entry_width_coords
)
self.mouse_latlon_entry.grid(
row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1
self.mouse_latlon_entry.grid(row=info_row, column=1, columnspan=2, sticky=tk.EW, padx=(0, 2), pady=1)
# --- >>> NEW: SAR Mouse Elevation Label <<< ---
self.sar_mouse_elev_label = ttk.Label(
self.info_display_frame, textvariable=self.sar_mouse_elevation_var,
width=entry_width_elevation, anchor=tk.W, relief=tk.SUNKEN, borderwidth=1
)
self.sar_mouse_elev_label.grid(row=info_row, column=3, sticky=tk.EW, padx=(2,2), pady=1)
# --- >>> END NEW <<< ---
self.sar_mouse_gmaps_button = ttk.Button(
self.info_display_frame,
text="Go",
width=button_width,
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
self.info_display_frame, text="Go", width=button_width,
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)
self.sar_mouse_gearth_button = ttk.Button(
self.info_display_frame,
text="GE",
width=button_width,
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
self.info_display_frame, text="GE", width=button_width,
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)
info_row += 1
# --- Row 3: Map Mouse Coords ---
# --- Row 3: Map Mouse Coords & Elevation ---
map_mouse_label = ttk.Label(self.info_display_frame, text="Map Mouse:")
map_mouse_label.grid(row=info_row, column=0, sticky=tk.W, padx=5, pady=1)
self.map_mouse_latlon_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.map_mouse_coords_var,
state="readonly",
width=35,
self.info_display_frame, textvariable=self.map_mouse_coords_var,
state="readonly", width=entry_width_coords
)
self.map_mouse_latlon_entry.grid(
row=info_row, column=1, columnspan=3, sticky=tk.EW, padx=(0, 2), pady=1
self.map_mouse_latlon_entry.grid(row=info_row, column=1, columnspan=2, sticky=tk.EW, padx=(0, 2), pady=1)
# --- >>> NEW: Map Mouse Elevation Label <<< ---
self.map_mouse_elev_label = ttk.Label(
self.info_display_frame, textvariable=self.map_mouse_elevation_var,
width=entry_width_elevation, anchor=tk.W, relief=tk.SUNKEN, borderwidth=1
)
self.map_mouse_elev_label.grid(row=info_row, column=3, sticky=tk.EW, padx=(2,2), pady=1)
# --- >>> END NEW <<< ---
self.map_mouse_gmaps_button = ttk.Button(
self.info_display_frame,
text="Go",
width=button_width,
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
self.info_display_frame, text="Go", width=button_width,
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)
self.map_mouse_gearth_button = ttk.Button(
self.info_display_frame,
text="GE",
width=button_width,
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
self.info_display_frame, text="GE", width=button_width,
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)
info_row += 1
# --- 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)
self.dropped_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.dropped_stats_var,
state="readonly",
width=30,
)
self.dropped_entry.grid(
row=info_row, column=1, sticky=tk.EW, padx=(0, 5), pady=1
self.info_display_frame, textvariable=self.dropped_stats_var,
state="readonly", width=entry_width_stats_drop
)
self.dropped_entry.grid(row=info_row, column=1, columnspan=2, sticky=tk.EW, padx=(0, 5), pady=1) # Span 2 cols
incomplete_label_text = ttk.Label(self.info_display_frame, text="Incomplete:")
incomplete_label_text.grid(
row=info_row, column=2, sticky=tk.W, padx=(10, 2), pady=1
)
incomplete_label_text.grid(row=info_row, column=3, sticky=tk.W, padx=(5, 2), pady=1) # Start at col 3
self.incomplete_entry = ttk.Entry(
self.info_display_frame,
textvariable=self.incomplete_stats_var,
state="readonly",
width=15,
)
self.incomplete_entry.grid(
row=info_row, column=3, columnspan=3, sticky=tk.EW, padx=(0, 5), pady=1
self.info_display_frame, textvariable=self.incomplete_stats_var,
state="readonly", width=entry_width_stats_incmpl
)
self.incomplete_entry.grid(row=info_row, column=4, columnspan=2, sticky=tk.EW, padx=(0, 5), pady=1) # Span 2 cols
info_row += 1
# --- Row 5: "GE All" Button ---
@ -594,85 +397,78 @@ class ControlPanel(ttk.Frame): # This is the main panel holding user controls
)
# Configure column weights for Info Display frame
self.info_display_frame.columnconfigure(1, weight=1) # Entry column
self.info_display_frame.columnconfigure(3, weight=1) # Entry column
self.info_display_frame.columnconfigure(4, weight=0) # Button Go
self.info_display_frame.columnconfigure(5, weight=0) # Button GE
logging.debug(f"{log_prefix} Info Display frame created.")
self.info_display_frame.columnconfigure(1, weight=1) # Coords entry
self.info_display_frame.columnconfigure(2, weight=1) # Coords entry (continued for span) / Size Label
self.info_display_frame.columnconfigure(3, weight=0) # Elevation Label / Size Entry / Incomplete Label
self.info_display_frame.columnconfigure(4, weight=0) # Go Button / Size Entry / Incomplete Entry
self.info_display_frame.columnconfigure(5, weight=0) # GE Button / Size Entry / Incomplete Entry
# --- 5. Metadata Display Frame (Creation REMOVED from here) ---
# This is now created and managed in ControlPanelApp
logging.debug(f"{log_prefix} Info Display frame created with elevation labels.")
# --- End of init_ui ---
logging.debug(f"{log_prefix} init_ui widget creation complete.")
# --- UI Update Methods ---
# ... (metodi set_sar_center_coords, set_mouse_coordinates, ecc. rimangono invariati) ...
def set_sar_center_coords(self, latitude_str: str, longitude_str: str):
"""Updates the SAR Center coordinates display."""
text = f"Lat={latitude_str}, Lon={longitude_str}"
try:
self.sar_center_coords_var.set(text)
except Exception as e:
logging.warning(f"[UI Update] Error setting SAR center coords: {e}")
try: self.sar_center_coords_var.set(text)
except Exception as e: logging.warning(f"[UI Update] Error setting SAR center coords: {e}")
def set_mouse_coordinates(self, latitude_str: str, longitude_str: str):
"""Updates the SAR Mouse coordinates display."""
text = f"Lat={latitude_str}, Lon={longitude_str}"
try:
self.mouse_coords_var.set(text)
except Exception as e:
logging.warning(f"[UI Update] Error setting SAR mouse coords: {e}")
try: self.mouse_coords_var.set(text)
except Exception as e: logging.warning(f"[UI Update] Error setting SAR mouse coords: {e}")
def set_map_mouse_coordinates(self, latitude_str: str, longitude_str: str):
"""Updates the Map Mouse coordinates display."""
text = f"Lat={latitude_str}, Lon={longitude_str}"
try:
self.map_mouse_coords_var.set(text)
except Exception as e:
logging.warning(f"[UI Update] Error setting Map mouse coords: {e}")
try: self.map_mouse_coords_var.set(text)
except Exception as e: logging.warning(f"[UI Update] Error setting Map mouse coords: {e}")
def set_sar_orientation(self, orientation_deg_str: str):
"""Updates the SAR Orientation display."""
try:
self.sar_orientation_var.set(orientation_deg_str)
except Exception as e:
logging.warning(f"[UI Update] Error setting SAR orientation: {e}")
try: self.sar_orientation_var.set(orientation_deg_str)
except Exception as e: logging.warning(f"[UI Update] Error setting SAR orientation: {e}")
def set_sar_size_km(self, size_text: str):
"""Updates the SAR Size display."""
try:
self.sar_size_km_var.set(size_text)
except Exception as e:
logging.warning(f"[UI Update] Error setting SAR size: {e}")
try: self.sar_size_km_var.set(size_text)
except Exception as e: logging.warning(f"[UI Update] Error setting SAR size: {e}")
def set_statistics_display(self, dropped_text: str, incomplete_text: str):
"""Updates the statistics display fields."""
try:
self.dropped_stats_var.set(dropped_text)
self.incomplete_stats_var.set(incomplete_text)
except Exception as e:
logging.warning(f"[UI Update] Error setting stats display: {e}")
except Exception as e: logging.warning(f"[UI Update] Error setting stats display: {e}")
def update_mfd_color_display(
self, category_name: str, color_bgr_tuple: Tuple[int, int, int]
):
"""Updates the background color of an MFD category preview label."""
def update_mfd_color_display(self, category_name: str, color_bgr_tuple: Tuple[int, int, int]):
if category_name in self.mfd_color_labels:
lbl = self.mfd_color_labels[category_name]
# Format BGR tuple to hex color string #RRGGBB
hex_color = (
f"#{color_bgr_tuple[2]:02x}"
f"{color_bgr_tuple[1]:02x}"
f"{color_bgr_tuple[0]:02x}"
)
hex_color = f"#{color_bgr_tuple[2]:02x}{color_bgr_tuple[1]:02x}{color_bgr_tuple[0]:02x}"
try:
# Update label background if it still exists
if lbl.winfo_exists():
lbl.config(background=hex_color)
except Exception as e:
logging.exception(
f"[UI Update] Error updating MFD color for {category_name}: {e}"
)
if lbl.winfo_exists(): lbl.config(background=hex_color)
except Exception as e: logging.exception(f"[UI Update] Error updating MFD color for {category_name}: {e}")
def set_sar_center_elevation(self, elevation_text: str):
"""Updates the SAR Center elevation display label."""
try:
# The text is now pre-formatted by app_main.py
self.sar_center_elevation_var.set(elevation_text)
except Exception as e:
logging.warning(f"[UI Update] Error setting SAR center elevation: {e}")
def set_sar_mouse_elevation(self, elevation_text: str):
"""Updates the SAR Mouse elevation display label."""
try:
# The text is now pre-formatted by app_main.py
self.sar_mouse_elevation_var.set(elevation_text)
except Exception as e:
logging.warning(f"[UI Update] Error setting SAR mouse elevation: {e}")
def set_map_mouse_elevation(self, elevation_text: str):
"""Updates the Map Mouse elevation display label."""
try:
# The text is now pre-formatted by app_main.py
self.map_mouse_elevation_var.set(elevation_text)
except Exception as e:
logging.warning(f"[UI Update] Error setting Map mouse elevation: {e}")
# --- StatusBar Class ---