fix track on details gui

This commit is contained in:
VALLONGOL 2025-06-03 11:18:58 +02:00
parent aed7be91a4
commit 5fd3dffeef
3 changed files with 818 additions and 1749 deletions

View File

@ -1,16 +1,15 @@
# FlightMonitor/gui/dialogs/full_flight_details_window.py # FlightMonitor/gui/dialogs/full_flight_details_window.py
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
# from tkinter import messagebox # Rimosso per ora
import webbrowser import webbrowser
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional, Dict, Any, List # Rimosso Tuple from typing import Optional, Dict, Any, List
import time # Aggiunto per il test di live_data import time
from ...map.map_canvas_manager import MapCanvasManager from ...map.map_canvas_manager import MapCanvasManager
from ...map.map_utils import _is_valid_bbox_dict from ...map.map_utils import _is_valid_bbox_dict
from ...data.common_models import CanonicalFlightState from ...data.common_models import CanonicalFlightState
from collections import deque # Aggiunto per il type hinting e l'uso from collections import deque
try: try:
from ...utils.logger import get_logger from ...utils.logger import get_logger
@ -22,41 +21,23 @@ except ImportError:
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s: %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s: %(message)s")
logger.warning("FullFlightDetailsWindow using fallback standard Python logger.") logger.warning("FullFlightDetailsWindow using fallback standard Python logger.")
DEFAULT_SINGLE_POINT_PATCH_KM = 20.0
class FullFlightDetailsWindow(tk.Toplevel): class FullFlightDetailsWindow(tk.Toplevel):
def __init__(self, parent, icao24: str, controller: Optional[Any] = None): def __init__(self, parent, icao24: str, controller: Optional[Any] = None):
# --- INIZIO BLOCCO DI DEBUG --- super().__init__(parent)
print(f"DEBUG PRE-SUPER: FullFlightDetailsWindow __init__ for ICAO: {icao24}. Parent type: {type(parent)}")
try:
super().__init__(parent) # DEVE ESSERE LA PRIMA RIGA EFFETTIVA DOPO LE STAMPE DI DEBUG INIZIALI
print(f"DEBUG POST-SUPER: FullFlightDetailsWindow __init__ called for ICAO: {icao24}")
print(f"DEBUG POST-SUPER: Type of self: {type(self)}")
print(f"DEBUG POST-SUPER: Is self an instance of tk.Toplevel? {isinstance(self, tk.Toplevel)}")
print(f"DEBUG POST-SUPER: dir(self) contains 'tk': {'tk' in dir(self)}")
if hasattr(self, 'tk'): # MODIFIED: Normalize ICAO24 to lowercase for internal use as key
print(f"DEBUG POST-SUPER: self.tk IS PRESENT. Value: {self.tk}") self.icao24 = icao24.lower().strip()
print(f"DEBUG POST-SUPER: Type of self.tk: {type(self.tk)}") self.title(f"Full Details - {self.icao24.upper()}") # Display title can still be uppercase
else:
print(f"CRITICAL DEBUG POST-SUPER: self.tk IS MISSING!")
if hasattr(self, 'protocol'):
print(f"DEBUG POST-SUPER: self.protocol method IS available.")
else:
print(f"DEBUG POST-SUPER: self.protocol method IS NOT available (pre-title).")
except Exception as e_super_init:
print(f"CRITICAL DEBUG: Exception during or immediately after super().__init__(): {e_super_init}")
# Potrebbe essere rischioso continuare se super() fallisce, ma proviamo a vedere se gli attributi base ci sono
if not hasattr(self, 'tk'):
print(f"CRITICAL DEBUG: self.tk is still missing after super() exception.")
raise # Rilancia l'eccezione se super() fallisce catastroficamente
# --- FINE BLOCCO DI DEBUG ---
self.title(f"Full Details - {icao24.upper()}")
self.parent = parent self.parent = parent
self.controller = controller self.controller = controller
self.icao24 = icao24.upper()
self._initial_static_data: Optional[Dict[str, Any]] = None
self._initial_live_data: Optional[Dict[str, Any]] = None
self._initial_track_data: Optional[List[Dict[str, Any]]] = None
self.geometry("1000x750") self.geometry("1000x750")
self.minsize(800, 600) self.minsize(800, 600)
@ -70,7 +51,8 @@ class FullFlightDetailsWindow(tk.Toplevel):
top_info_frame.columnconfigure(0, weight=3) top_info_frame.columnconfigure(0, weight=3)
top_info_frame.columnconfigure(1, weight=2) top_info_frame.columnconfigure(1, weight=2)
static_details_container = ttk.LabelFrame(top_info_frame, text=f"Aircraft Information ({self.icao24})", padding=10) # Usa self.icao24.upper() per il testo del LabelFrame se vuoi visualizzarlo in maiuscolo
static_details_container = ttk.LabelFrame(top_info_frame, text=f"Aircraft Information ({self.icao24.upper()})", padding=10)
static_details_container.grid(row=0, column=0, sticky="nsew", padx=(0, 5), rowspan=2) static_details_container.grid(row=0, column=0, sticky="nsew", padx=(0, 5), rowspan=2)
self.detail_labels: Dict[str, ttk.Label] = {} self.detail_labels: Dict[str, ttk.Label] = {}
@ -83,16 +65,16 @@ class FullFlightDetailsWindow(tk.Toplevel):
image_link_container.columnconfigure(0, weight=1) image_link_container.columnconfigure(0, weight=1)
self.image_placeholder_label = ttk.Label(image_link_container, text="[ Aircraft Image Area ]", relief="groove", anchor="center", borderwidth=2, padding=10) self.image_placeholder_label = ttk.Label(image_link_container, text="[ Aircraft Image Area ]", relief="groove", anchor="center", borderwidth=2, padding=10)
self.image_placeholder_label.grid(row=0, column=0, sticky="nsew", pady=(0, 10)) self.image_placeholder_label.grid(row=0, column=0, sticky="nsew", pady=(0, 5))
self.jetphotos_link_label = ttk.Label(image_link_container, text="View on JetPhotos", foreground="blue", cursor="hand2", anchor="center") self.jetphotos_link_label = ttk.Label(image_link_container, text="View on JetPhotos", foreground="blue", cursor="hand2", anchor="center")
self.jetphotos_link_label.grid(row=1, column=0, sticky="ew", pady=(5, 0)) self.jetphotos_link_label.grid(row=1, column=0, sticky="ew", pady=(0,0))
self.jetphotos_link_label.bind("<Button-1>", self._open_jetphotos_link_action) self.jetphotos_link_label.bind("<Button-1>", self._open_jetphotos_link_action)
self.current_jetphotos_url: Optional[str] = None self.current_jetphotos_url: Optional[str] = None
self.jetphotos_link_label.grid_remove() self.jetphotos_link_label.grid_remove()
live_data_container = ttk.LabelFrame(top_info_frame, text="Current Flight Status", padding=10) live_data_container = ttk.LabelFrame(top_info_frame, text="Current Flight Status", padding=10)
live_data_container.grid(row=1, column=1, sticky="nsew", padx=(5, 0), pady=(5, 0)) live_data_container.grid(row=1, column=1, sticky="nsew", padx=(5,0), pady=(5,0))
self._create_live_details_layout(live_data_container) self._create_live_details_layout(live_data_container)
bottom_track_frame_container = ttk.Frame(main_v_pane, padding=5) bottom_track_frame_container = ttk.Frame(main_v_pane, padding=5)
@ -107,6 +89,7 @@ class FullFlightDetailsWindow(tk.Toplevel):
self.track_map_canvas = tk.Canvas(self.track_map_tab, bg="gray70", highlightthickness=0) self.track_map_canvas = tk.Canvas(self.track_map_tab, bg="gray70", highlightthickness=0)
self.track_map_canvas.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.track_map_canvas.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.detail_map_manager: Optional[MapCanvasManager] = None self.detail_map_manager: Optional[MapCanvasManager] = None
self._map_init_error_displayed = False
self.track_table_tab = ttk.Frame(self.track_notebook) self.track_table_tab = ttk.Frame(self.track_notebook)
self.track_notebook.add(self.track_table_tab, text="Track Data Table") self.track_notebook.add(self.track_table_tab, text="Track Data Table")
@ -147,64 +130,48 @@ class FullFlightDetailsWindow(tk.Toplevel):
self.focus_set() self.focus_set()
self.after(100, self._initialize_detail_map_manager) self.after(100, self._initialize_detail_map_manager)
self.protocol("WM_DELETE_WINDOW", self._on_closing_details_window)
# --- BLOCCO DI DEBUG PER PROTOCOL ---
try:
print(f"DEBUG PRE-PROTOCOL: Attempting to set protocol WM_DELETE_WINDOW...")
if hasattr(self, 'tk'):
print(f"DEBUG PRE-PROTOCOL: self.tk IS STILL PRESENT before protocol call. Value: {self.tk}")
else:
print(f"CRITICAL DEBUG PRE-PROTOCOL: self.tk IS MISSING before protocol call!")
if hasattr(self, 'protocol'):
print(f"DEBUG PRE-PROTOCOL: self.protocol method IS available before protocol call.")
self.protocol("WM_DELETE_WINDOW", self._on_closing_details_window) # Riga 614 nel log precedente
print(f"DEBUG POST-PROTOCOL: Successfully set protocol WM_DELETE_WINDOW.")
else:
print(f"CRITICAL DEBUG PRE-PROTOCOL: self.protocol method IS NOT available before protocol call!")
except AttributeError as e_protocol_attr:
print(f"ERROR DURING PROTOCOL SET (AttributeError): {e_protocol_attr}")
logger.error(f"FullFlightDetailsWindow __init__ protocol AttributeError: {e_protocol_attr}", exc_info=True) # Log con traceback
except Exception as e_protocol_gen:
print(f"ERROR DURING PROTOCOL SET (General Exception): {e_protocol_gen}")
logger.error(f"FullFlightDetailsWindow __init__ protocol General Exception: {e_protocol_gen}", exc_info=True) # Log con traceback
# --- FINE BLOCCO DI DEBUG PER PROTOCOL ---
def _initialize_detail_map_manager(self): def _initialize_detail_map_manager(self):
# ... (invariato)
if not self.track_map_canvas.winfo_exists(): if not self.track_map_canvas.winfo_exists():
logger.warning("Detail map canvas does not exist. Cannot initialize MapCanvasManager.") logger.warning(f"FullDetailsWindow ({self.icao24.upper()}): Detail map canvas does not exist. Cannot initialize MapCanvasManager.")
return return
canvas_w = self.track_map_canvas.winfo_width() canvas_w = self.track_map_canvas.winfo_width()
canvas_h = self.track_map_canvas.winfo_height() canvas_h = self.track_map_canvas.winfo_height()
if canvas_w <= 1 or canvas_h <= 1: # Canvas non ancora dimensionato if canvas_w <= 1 or canvas_h <= 1:
logger.info("Detail map canvas not yet sized. Retrying initialization later.") logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Detail map canvas not yet sized. Retrying initialization later.")
self.after(200, self._initialize_detail_map_manager) # Riprova self.after(200, self._initialize_detail_map_manager)
return return
logger.info(f"Initializing MapCanvasManager for detail window (Canvas size: {canvas_w}x{canvas_h})") logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Initializing MapCanvasManager for detail window (Canvas size: {canvas_w}x{canvas_h})")
try: try:
self.detail_map_manager = MapCanvasManager( self.detail_map_manager = MapCanvasManager(
app_controller=self.controller, app_controller=self.controller,
tk_canvas=self.track_map_canvas, tk_canvas=self.track_map_canvas,
initial_bbox_dict=None initial_bbox_dict=None,
is_detail_map=True
) )
self.detail_map_manager._display_placeholder_text("Awaiting flight track data...") logger.info(f"FullDetailsWindow ({self.icao24.upper()}): MapCanvasManager for detail window initialized.")
logger.info("MapCanvasManager for detail window initialized.")
if self._initial_track_data is not None or self._initial_live_data is not None:
logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Map manager ready, applying initial track/live data to map.")
self._update_track_map(self._initial_track_data, self._initial_live_data)
else:
self.detail_map_manager._display_placeholder_text("Awaiting flight data...")
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize MapCanvasManager for detail window: {e}", exc_info=True) logger.error(f"FullDetailsWindow ({self.icao24.upper()}): Failed to initialize MapCanvasManager for detail window: {e}", exc_info=True)
if self.track_map_canvas.winfo_exists(): if self.track_map_canvas.winfo_exists() and not self._map_init_error_displayed:
self.track_map_canvas.create_text( self.track_map_canvas.create_text(
canvas_w / 2, canvas_h / 2, canvas_w / 2, canvas_h / 2,
text=f"Error initializing map:\n{e}", text=f"Error initializing map:\n{e}",
fill="red", font=("Arial", 10), justify=tk.CENTER fill="red", font=("Arial", 10), justify=tk.CENTER
) )
self._map_init_error_displayed = True
def _create_details_layout(self, parent_frame: ttk.LabelFrame): def _create_details_layout(self, parent_frame: ttk.LabelFrame):
# ... (invariato)
parent_frame.columnconfigure(1, weight=1) parent_frame.columnconfigure(1, weight=1)
parent_frame.columnconfigure(3, weight=1) parent_frame.columnconfigure(3, weight=1)
fields = [ fields = [
@ -218,8 +185,15 @@ class FullFlightDetailsWindow(tk.Toplevel):
] ]
row_idx, col_idx, max_cols_per_row = 0, 0, 2 row_idx, col_idx, max_cols_per_row = 0, 0, 2
for key, text_label in fields: for key, text_label in fields:
lbl = ttk.Label(parent_frame, text=text_label, font="-weight bold" if key == "icao24" else None) lbl_text = text_label
if key == "icao24": # Per il LabelFrame, usiamo l'ICAO normalizzato (lowercase) ma per il display lo vogliamo uppercase
lbl = ttk.Label(parent_frame, text=lbl_text, font="-weight bold")
else:
lbl = ttk.Label(parent_frame, text=lbl_text)
lbl.grid(row=row_idx, column=col_idx * 2, sticky=tk.W, pady=1, padx=(0, 3)) lbl.grid(row=row_idx, column=col_idx * 2, sticky=tk.W, pady=1, padx=(0, 3))
# Per il valore di icao24, lo prenderemo da all_data che sarà già normalizzato se proviene da CanonicalFlightState
# Ma il campo ICAO24 nei dati statici potrebbe essere case-insensitive.
# La visualizzazione lo metterà in uppercase se lo forniamo così.
val_lbl = ttk.Label(parent_frame, text="N/A", wraplength=180) val_lbl = ttk.Label(parent_frame, text="N/A", wraplength=180)
val_lbl.grid(row=row_idx, column=col_idx * 2 + 1, sticky=tk.W, pady=1, padx=(0, 10)) val_lbl.grid(row=row_idx, column=col_idx * 2 + 1, sticky=tk.W, pady=1, padx=(0, 10))
self.detail_labels[key] = val_lbl self.detail_labels[key] = val_lbl
@ -227,9 +201,7 @@ class FullFlightDetailsWindow(tk.Toplevel):
if col_idx >= max_cols_per_row: col_idx, row_idx = 0, row_idx + 1 if col_idx >= max_cols_per_row: col_idx, row_idx = 0, row_idx + 1
if col_idx != 0: ttk.Frame(parent_frame).grid(row=row_idx, column=col_idx*2, columnspan=(max_cols_per_row - col_idx)*2) if col_idx != 0: ttk.Frame(parent_frame).grid(row=row_idx, column=col_idx*2, columnspan=(max_cols_per_row - col_idx)*2)
def _create_live_details_layout(self, parent_frame: ttk.LabelFrame): def _create_live_details_layout(self, parent_frame: ttk.LabelFrame):
# ... (invariato)
parent_frame.columnconfigure(1, weight=1) parent_frame.columnconfigure(1, weight=1)
live_fields = [ live_fields = [
("callsign", "Callsign (Live):"), ("baro_altitude_m", "Altitude (Baro):"), ("callsign", "Callsign (Live):"), ("baro_altitude_m", "Altitude (Baro):"),
@ -252,57 +224,86 @@ class FullFlightDetailsWindow(tk.Toplevel):
live_data: Optional[Dict[str, Any]], live_data: Optional[Dict[str, Any]],
full_track_data: Optional[List[Dict[str, Any]]], full_track_data: Optional[List[Dict[str, Any]]],
): ):
# ... (invariato)
if not self.winfo_exists(): return if not self.winfo_exists(): return
logger.debug(f"FullDetailsWindow: Updating. Static keys: {static_data.keys() if static_data else 'None'}, Live keys: {live_data.keys() if live_data else 'None'}")
self._initial_static_data = static_data
self._initial_live_data = live_data
self._initial_track_data = full_track_data
logger.debug(f"FullDetailsWindow ({self.icao24.upper()}): Updating. Static: {bool(static_data)}, Live: {bool(live_data)}, Track points: {len(full_track_data) if full_track_data else 0}")
all_data = {} all_data = {}
if static_data: all_data.update(static_data) if static_data: all_data.update(static_data)
if live_data: all_data.update(live_data) if live_data: # Dati live sovrascrivono quelli statici se le chiavi coincidono
# Normalizza icao24 in live_data se presente, per coerenza
if 'icao24' in live_data and isinstance(live_data['icao24'], str):
live_data_processed = live_data.copy()
live_data_processed['icao24'] = live_data_processed['icao24'].lower().strip()
all_data.update(live_data_processed)
else:
all_data.update(live_data)
for key, label_widget in self.detail_labels.items(): for key, label_widget in self.detail_labels.items():
if label_widget.winfo_exists(): if label_widget.winfo_exists():
value = all_data.get(key) value = all_data.get(key)
display_text = "N/A" # Caso speciale per icao24: visualizzalo sempre in uppercase
if value is not None and not (isinstance(value, str) and not value.strip()): if key == "icao24":
if key in ["baro_altitude_m", "geo_altitude_m"] and isinstance(value, (float, int)): display_text = f"{value:.0f} m" display_text = str(value).upper() if value else "N/A"
elif key == "velocity_mps" and isinstance(value, (float, int)): display_text = f"{value:.1f} m/s ({value * 1.94384:.1f} kts)" else:
elif key == "vertical_rate_mps" and isinstance(value, (float, int)): display_text = f"{value * 196.85:.0f} ft/min ({value:.1f} m/s)" display_text = "N/A"
elif key == "true_track_deg" and isinstance(value, (float, int)): display_text = f"{value:.1f}°" if value is not None and not (isinstance(value, str) and not value.strip()):
elif key in ["timestamp", "last_contact_timestamp", "firstflightdate", "timestamp_metadata"]: if key in ["baro_altitude_m", "geo_altitude_m"] and isinstance(value, (float, int)): display_text = f"{value:.0f} m"
if isinstance(value, (int, float)) and value > 0: elif key == "velocity_mps" and isinstance(value, (float, int)): display_text = f"{value:.1f} m/s ({value * 1.94384:.1f} kts)"
try: display_text = datetime.fromtimestamp(value, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S Z') elif key == "vertical_rate_mps" and isinstance(value, (float, int)): display_text = f"{value * 196.85:.0f} ft/min ({value:.1f} m/s)"
except: display_text = str(value) + " (raw ts)" elif key == "true_track_deg" and isinstance(value, (float, int)): display_text = f"{value:.1f}°"
elif isinstance(value, str) and value.strip(): display_text = value elif key in ["timestamp", "last_contact_timestamp", "timestamp_metadata"]:
elif isinstance(value, bool): display_text = str(value) if isinstance(value, (int, float)) and value > 0:
elif key == "built_year" and value: display_text = str(int(value)) if isinstance(value, (float, int)) and value > 0 else str(value) try: display_text = datetime.fromtimestamp(value, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S Z')
else: display_text = str(value) except: display_text = str(value) + " (raw ts)"
elif isinstance(value, str) and value.strip(): display_text = value
elif key == "firstflightdate":
if isinstance(value, str) and value.strip(): display_text = value
elif isinstance(value, (int,float)) and value > 0 :
try: display_text = datetime.fromtimestamp(value, tz=timezone.utc).strftime('%Y-%m-%d')
except: display_text = str(value)
elif isinstance(value, bool): display_text = str(value)
elif key == "built_year" and value: display_text = str(int(value)) if isinstance(value, (float, int)) and value > 0 else str(value)
else: display_text = str(value)
label_widget.config(text=display_text) label_widget.config(text=display_text)
registration = all_data.get("registration") registration = all_data.get("registration")
if registration and str(registration).strip() and str(registration) != "N/A": if registration and str(registration).strip() and str(registration) != "N/A":
self.current_jetphotos_url = f"https://www.jetphotos.com/photo/keyword/{str(registration).upper()}" self.current_jetphotos_url = f"https://www.jetphotos.com/photo/keyword/{str(registration).upper()}"
self.jetphotos_link_label.config(text=f"View '{str(registration).upper()}' on JetPhotos") self.jetphotos_link_label.config(text=f"View '{str(registration).upper()}' on JetPhotos")
self.jetphotos_link_label.grid() if not self.jetphotos_link_label.winfo_ismapped(): self.jetphotos_link_label.grid()
else: else:
self.current_jetphotos_url = None self.current_jetphotos_url = None
self.jetphotos_link_label.grid_remove() if self.jetphotos_link_label.winfo_ismapped(): self.jetphotos_link_label.grid_remove()
self._update_track_table(full_track_data) self._update_track_table(full_track_data)
self._update_track_map(full_track_data, live_data) if self.detail_map_manager and self.detail_map_manager.canvas.winfo_exists():
self._update_track_map(full_track_data, live_data)
else:
logger.info(f"FullDetailsWindow ({self.icao24.upper()}): update_details called, but detail_map_manager not ready. Map will update upon manager initialization.")
def _update_track_table(self, track_data_list: Optional[List[Dict[str, Any]]]): def _update_track_table(self, track_data_list: Optional[List[Dict[str, Any]]]):
# ... (invariato)
if not self.track_table.winfo_exists(): return if not self.track_table.winfo_exists(): return
for item in self.track_table.get_children(): self.track_table.delete(item) for item in self.track_table.get_children(): self.track_table.delete(item)
if not track_data_list: if not track_data_list:
logger.info("FullDetailsWindow: No track data to display in table.") logger.info(f"FullDetailsWindow ({self.icao24.upper()}): No track data to display in table.")
return return
logger.info(f"FullDetailsWindow: Populating track table with {len(track_data_list)} points.")
for point_dict in track_data_list: logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Populating track table with {len(track_data_list)} points.")
for point_dict in reversed(track_data_list):
values = [] values = []
for col_key in self.track_table_cols.keys(): for col_key in self.track_table_cols.keys():
raw_value = point_dict.get(col_key) raw_value = point_dict.get(col_key)
display_value = "N/A" display_value = "N/A"
if raw_value is not None: if raw_value is not None:
if col_key == "timestamp": if col_key == "timestamp":
try: display_value = datetime.fromtimestamp(float(raw_value), tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S') try: display_value = datetime.fromtimestamp(float(raw_value), tz=timezone.utc).strftime('%H:%M:%S')
except: display_value = str(raw_value) except: display_value = str(raw_value)
elif isinstance(raw_value, float): elif isinstance(raw_value, float):
if col_key in ["latitude", "longitude"]: display_value = f"{raw_value:.5f}" if col_key in ["latitude", "longitude"]: display_value = f"{raw_value:.5f}"
@ -312,118 +313,177 @@ class FullFlightDetailsWindow(tk.Toplevel):
elif isinstance(raw_value, bool): display_value = str(raw_value) elif isinstance(raw_value, bool): display_value = str(raw_value)
else: display_value = str(raw_value) else: display_value = str(raw_value)
values.append(display_value) values.append(display_value)
self.track_table.insert("", tk.END, values=values) self.track_table.insert("", 0, values=values)
def _update_track_map(self, track_data_list: Optional[List[Dict[str, Any]]], live_data: Optional[Dict[str, Any]]): def _update_track_map(self, track_data_list: Optional[List[Dict[str, Any]]], current_live_data: Optional[Dict[str, Any]]):
# ... (invariato) if not self.detail_map_manager or not self.detail_map_manager.canvas.winfo_exists():
if not self.detail_map_manager: logger.warning(f"FullDetailsWindow ({self.icao24.upper()}): _update_track_map called but DetailMapManager not ready or canvas gone.")
logger.warning("FullDetailsWindow: DetailMapManager not initialized. Cannot update track map.") self._initial_track_data = track_data_list
if self.track_map_canvas.winfo_exists() and not hasattr(self, "_map_init_error_displayed"): self._initial_live_data = current_live_data
self.track_map_canvas.create_text(self.track_map_canvas.winfo_width()/2, self.track_map_canvas.winfo_height()/2, text="Map manager for detail view\nis not available.", fill="orange", font=("Arial", 10), justify=tk.CENTER)
self._map_init_error_displayed = True
return return
logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Updating detail map with track and live data.")
self.detail_map_manager.clear_map_display() self.detail_map_manager.clear_map_display()
if not track_data_list or not track_data_list:
logger.info("FullDetailsWindow: No track data to display on map.") valid_track_points_for_bbox: List[Tuple[float, float]] = []
self.detail_map_manager._display_placeholder_text("No flight track data available.") canonical_track_states_for_drawing: List[CanonicalFlightState] = []
return
logger.info(f"FullDetailsWindow: Displaying track with {len(track_data_list)} points on detail map.") current_icao_normalized = self.icao24 # Già lowercase da __init__
canonical_track_states: List[CanonicalFlightState] = []
for point_dict in track_data_list: if track_data_list:
try: for point_dict in track_data_list:
state = CanonicalFlightState( lat, lon = point_dict.get("latitude"), point_dict.get("longitude")
icao24=self.icao24, timestamp=float(point_dict.get("timestamp", 0)), if lat is not None and lon is not None:
last_contact_timestamp=float(point_dict.get("timestamp", 0)), latitude=point_dict.get("latitude"), valid_track_points_for_bbox.append((lat, lon))
longitude=point_dict.get("longitude"), on_ground=point_dict.get("on_ground", False), callsign=None, origin_country=None, try:
baro_altitude_m=point_dict.get("baro_altitude_m"), geo_altitude_m=point_dict.get("geo_altitude_m"), state = CanonicalFlightState(
velocity_mps=point_dict.get("velocity_mps"), true_track_deg=point_dict.get("true_track_deg"), icao24=current_icao_normalized, # Usa l'ICAO normalizzato della finestra
vertical_rate_mps=point_dict.get("vertical_rate_mps") timestamp=float(point_dict.get("timestamp", 0)),
) last_contact_timestamp=float(point_dict.get("timestamp", 0)),
canonical_track_states.append(state) latitude=lat, longitude=lon, on_ground=bool(point_dict.get("on_ground", False)),
except Exception as e: logger.warning(f"Could not convert track point to CanonicalFlightState: {point_dict}, Error: {e}"); continue # Altri campi non sono cruciali per il disegno della traccia base
if not canonical_track_states: baro_altitude_m=point_dict.get("baro_altitude_m"),
logger.warning("FullDetailsWindow: No valid canonical states created from track data.") geo_altitude_m=point_dict.get("geo_altitude_m"),
self.detail_map_manager._display_placeholder_text("Error processing track data."); return velocity_mps=point_dict.get("velocity_mps"),
current_flight_for_map = [] true_track_deg=point_dict.get("true_track_deg"),
if live_data and live_data.get("latitude") is not None and live_data.get("longitude") is not None: vertical_rate_mps=point_dict.get("vertical_rate_mps")
)
canonical_track_states_for_drawing.append(state)
except Exception as e:
logger.warning(f"Could not convert track point to CanonicalFlightState for drawing: {point_dict}, Error: {e}")
continue
current_flight_for_map_drawing: List[CanonicalFlightState] = []
if current_live_data:
live_lat, live_lon = current_live_data.get("latitude"), current_live_data.get("longitude")
if live_lat is not None and live_lon is not None:
valid_track_points_for_bbox.append((live_lat, live_lon))
try: try:
# Assicurati che l'ICAO del punto live sia normalizzato se lo usi per creare CanonicalFlightState
live_icao = current_live_data.get("icao24", current_icao_normalized)
if isinstance(live_icao, str): live_icao = live_icao.lower().strip()
else: live_icao = current_icao_normalized
live_state = CanonicalFlightState( live_state = CanonicalFlightState(
icao24=live_data.get("icao24", self.icao24), callsign=live_data.get("callsign"), origin_country=live_data.get("origin_country"), icao24=live_icao, # Deve corrispondere alla chiave usata per la traccia
timestamp=float(live_data.get("timestamp", time.time())), last_contact_timestamp=float(live_data.get("last_contact_timestamp", time.time())), callsign=current_live_data.get("callsign"),
latitude=live_data.get("latitude"), longitude=live_data.get("longitude"), baro_altitude_m=live_data.get("baro_altitude_m"), origin_country=current_live_data.get("origin_country"),
geo_altitude_m=live_data.get("geo_altitude_m"), on_ground=live_data.get("on_ground", False), velocity_mps=live_data.get("velocity_mps"), timestamp=float(current_live_data.get("timestamp", time.time())),
true_track_deg=live_data.get("true_track_deg"), vertical_rate_mps=live_data.get("vertical_rate_mps"), squawk=live_data.get("squawk"), last_contact_timestamp=float(current_live_data.get("last_contact_timestamp", time.time())),
spi=live_data.get("spi"), position_source=live_data.get("position_source") latitude=live_lat, longitude=live_lon,
baro_altitude_m=current_live_data.get("baro_altitude_m"),
geo_altitude_m=current_live_data.get("geo_altitude_m"),
on_ground=bool(current_live_data.get("on_ground", False)),
velocity_mps=current_live_data.get("velocity_mps"),
true_track_deg=current_live_data.get("true_track_deg"),
vertical_rate_mps=current_live_data.get("vertical_rate_mps"),
squawk=current_live_data.get("squawk"),
spi=bool(current_live_data.get("spi")),
position_source=current_live_data.get("position_source")
) )
current_flight_for_map = [live_state] current_flight_for_map_drawing = [live_state]
except Exception as e: logger.warning(f"Could not convert live data to CanonicalFlightState: {live_data}, Error: {e}") except Exception as e:
logger.warning(f"Could not convert live data to CanonicalFlightState for drawing: {current_live_data}, Error: {e}")
if not valid_track_points_for_bbox:
logger.info(f"FullDetailsWindow ({self.icao24.upper()}): No valid track points or live position to display on map.")
self.detail_map_manager._display_placeholder_text("No flight track data available to display.")
return
with self.detail_map_manager._map_data_lock: with self.detail_map_manager._map_data_lock:
self.detail_map_manager.flight_tracks_gui.clear() self.detail_map_manager.flight_tracks_gui.clear()
track_deque = deque(maxlen=len(canonical_track_states) + 5) track_deque = deque(maxlen=(len(canonical_track_states_for_drawing) + 5) if canonical_track_states_for_drawing else 5)
for state in canonical_track_states: for state in canonical_track_states_for_drawing:
if state.latitude is not None and state.longitude is not None and state.timestamp is not None: if state.latitude is not None and state.longitude is not None and state.timestamp is not None:
track_deque.append((state.latitude, state.longitude, state.timestamp)) track_deque.append((state.latitude, state.longitude, state.timestamp))
if track_deque: self.detail_map_manager.flight_tracks_gui[self.icao24] = track_deque if track_deque:
if current_flight_for_map: self.detail_map_manager._current_flights_to_display_gui = current_flight_for_map # Usa self.icao24 (che è già lowercase) come chiave
elif canonical_track_states: self.detail_map_manager._current_flights_to_display_gui = [canonical_track_states[-1]] self.detail_map_manager.flight_tracks_gui[self.icao24] = track_deque
else: self.detail_map_manager._current_flights_to_display_gui = []
if canonical_track_states: self.detail_map_manager._current_flights_to_display_gui = current_flight_for_map_drawing
min_lat = min(s.latitude for s in canonical_track_states if s.latitude is not None)
max_lat = max(s.latitude for s in canonical_track_states if s.latitude is not None) if len(valid_track_points_for_bbox) == 1:
min_lon = min(s.longitude for s in canonical_track_states if s.longitude is not None) lat, lon = valid_track_points_for_bbox[0]
max_lon = max(s.longitude for s in canonical_track_states if s.longitude is not None) logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Single point track/live. Centering map with patch.")
padding_lat = (max_lat - min_lat) * 0.10; padding_lon = (max_lon - min_lon) * 0.10 self.detail_map_manager.center_map_and_fit_patch(lat, lon, patch_size_km=DEFAULT_SINGLE_POINT_PATCH_KM)
if padding_lat == 0: padding_lat = 0.1 else:
if padding_lon == 0: padding_lon = 0.1 min_lat = min(p[0] for p in valid_track_points_for_bbox)
track_bbox = {"lat_min": max(-90, min_lat-padding_lat), "lon_min": max(-180, min_lon-padding_lon), "lat_max": min(90, max_lat+padding_lat), "lon_max": min(180, max_lon+padding_lon)} max_lat = max(p[0] for p in valid_track_points_for_bbox)
if track_bbox["lat_min"] >= track_bbox["lat_max"]: track_bbox["lat_max"] = track_bbox["lat_min"] + 0.01 min_lon = min(p[1] for p in valid_track_points_for_bbox)
if track_bbox["lon_min"] >= track_bbox["lon_max"]: track_bbox["lon_max"] = track_bbox["lon_min"] + 0.01 max_lon = max(p[1] for p in valid_track_points_for_bbox)
padding_deg = 0.05
if abs(max_lat - min_lat) < 1e-6 : padding_lat_eff = padding_deg # Se i punti sono quasi collineari verticalmente
else: padding_lat_eff = max(padding_deg, (max_lat - min_lat) * 0.15)
if abs(max_lon - min_lon) < 1e-6 : padding_lon_eff = padding_deg # Se i punti sono quasi collineari orizzontalmente
else: padding_lon_eff = max(padding_deg, (max_lon - min_lon) * 0.15)
track_bbox = {
"lat_min": max(-90.0, min_lat - padding_lat_eff),
"lon_min": max(-180.0, min_lon - padding_lon_eff),
"lat_max": min(90.0, max_lat + padding_lat_eff),
"lon_max": min(180.0, max_lon + padding_lon_eff),
}
if track_bbox["lat_min"] >= track_bbox["lat_max"]:
track_bbox["lat_max"] = track_bbox["lat_min"] + 0.01
if track_bbox["lon_min"] >= track_bbox["lon_max"]:
track_bbox["lon_max"] = track_bbox["lon_min"] + 0.01
if _is_valid_bbox_dict(track_bbox): if _is_valid_bbox_dict(track_bbox):
logger.info(f"FullDetailsWindow: Requesting detail map render for BBox of track: {track_bbox}") logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Requesting detail map render for calculated BBox of track: {track_bbox}")
self.detail_map_manager._target_bbox_input_gui = track_bbox
self.detail_map_manager._request_map_render_for_bbox(track_bbox, preserve_current_zoom_if_possible=False) self.detail_map_manager._request_map_render_for_bbox(track_bbox, preserve_current_zoom_if_possible=False)
else: else:
logger.warning(f"FullDetailsWindow: Calculated track BBox is invalid: {track_bbox}. Using last known view.") logger.warning(f"FullDetailsWindow ({self.icao24.upper()}): Calculated track BBox is invalid: {track_bbox}. Attempting fallback.")
if self.detail_map_manager._current_center_lat_gui and self.detail_map_manager._current_center_lon_gui and self.detail_map_manager._current_zoom_gui: last_known_lat, last_known_lon = valid_track_points_for_bbox[-1]
self.detail_map_manager._request_map_render(self.detail_map_manager._current_center_lat_gui, self.detail_map_manager._current_center_lon_gui, self.detail_map_manager._current_zoom_gui) self.detail_map_manager.center_map_and_fit_patch(last_known_lat, last_known_lon, patch_size_km=DEFAULT_SINGLE_POINT_PATCH_KM * 2)
else: self.detail_map_manager._display_placeholder_text("Cannot determine map view for track.")
else:
if self.detail_map_manager._current_flights_to_display_gui:
live_pt = self.detail_map_manager._current_flights_to_display_gui[0]
if live_pt.latitude and live_pt.longitude:
self.detail_map_manager.center_map_and_fit_patch(live_pt.latitude, live_pt.longitude, patch_size_km=50)
else: self.detail_map_manager._display_placeholder_text("No track data to display on map.")
def _open_jetphotos_link_action(self, event=None): def _open_jetphotos_link_action(self, event=None):
# ... (invariato)
if self.current_jetphotos_url: if self.current_jetphotos_url:
try: webbrowser.open_new_tab(self.current_jetphotos_url); logger.info(f"Opening JetPhotos link: {self.current_jetphotos_url}") try:
except Exception as e: logger.error(f"Failed to open JetPhotos link {self.current_jetphotos_url}: {e}"); # messagebox.showerror("Link Error", f"Could not open link: {e}", parent=self) webbrowser.open_new_tab(self.current_jetphotos_url)
else: logger.warning("No JetPhotos URL to open for current aircraft."); # messagebox.showinfo("No Link", "No registration found to search on JetPhotos.", parent=self) logger.info(f"Opening JetPhotos link: {self.current_jetphotos_url}")
except Exception as e:
logger.error(f"Failed to open JetPhotos link {self.current_jetphotos_url}: {e}")
else:
logger.warning("No JetPhotos URL to open for current aircraft.")
def center_window(self): def center_window(self):
# ... (invariato)
self.update_idletasks() self.update_idletasks()
width = self.winfo_width(); height = self.winfo_height() width = self.winfo_width()
if width <= 1: width = 1000 height = self.winfo_height()
if height <= 1: height = 750 if width <= 1: width = self.winfo_reqwidth() if self.winfo_reqwidth() > 1 else 1000
x_screen = self.winfo_screenwidth(); y_screen = self.winfo_screenheight() if height <= 1: height = self.winfo_reqheight() if self.winfo_reqheight() > 1 else 750
x = (x_screen // 2) - (width // 2); y = (y_screen // 2) - (height // 2)
if x < 0: x = 0 x_screen = self.winfo_screenwidth()
if y < 0: y = 0 y_screen = self.winfo_screenheight()
x = (x_screen // 2) - (width // 2)
y = (y_screen // 2) - (height // 2)
if x + width > x_screen: x = x_screen - width if x + width > x_screen: x = x_screen - width
if y + height > y_screen: y = y_screen - height if y + height > y_screen: y = y_screen - height
if width > 0 and height > 0: self.geometry(f"{width}x{height}+{x}+{y}") if x < 0: x = 0
if y < 0: y = 0
if width > 0 and height > 0 : self.geometry(f"{width}x{height}+{x}+{y}")
def _on_closing_details_window(self): def _on_closing_details_window(self):
logger.info(f"FullFlightDetailsWindow for {self.icao24} is closing.") logger.info(f"FullFlightDetailsWindow for {self.icao24.upper()} is closing.")
if self.detail_map_manager: if self.detail_map_manager:
logger.info("Requesting shutdown for detail_map_manager worker.") logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Requesting shutdown for detail_map_manager worker.")
try: self.detail_map_manager.shutdown_worker() try:
except Exception as e: logger.error(f"Error during detail_map_manager worker shutdown: {e}", exc_info=True) self.detail_map_manager.shutdown_worker()
except Exception as e:
logger.error(f"FullDetailsWindow ({self.icao24.upper()}): Error during detail_map_manager worker shutdown: {e}", exc_info=True)
if self.controller and hasattr(self.controller, "details_window_closed"):
try:
self.controller.details_window_closed(self.icao24) # self.icao24 è già lowercase
except Exception as e_notify_close:
logger.error(f"FullDetailsWindow ({self.icao24.upper()}): Error notifying controller of details window closure: {e_notify_close}", exc_info=True)
self.destroy() self.destroy()
# ... (blocco if __name__ == "__main__": invariato)
if __name__ == "__main__": if __name__ == "__main__":
root_test = tk.Tk() root_test = tk.Tk()
root_test.title("Main Window (Test)") root_test.title("Main Window (Test)")
@ -431,22 +491,42 @@ if __name__ == "__main__":
"icao24": "TST001", "registration": "N-TEST", "manufacturername": "TestAircraft Co.", "model": "SkyTester Pro", "icao24": "TST001", "registration": "N-TEST", "manufacturername": "TestAircraft Co.", "model": "SkyTester Pro",
"typecode": "TSPR", "operator": "Test Flight Ops", "built_year": 2022, "categorydescription": "Experimental Test Vehicle", "typecode": "TSPR", "operator": "Test Flight Ops", "built_year": 2022, "categorydescription": "Experimental Test Vehicle",
"country": "Testland", "timestamp_metadata": time.time() - 3600 * 24 * 30, "country": "Testland", "timestamp_metadata": time.time() - 3600 * 24 * 30,
"firstflightdate": "2022-01-15"
} }
sample_live = { sample_live = {
"icao24": "tst001", #Lowercase per test
"callsign": "TEST01", "baro_altitude_m": 10000.0, "geo_altitude_m": 10050.0, "velocity_mps": 250.5, "callsign": "TEST01", "baro_altitude_m": 10000.0, "geo_altitude_m": 10050.0, "velocity_mps": 250.5,
"vertical_rate_mps": 5.2, "true_track_deg": 123.4, "on_ground": False, "squawk": "7000", "vertical_rate_mps": 5.2, "true_track_deg": 123.4, "on_ground": False, "squawk": "7000",
"origin_country": "Testland Live", "timestamp": time.time() - 60, "origin_country": "Testland Live", "timestamp": time.time() - 60, "last_contact_timestamp": time.time() - 58,
"latitude": 45.15, "longitude": 9.15
} }
sample_track = [ sample_track = [
{"latitude": 45.0, "longitude": 9.0, "baro_altitude_m": 9000, "timestamp": time.time() - 300,}, {"latitude": 45.0, "longitude": 9.0, "baro_altitude_m": 9000, "timestamp": time.time() - 300, "on_ground": False},
{"latitude": 45.1, "longitude": 9.1, "baro_altitude_m": 9500, "timestamp": time.time() - 180,}, {"latitude": 45.1, "longitude": 9.1, "baro_altitude_m": 9500, "timestamp": time.time() - 180, "on_ground": False},
{"latitude": 45.2, "longitude": 9.2, "baro_altitude_m": 10000, "timestamp": time.time() - 60,}, {"latitude": 45.2, "longitude": 9.2, "baro_altitude_m": 10000, "timestamp": time.time() - 60, "on_ground": False},
] ]
def open_details_test(): sample_track_single_point = [
if hasattr(root_test, "details_win_instance") and root_test.details_win_instance.winfo_exists(): # type: ignore {"latitude": 46.0, "longitude": 10.0, "baro_altitude_m": 8000, "timestamp": time.time() - 120, "on_ground": False}
root_test.details_win_instance.destroy() # type: ignore ]
details_window = FullFlightDetailsWindow(root_test, "TST001") sample_live_only = {
details_window.update_details(sample_static, sample_live, sample_track) "icao24": "tst002", "callsign": "LIVEONLY", "latitude": 45.5, "longitude": 9.5, "baro_altitude_m": 5000,
root_test.details_win_instance = details_window # type: ignore "timestamp": time.time()-10, "last_contact_timestamp": time.time()-10, "on_ground": False
ttk.Button(root_test, text="Open Full Details Test", command=open_details_test).pack(padx=50, pady=50) }
def open_details_test(static, live, track, icao="TST001"):
if hasattr(root_test, "details_win_instance_test") and root_test.details_win_instance_test.winfo_exists(): # type: ignore
root_test.details_win_instance_test.destroy() # type: ignore
details_window = FullFlightDetailsWindow(root_test, icao)
details_window._initial_static_data = static
details_window._initial_live_data = live
details_window._initial_track_data = track
details_window.update_details(static, live, track)
root_test.details_win_instance_test = details_window # type: ignore
ttk.Button(root_test, text="Open Full Details (Multi-Point Track)", command=lambda: open_details_test(sample_static, sample_live, sample_track, icao="tst001")).pack(padx=20, pady=5)
ttk.Button(root_test, text="Open Full Details (Single-Point Track)", command=lambda: open_details_test(sample_static, sample_live, sample_track_single_point, icao="tst001")).pack(padx=20, pady=5)
ttk.Button(root_test, text="Open Full Details (Live Only, No Track)", command=lambda: open_details_test(None, sample_live_only, None, icao="tst002")).pack(padx=20, pady=5)
ttk.Button(root_test, text="Open Full Details (No Track, No Live)", command=lambda: open_details_test(sample_static, None, None, icao="tst001")).pack(padx=20, pady=5)
root_test.mainloop() root_test.mainloop()

File diff suppressed because it is too large Load Diff

View File

@ -741,6 +741,10 @@ def _draw_single_flight(
color_index = hash(flight_state.icao24) % len(map_constants.TRACK_COLOR_PALETTE) color_index = hash(flight_state.icao24) % len(map_constants.TRACK_COLOR_PALETTE)
flight_and_track_base_color_hex = map_constants.TRACK_COLOR_PALETTE[color_index] flight_and_track_base_color_hex = map_constants.TRACK_COLOR_PALETTE[color_index]
logger.debug(
f"MAP_DRAWING: _draw_single_flight for {flight_state.icao24} at px {pixel_coords}. Track deque length: {len(track_deque) if track_deque else 'None'}"
)
# --- Draw Track First (so aircraft is on top) --- # --- Draw Track First (so aircraft is on top) ---
if track_deque and len(track_deque) > 1: if track_deque and len(track_deque) > 1:
# Convert all relevant geo points in track_deque to pixel points # Convert all relevant geo points in track_deque to pixel points
@ -768,6 +772,7 @@ def _draw_single_flight(
# pixel_track_points are now ordered from most recent (closest to aircraft) to oldest # pixel_track_points are now ordered from most recent (closest to aircraft) to oldest
if len(pixel_track_points) > 1: if len(pixel_track_points) > 1:
logger.debug(f"MAP_DRAWING ({flight_state.icao24}): Will draw {len(pixel_track_points)-1} line segments.")
for i in range(len(pixel_track_points) - 1): for i in range(len(pixel_track_points) - 1):
start_point_px = pixel_track_points[i] start_point_px = pixel_track_points[i]
end_point_px = pixel_track_points[i + 1] end_point_px = pixel_track_points[i + 1]
@ -775,6 +780,8 @@ def _draw_single_flight(
faded_color = _calculate_faded_color( faded_color = _calculate_faded_color(
flight_and_track_base_color_hex, i, len(pixel_track_points) flight_and_track_base_color_hex, i, len(pixel_track_points)
) )
logger.debug(f"MAP_DRAWING ({flight_state.icao24}): Segment {i}, Start: {start_point_px}, End: {end_point_px}, Color: {faded_color}") # LOG AGGIUNTO
if faded_color: if faded_color:
try: try:
draw.line( draw.line(