355 lines
16 KiB
Python
355 lines
16 KiB
Python
# FlightMonitor/gui/panels/map_info_panel.py
|
|
"""
|
|
Panel for displaying various map-related information:
|
|
clicked coordinates, map bounds, target bounding box, zoom level,
|
|
flight count, and map size.
|
|
"""
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from typing import Dict, Any, Optional, Tuple
|
|
|
|
from ...utils.logger import get_logger
|
|
from ...data import config as app_config
|
|
from ...map.map_utils import _is_valid_bbox_dict # Used for BBox validation internally
|
|
from ...utils.gui_utils import GUI_STATUS_UNKNOWN # Used for default color in status updates
|
|
|
|
|
|
module_logger = get_logger(__name__)
|
|
|
|
# Colors for BBox status display, matched with MainWindow's previous definitions
|
|
BBOX_COLOR_INSIDE = "green4"
|
|
BBOX_COLOR_OUTSIDE = "red2"
|
|
BBOX_COLOR_PARTIAL = "darkorange"
|
|
BBOX_COLOR_NA = "gray50"
|
|
|
|
|
|
class MapInfoPanel:
|
|
"""
|
|
Manages the GUI elements for displaying map-related information.
|
|
"""
|
|
|
|
def __init__(self, parent_frame: ttk.Frame):
|
|
"""
|
|
Initializes the MapInfoPanel.
|
|
|
|
Args:
|
|
parent_frame: The parent ttk.Frame where this panel will be placed.
|
|
"""
|
|
self.parent_frame = parent_frame
|
|
# Configura le colonne per un layout a griglia pulito, con le etichette dei valori più piccole.
|
|
self.parent_frame.columnconfigure(1, weight=0)
|
|
self.parent_frame.columnconfigure(3, weight=0)
|
|
|
|
# --- Clicked Map Coordinates ---
|
|
info_row = 0
|
|
ttk.Label(self.parent_frame, text="Click Lat:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_lat_value = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_lat_value.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(self.parent_frame, text="Lon:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_lon_value = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_lon_value.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Label(self.parent_frame, text="Lat DMS:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_lat_dms_value = ttk.Label(
|
|
self.parent_frame, text="N/A", width=18, wraplength=140
|
|
)
|
|
self.info_lat_dms_value.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(self.parent_frame, text="Lon DMS:").grid(
|
|
row=info_row, column=2, padx=(5, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.info_lon_dms_value = ttk.Label(
|
|
self.parent_frame, text="N/A", width=18, wraplength=140
|
|
)
|
|
self.info_lon_dms_value.grid(
|
|
row=info_row, column=3, padx=(0, 0), pady=2, sticky=tk.EW
|
|
)
|
|
info_row += 1
|
|
ttk.Separator(self.parent_frame, orient=tk.HORIZONTAL).grid(
|
|
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2
|
|
)
|
|
|
|
# --- Current Map Geographic Bounds ---
|
|
info_row += 1
|
|
ttk.Label(self.parent_frame, text="Map N:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_map_bounds_n = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_map_bounds_n.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(self.parent_frame, text="W:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_map_bounds_w = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_map_bounds_w.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Label(self.parent_frame, text="Map S:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_map_bounds_s = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_map_bounds_s.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(self.parent_frame, text="E:").grid(
|
|
row=info_row, column=2, sticky=tk.W, pady=1, padx=(5, 2)
|
|
)
|
|
self.info_map_bounds_e = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_map_bounds_e.grid(
|
|
row=info_row, column=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
info_row += 1
|
|
ttk.Separator(self.parent_frame, orient=tk.HORIZONTAL).grid(
|
|
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2
|
|
)
|
|
|
|
# --- Target Bounding Box Input (Monitoring Area) ---
|
|
info_row += 1
|
|
ttk.Label(self.parent_frame, text="Target N:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_target_bbox_n = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_target_bbox_n.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(self.parent_frame, text="W:").grid(
|
|
row=info_row, column=2, padx=(5, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.info_target_bbox_w = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_target_bbox_w.grid(
|
|
row=info_row, column=3, padx=(0, 5), pady=1, sticky=tk.W
|
|
)
|
|
info_row += 1
|
|
ttk.Label(self.parent_frame, text="Target S:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_target_bbox_s = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_target_bbox_s.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(self.parent_frame, text="E:").grid(
|
|
row=info_row, column=2, padx=(5, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.info_target_bbox_e = ttk.Label(self.parent_frame, text="N/A", width=12)
|
|
self.info_target_bbox_e.grid(
|
|
row=info_row, column=3, padx=(0, 5), pady=1, sticky=tk.W
|
|
)
|
|
info_row += 1
|
|
ttk.Separator(self.parent_frame, orient=tk.HORIZONTAL).grid(
|
|
row=info_row, column=0, columnspan=4, sticky=tk.EW, pady=2
|
|
)
|
|
|
|
# --- General Map Metrics (Zoom, Flights, Size) ---
|
|
info_row += 1
|
|
ttk.Label(self.parent_frame, text="Zoom:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_zoom_value = ttk.Label(self.parent_frame, text="N/A", width=4)
|
|
self.info_zoom_value.grid(
|
|
row=info_row, column=1, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
ttk.Label(self.parent_frame, text="Flights:").grid(
|
|
row=info_row, column=2, padx=(5, 2), pady=2, sticky=tk.W
|
|
)
|
|
self.info_flight_count_value = ttk.Label(self.parent_frame, text="N/A", width=5)
|
|
self.info_flight_count_value.grid(
|
|
row=info_row, column=3, padx=(0, 5), pady=1, sticky=tk.W
|
|
)
|
|
info_row += 1
|
|
ttk.Label(self.parent_frame, text="Map Size:").grid(
|
|
row=info_row, column=0, sticky=tk.W, pady=1, padx=(0, 2)
|
|
)
|
|
self.info_map_size_value = ttk.Label(self.parent_frame, text="N/A", wraplength=180)
|
|
self.info_map_size_value.grid(
|
|
row=info_row, column=1, columnspan=3, sticky=tk.W, pady=1, padx=(0, 5)
|
|
)
|
|
|
|
module_logger.debug("MapInfoPanel initialized.")
|
|
|
|
def update_clicked_map_info(
|
|
self,
|
|
lat_deg: Optional[float],
|
|
lon_deg: Optional[float],
|
|
lat_dms: str,
|
|
lon_dms: str,
|
|
):
|
|
"""
|
|
Updates the display for the geographic coordinates of a clicked point on the map.
|
|
|
|
Args:
|
|
lat_deg: Latitude in decimal degrees (Optional).
|
|
lon_deg: Longitude in decimal degrees (Optional).
|
|
lat_dms: Latitude formatted as Degrees-Minutes-Seconds string.
|
|
lon_dms: Longitude formatted as Degrees-Minutes-Seconds string.
|
|
"""
|
|
if not (hasattr(self, "info_lat_value") and self.info_lat_value.winfo_exists()):
|
|
return # Panel not fully initialized or destroyed
|
|
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
# Update decimal degree labels
|
|
self.info_lat_value.config(
|
|
text=f"{lat_deg:.{decimals}f}" if lat_deg is not None else "N/A"
|
|
)
|
|
self.info_lon_value.config(
|
|
text=f"{lon_deg:.{decimals}f}" if lon_deg is not None else "N/A"
|
|
)
|
|
# Update DMS labels (already formatted strings)
|
|
self.info_lat_dms_value.config(text=lat_dms or "N/A")
|
|
self.info_lon_dms_value.config(text=lon_dms or "N/A")
|
|
except tk.TclError:
|
|
# Widgets might be destroyed during application shutdown
|
|
module_logger.debug("TclError updating clicked map info, widgets likely destroyed.")
|
|
except Exception as e_update_click:
|
|
module_logger.error(f"Error updating clicked map info panel: {e_update_click}", exc_info=False)
|
|
|
|
|
|
def update_general_map_info_display(
|
|
self,
|
|
zoom: Optional[int],
|
|
map_size_str: str, # Already formatted (e.g., "100km x 80km")
|
|
map_geo_bounds: Optional[Tuple[float, float, float, float]], # (W, S, E, N)
|
|
target_bbox_input: Optional[Dict[str, float]], # Standard dict {lat_min, lon_min, lat_max, lon_max}
|
|
flight_count: Optional[int],
|
|
):
|
|
"""
|
|
Updates the display for general map information like current map bounds,
|
|
target monitoring area, zoom level, total flights shown, and map size.
|
|
|
|
Args:
|
|
zoom: Current map zoom level.
|
|
map_size_str: Formatted string representing the map's current geographic size (e.g., "500km x 300km").
|
|
map_geo_bounds: Tuple (west_lon, south_lat, east_lon, north_lat) representing the currently displayed map's bounds.
|
|
target_bbox_input: Dictionary with keys "lat_min", "lon_min", "lat_max", "lon_max"
|
|
representing the user-defined monitoring area.
|
|
flight_count: Number of flights currently displayed on the map.
|
|
"""
|
|
if not (hasattr(self, "info_zoom_value") and self.info_zoom_value.winfo_exists()):
|
|
return # Panel not fully initialized or destroyed
|
|
|
|
decimals = getattr(app_config, "COORDINATE_DECIMAL_PLACES", 5)
|
|
try:
|
|
# --- Map Geographic Bounds ---
|
|
map_w, map_s, map_e, map_n = ("N/A",) * 4
|
|
if map_geo_bounds:
|
|
map_w_val, map_s_val, map_e_val, map_n_val = map_geo_bounds
|
|
map_w, map_s, map_e, map_n = (
|
|
f"{map_w_val:.{decimals}f}",
|
|
f"{map_s_val:.{decimals}f}",
|
|
f"{map_e_val:.{decimals}f}",
|
|
f"{map_n_val:.{decimals}f}",
|
|
)
|
|
|
|
self.info_map_bounds_w.config(text=map_w)
|
|
self.info_map_bounds_s.config(text=map_s)
|
|
self.info_map_bounds_e.config(text=map_e)
|
|
self.info_map_bounds_n.config(text=map_n)
|
|
|
|
# --- Target BBox Input (Monitoring Area) ---
|
|
target_w, target_s, target_e, target_n = ("N/A",) * 4
|
|
color_for_target_bbox = BBOX_COLOR_NA # Default color
|
|
|
|
if target_bbox_input and _is_valid_bbox_dict(target_bbox_input):
|
|
# Ensure correct mapping from dictionary keys to geographic coordinates for display
|
|
target_n = f"{target_bbox_input['lat_max']:.{decimals}f}" # North: max_lat
|
|
target_w = f"{target_bbox_input['lon_min']:.{decimals}f}" # West: min_lon
|
|
target_s = f"{target_bbox_input['lat_min']:.{decimals}f}" # South: min_lat
|
|
target_e = f"{target_bbox_input['lon_max']:.{decimals}f}" # East: max_lon
|
|
|
|
# Determine color based on how target BBox relates to current map view
|
|
if map_geo_bounds:
|
|
relation_status = self._is_bbox_inside_bbox(
|
|
target_bbox_input, map_geo_bounds
|
|
)
|
|
if relation_status == "Inside":
|
|
color_for_target_bbox = BBOX_COLOR_INSIDE
|
|
elif relation_status == "Partial":
|
|
color_for_target_bbox = BBOX_COLOR_PARTIAL
|
|
else:
|
|
color_for_target_bbox = BBOX_COLOR_OUTSIDE
|
|
else: # No map view bounds, can't determine relation
|
|
color_for_target_bbox = BBOX_COLOR_NA
|
|
|
|
self.info_target_bbox_n.config(text=target_n, foreground=color_for_target_bbox)
|
|
self.info_target_bbox_w.config(text=target_w, foreground=color_for_target_bbox)
|
|
self.info_target_bbox_s.config(text=target_s, foreground=color_for_target_bbox)
|
|
self.info_target_bbox_e.config(text=target_e, foreground=color_for_target_bbox)
|
|
|
|
# --- Other General Info ---
|
|
self.info_zoom_value.config(text=str(zoom) if zoom is not None else "N/A")
|
|
self.info_map_size_value.config(text=map_size_str or "N/A")
|
|
self.info_flight_count_value.config(text=str(flight_count) if flight_count is not None else "N/A")
|
|
|
|
except tk.TclError:
|
|
module_logger.debug("TclError updating general map info, widgets likely destroyed.")
|
|
except Exception as e_update_gen_info:
|
|
module_logger.error(f"Error updating general map info panel: {e_update_gen_info}", exc_info=False)
|
|
|
|
def _is_bbox_inside_bbox(
|
|
self,
|
|
inner_bbox_dict: Dict[str, float], # Standard dict {lat_min, lon_min, ...}
|
|
outer_bbox_tuple: Tuple[float, float, float, float], # (W, S, E, N)
|
|
) -> str:
|
|
"""
|
|
Determines if an inner bounding box is fully inside, partially overlaps,
|
|
or is entirely outside of an outer bounding box.
|
|
|
|
Args:
|
|
inner_bbox_dict: The inner bounding box as a dictionary.
|
|
outer_bbox_tuple: The outer bounding box as a tuple (west, south, east, north).
|
|
|
|
Returns:
|
|
"Inside", "Outside", "Partial", or "N/A" if inputs are invalid.
|
|
"""
|
|
# Validate inputs using _is_valid_bbox_dict and basic tuple checks
|
|
if not _is_valid_bbox_dict(inner_bbox_dict) or not (
|
|
outer_bbox_tuple
|
|
and len(outer_bbox_tuple) == 4
|
|
and all(isinstance(c, (int, float)) for c in outer_bbox_tuple)
|
|
):
|
|
return "N/A"
|
|
|
|
# Convert outer tuple to a comparable dict for clarity and consistency
|
|
outer_dict = {
|
|
"lon_min": outer_bbox_tuple[0],
|
|
"lat_min": outer_bbox_tuple[1],
|
|
"lon_max": outer_bbox_tuple[2],
|
|
"lat_max": outer_bbox_tuple[3],
|
|
}
|
|
eps = 1e-6 # Epsilon for robust float comparisons
|
|
|
|
# Check for full containment
|
|
# If inner's min is >= outer's min, AND inner's max is <= outer's max for both lat/lon
|
|
if (
|
|
inner_bbox_dict["lon_min"] >= outer_dict["lon_min"] - eps
|
|
and inner_bbox_dict["lat_min"] >= outer_dict["lat_min"] - eps
|
|
and inner_bbox_dict["lon_max"] <= outer_dict["lon_max"] + eps
|
|
and inner_bbox_dict["lat_max"] <= outer_dict["lat_max"] + eps
|
|
):
|
|
return "Inside"
|
|
|
|
# Check for no overlap (completely outside)
|
|
# If inner is entirely to the right OR entirely to the left OR entirely above OR entirely below the outer
|
|
if (
|
|
inner_bbox_dict["lon_min"] >= outer_dict["lon_max"] - eps # Inner right of outer
|
|
or inner_bbox_dict["lon_max"] <= outer_dict["lon_min"] + eps # Inner left of outer
|
|
or inner_bbox_dict["lat_min"] >= outer_dict["lat_max"] - eps # Inner above outer
|
|
or inner_bbox_dict["lat_max"] <= outer_dict["lat_min"] + eps # Inner below outer
|
|
):
|
|
return "Outside"
|
|
|
|
# If it's not fully inside and not entirely outside, it must be partial
|
|
return "Partial" |