diff --git a/flightmonitor/gui/dialogs/full_flight_details_window.py b/flightmonitor/gui/dialogs/full_flight_details_window.py index c26b2ba..9faa8ad 100644 --- a/flightmonitor/gui/dialogs/full_flight_details_window.py +++ b/flightmonitor/gui/dialogs/full_flight_details_window.py @@ -1,16 +1,15 @@ # FlightMonitor/gui/dialogs/full_flight_details_window.py import tkinter as tk from tkinter import ttk -# from tkinter import messagebox # Rimosso per ora import webbrowser from datetime import datetime, timezone -from typing import Optional, Dict, Any, List # Rimosso Tuple -import time # Aggiunto per il test di live_data +from typing import Optional, Dict, Any, List +import time from ...map.map_canvas_manager import MapCanvasManager from ...map.map_utils import _is_valid_bbox_dict from ...data.common_models import CanonicalFlightState -from collections import deque # Aggiunto per il type hinting e l'uso +from collections import deque try: 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") logger.warning("FullFlightDetailsWindow using fallback standard Python logger.") +DEFAULT_SINGLE_POINT_PATCH_KM = 20.0 + class FullFlightDetailsWindow(tk.Toplevel): def __init__(self, parent, icao24: str, controller: Optional[Any] = None): - # --- INIZIO BLOCCO DI DEBUG --- - 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)}") + super().__init__(parent) + + # MODIFIED: Normalize ICAO24 to lowercase for internal use as key + self.icao24 = icao24.lower().strip() + self.title(f"Full Details - {self.icao24.upper()}") # Display title can still be uppercase - if hasattr(self, 'tk'): - print(f"DEBUG POST-SUPER: self.tk IS PRESENT. Value: {self.tk}") - print(f"DEBUG POST-SUPER: Type of self.tk: {type(self.tk)}") - 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.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.minsize(800, 600) @@ -70,7 +51,8 @@ class FullFlightDetailsWindow(tk.Toplevel): top_info_frame.columnconfigure(0, weight=3) 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) self.detail_labels: Dict[str, ttk.Label] = {} @@ -83,16 +65,16 @@ class FullFlightDetailsWindow(tk.Toplevel): 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.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.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("", self._open_jetphotos_link_action) self.current_jetphotos_url: Optional[str] = None self.jetphotos_link_label.grid_remove() 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) 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.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.detail_map_manager: Optional[MapCanvasManager] = None + self._map_init_error_displayed = False self.track_table_tab = ttk.Frame(self.track_notebook) self.track_notebook.add(self.track_table_tab, text="Track Data Table") @@ -147,64 +130,48 @@ class FullFlightDetailsWindow(tk.Toplevel): self.focus_set() self.after(100, self._initialize_detail_map_manager) - - # --- 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 --- + self.protocol("WM_DELETE_WINDOW", self._on_closing_details_window) def _initialize_detail_map_manager(self): - # ... (invariato) 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 canvas_w = self.track_map_canvas.winfo_width() canvas_h = self.track_map_canvas.winfo_height() - if canvas_w <= 1 or canvas_h <= 1: # Canvas non ancora dimensionato - logger.info("Detail map canvas not yet sized. Retrying initialization later.") - self.after(200, self._initialize_detail_map_manager) # Riprova + if canvas_w <= 1 or canvas_h <= 1: + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Detail map canvas not yet sized. Retrying initialization later.") + self.after(200, self._initialize_detail_map_manager) 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: self.detail_map_manager = MapCanvasManager( app_controller=self.controller, 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("MapCanvasManager for detail window initialized.") + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): 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: - logger.error(f"Failed to initialize MapCanvasManager for detail window: {e}", exc_info=True) - if self.track_map_canvas.winfo_exists(): + 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() and not self._map_init_error_displayed: self.track_map_canvas.create_text( canvas_w / 2, canvas_h / 2, text=f"Error initializing map:\n{e}", fill="red", font=("Arial", 10), justify=tk.CENTER ) + self._map_init_error_displayed = True def _create_details_layout(self, parent_frame: ttk.LabelFrame): - # ... (invariato) parent_frame.columnconfigure(1, weight=1) parent_frame.columnconfigure(3, weight=1) fields = [ @@ -218,8 +185,15 @@ class FullFlightDetailsWindow(tk.Toplevel): ] row_idx, col_idx, max_cols_per_row = 0, 0, 2 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)) + # 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.grid(row=row_idx, column=col_idx * 2 + 1, sticky=tk.W, pady=1, padx=(0, 10)) 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 != 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): - # ... (invariato) parent_frame.columnconfigure(1, weight=1) live_fields = [ ("callsign", "Callsign (Live):"), ("baro_altitude_m", "Altitude (Baro):"), @@ -252,57 +224,86 @@ class FullFlightDetailsWindow(tk.Toplevel): live_data: Optional[Dict[str, Any]], full_track_data: Optional[List[Dict[str, Any]]], ): - # ... (invariato) 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 = {} 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(): if label_widget.winfo_exists(): value = all_data.get(key) - display_text = "N/A" - if value is not None and not (isinstance(value, str) and not value.strip()): - if key in ["baro_altitude_m", "geo_altitude_m"] and isinstance(value, (float, int)): display_text = f"{value:.0f} m" - elif key == "velocity_mps" and isinstance(value, (float, int)): display_text = f"{value:.1f} m/s ({value * 1.94384:.1f} kts)" - elif key == "vertical_rate_mps" and isinstance(value, (float, int)): display_text = f"{value * 196.85:.0f} ft/min ({value:.1f} m/s)" - elif key == "true_track_deg" and isinstance(value, (float, int)): display_text = f"{value:.1f}°" - elif key in ["timestamp", "last_contact_timestamp", "firstflightdate", "timestamp_metadata"]: - if isinstance(value, (int, float)) and value > 0: - try: display_text = datetime.fromtimestamp(value, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S Z') - except: display_text = str(value) + " (raw ts)" - elif isinstance(value, str) and value.strip(): display_text = 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) + # Caso speciale per icao24: visualizzalo sempre in uppercase + if key == "icao24": + display_text = str(value).upper() if value else "N/A" + else: + display_text = "N/A" + if value is not None and not (isinstance(value, str) and not value.strip()): + if key in ["baro_altitude_m", "geo_altitude_m"] and isinstance(value, (float, int)): display_text = f"{value:.0f} m" + elif key == "velocity_mps" and isinstance(value, (float, int)): display_text = f"{value:.1f} m/s ({value * 1.94384:.1f} kts)" + elif key == "vertical_rate_mps" and isinstance(value, (float, int)): display_text = f"{value * 196.85:.0f} ft/min ({value:.1f} m/s)" + elif key == "true_track_deg" and isinstance(value, (float, int)): display_text = f"{value:.1f}°" + elif key in ["timestamp", "last_contact_timestamp", "timestamp_metadata"]: + if isinstance(value, (int, float)) and value > 0: + try: display_text = datetime.fromtimestamp(value, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S Z') + 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) + registration = all_data.get("registration") 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.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: 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_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]]]): - # ... (invariato) if not self.track_table.winfo_exists(): return for item in self.track_table.get_children(): self.track_table.delete(item) + 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 - 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 = [] for col_key in self.track_table_cols.keys(): raw_value = point_dict.get(col_key) display_value = "N/A" if raw_value is not None: 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) elif isinstance(raw_value, float): 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) else: display_value = str(raw_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]]): - # ... (invariato) - if not self.detail_map_manager: - logger.warning("FullDetailsWindow: DetailMapManager not initialized. Cannot update track map.") - if self.track_map_canvas.winfo_exists() and not hasattr(self, "_map_init_error_displayed"): - 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 + def _update_track_map(self, track_data_list: Optional[List[Dict[str, Any]]], current_live_data: Optional[Dict[str, Any]]): + if not self.detail_map_manager or not self.detail_map_manager.canvas.winfo_exists(): + logger.warning(f"FullDetailsWindow ({self.icao24.upper()}): _update_track_map called but DetailMapManager not ready or canvas gone.") + self._initial_track_data = track_data_list + self._initial_live_data = current_live_data return + + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Updating detail map with track and live data.") 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.") - self.detail_map_manager._display_placeholder_text("No flight track data available.") - return - logger.info(f"FullDetailsWindow: Displaying track with {len(track_data_list)} points on detail map.") - canonical_track_states: List[CanonicalFlightState] = [] - for point_dict in track_data_list: - try: - state = CanonicalFlightState( - icao24=self.icao24, timestamp=float(point_dict.get("timestamp", 0)), - last_contact_timestamp=float(point_dict.get("timestamp", 0)), latitude=point_dict.get("latitude"), - longitude=point_dict.get("longitude"), on_ground=point_dict.get("on_ground", False), callsign=None, origin_country=None, - baro_altitude_m=point_dict.get("baro_altitude_m"), geo_altitude_m=point_dict.get("geo_altitude_m"), - velocity_mps=point_dict.get("velocity_mps"), true_track_deg=point_dict.get("true_track_deg"), - vertical_rate_mps=point_dict.get("vertical_rate_mps") - ) - canonical_track_states.append(state) - except Exception as e: logger.warning(f"Could not convert track point to CanonicalFlightState: {point_dict}, Error: {e}"); continue - if not canonical_track_states: - logger.warning("FullDetailsWindow: No valid canonical states created from track data.") - self.detail_map_manager._display_placeholder_text("Error processing track data."); return - current_flight_for_map = [] - if live_data and live_data.get("latitude") is not None and live_data.get("longitude") is not None: + + valid_track_points_for_bbox: List[Tuple[float, float]] = [] + canonical_track_states_for_drawing: List[CanonicalFlightState] = [] + + current_icao_normalized = self.icao24 # Già lowercase da __init__ + + if track_data_list: + for point_dict in track_data_list: + lat, lon = point_dict.get("latitude"), point_dict.get("longitude") + if lat is not None and lon is not None: + valid_track_points_for_bbox.append((lat, lon)) + try: + state = CanonicalFlightState( + icao24=current_icao_normalized, # Usa l'ICAO normalizzato della finestra + timestamp=float(point_dict.get("timestamp", 0)), + last_contact_timestamp=float(point_dict.get("timestamp", 0)), + latitude=lat, longitude=lon, on_ground=bool(point_dict.get("on_ground", False)), + # Altri campi non sono cruciali per il disegno della traccia base + baro_altitude_m=point_dict.get("baro_altitude_m"), + geo_altitude_m=point_dict.get("geo_altitude_m"), + velocity_mps=point_dict.get("velocity_mps"), + true_track_deg=point_dict.get("true_track_deg"), + 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: + # 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( - icao24=live_data.get("icao24", self.icao24), callsign=live_data.get("callsign"), origin_country=live_data.get("origin_country"), - timestamp=float(live_data.get("timestamp", time.time())), last_contact_timestamp=float(live_data.get("last_contact_timestamp", time.time())), - latitude=live_data.get("latitude"), longitude=live_data.get("longitude"), baro_altitude_m=live_data.get("baro_altitude_m"), - geo_altitude_m=live_data.get("geo_altitude_m"), on_ground=live_data.get("on_ground", False), velocity_mps=live_data.get("velocity_mps"), - true_track_deg=live_data.get("true_track_deg"), vertical_rate_mps=live_data.get("vertical_rate_mps"), squawk=live_data.get("squawk"), - spi=live_data.get("spi"), position_source=live_data.get("position_source") + icao24=live_icao, # Deve corrispondere alla chiave usata per la traccia + callsign=current_live_data.get("callsign"), + origin_country=current_live_data.get("origin_country"), + timestamp=float(current_live_data.get("timestamp", time.time())), + last_contact_timestamp=float(current_live_data.get("last_contact_timestamp", time.time())), + 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] - except Exception as e: logger.warning(f"Could not convert live data to CanonicalFlightState: {live_data}, Error: {e}") + current_flight_for_map_drawing = [live_state] + 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: self.detail_map_manager.flight_tracks_gui.clear() - track_deque = deque(maxlen=len(canonical_track_states) + 5) - for state in canonical_track_states: + 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_drawing: 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)) - if track_deque: self.detail_map_manager.flight_tracks_gui[self.icao24] = track_deque - if current_flight_for_map: self.detail_map_manager._current_flights_to_display_gui = current_flight_for_map - elif canonical_track_states: self.detail_map_manager._current_flights_to_display_gui = [canonical_track_states[-1]] - else: self.detail_map_manager._current_flights_to_display_gui = [] - if canonical_track_states: - 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) - min_lon = min(s.longitude for s in canonical_track_states if s.longitude is not None) - max_lon = max(s.longitude for s in canonical_track_states if s.longitude is not None) - padding_lat = (max_lat - min_lat) * 0.10; padding_lon = (max_lon - min_lon) * 0.10 - if padding_lat == 0: padding_lat = 0.1 - if padding_lon == 0: padding_lon = 0.1 - 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)} - 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 track_deque: + # Usa self.icao24 (che è già lowercase) come chiave + self.detail_map_manager.flight_tracks_gui[self.icao24] = track_deque + + self.detail_map_manager._current_flights_to_display_gui = current_flight_for_map_drawing + + if len(valid_track_points_for_bbox) == 1: + lat, lon = valid_track_points_for_bbox[0] + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Single point track/live. Centering map with patch.") + self.detail_map_manager.center_map_and_fit_patch(lat, lon, patch_size_km=DEFAULT_SINGLE_POINT_PATCH_KM) + else: + min_lat = min(p[0] for p in valid_track_points_for_bbox) + max_lat = max(p[0] for p in valid_track_points_for_bbox) + min_lon = min(p[1] for p in valid_track_points_for_bbox) + 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): - logger.info(f"FullDetailsWindow: Requesting detail map render for BBox of track: {track_bbox}") - self.detail_map_manager._target_bbox_input_gui = track_bbox + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Requesting detail map render for calculated BBox of track: {track_bbox}") self.detail_map_manager._request_map_render_for_bbox(track_bbox, preserve_current_zoom_if_possible=False) else: - logger.warning(f"FullDetailsWindow: Calculated track BBox is invalid: {track_bbox}. Using last known view.") - 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: - 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) - 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.") + logger.warning(f"FullDetailsWindow ({self.icao24.upper()}): Calculated track BBox is invalid: {track_bbox}. Attempting fallback.") + last_known_lat, last_known_lon = valid_track_points_for_bbox[-1] + self.detail_map_manager.center_map_and_fit_patch(last_known_lat, last_known_lon, patch_size_km=DEFAULT_SINGLE_POINT_PATCH_KM * 2) def _open_jetphotos_link_action(self, event=None): - # ... (invariato) if self.current_jetphotos_url: - try: webbrowser.open_new_tab(self.current_jetphotos_url); 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}"); # messagebox.showerror("Link Error", f"Could not open link: {e}", parent=self) - else: logger.warning("No JetPhotos URL to open for current aircraft."); # messagebox.showinfo("No Link", "No registration found to search on JetPhotos.", parent=self) + try: + webbrowser.open_new_tab(self.current_jetphotos_url) + 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): - # ... (invariato) self.update_idletasks() - width = self.winfo_width(); height = self.winfo_height() - if width <= 1: width = 1000 - if height <= 1: height = 750 - x_screen = self.winfo_screenwidth(); y_screen = self.winfo_screenheight() - x = (x_screen // 2) - (width // 2); y = (y_screen // 2) - (height // 2) - if x < 0: x = 0 - if y < 0: y = 0 + width = self.winfo_width() + height = self.winfo_height() + if width <= 1: width = self.winfo_reqwidth() if self.winfo_reqwidth() > 1 else 1000 + if height <= 1: height = self.winfo_reqheight() if self.winfo_reqheight() > 1 else 750 + + x_screen = self.winfo_screenwidth() + 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 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): - logger.info(f"FullFlightDetailsWindow for {self.icao24} is closing.") + logger.info(f"FullFlightDetailsWindow for {self.icao24.upper()} is closing.") if self.detail_map_manager: - logger.info("Requesting shutdown for detail_map_manager worker.") - try: self.detail_map_manager.shutdown_worker() - except Exception as e: logger.error(f"Error during detail_map_manager worker shutdown: {e}", exc_info=True) + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Requesting shutdown for detail_map_manager worker.") + try: + 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() -# ... (blocco if __name__ == "__main__": invariato) if __name__ == "__main__": root_test = tk.Tk() root_test.title("Main Window (Test)") @@ -431,22 +491,42 @@ if __name__ == "__main__": "icao24": "TST001", "registration": "N-TEST", "manufacturername": "TestAircraft Co.", "model": "SkyTester Pro", "typecode": "TSPR", "operator": "Test Flight Ops", "built_year": 2022, "categorydescription": "Experimental Test Vehicle", "country": "Testland", "timestamp_metadata": time.time() - 3600 * 24 * 30, + "firstflightdate": "2022-01-15" } sample_live = { + "icao24": "tst001", #Lowercase per test "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", - "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 = [ - {"latitude": 45.0, "longitude": 9.0, "baro_altitude_m": 9000, "timestamp": time.time() - 300,}, - {"latitude": 45.1, "longitude": 9.1, "baro_altitude_m": 9500, "timestamp": time.time() - 180,}, - {"latitude": 45.2, "longitude": 9.2, "baro_altitude_m": 10000, "timestamp": time.time() - 60,}, + {"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, "on_ground": False}, + {"latitude": 45.2, "longitude": 9.2, "baro_altitude_m": 10000, "timestamp": time.time() - 60, "on_ground": False}, ] - def open_details_test(): - if hasattr(root_test, "details_win_instance") and root_test.details_win_instance.winfo_exists(): # type: ignore - root_test.details_win_instance.destroy() # type: ignore - details_window = FullFlightDetailsWindow(root_test, "TST001") - details_window.update_details(sample_static, sample_live, sample_track) - root_test.details_win_instance = details_window # type: ignore - ttk.Button(root_test, text="Open Full Details Test", command=open_details_test).pack(padx=50, pady=50) + sample_track_single_point = [ + {"latitude": 46.0, "longitude": 10.0, "baro_altitude_m": 8000, "timestamp": time.time() - 120, "on_ground": False} + ] + sample_live_only = { + "icao24": "tst002", "callsign": "LIVEONLY", "latitude": 45.5, "longitude": 9.5, "baro_altitude_m": 5000, + "timestamp": time.time()-10, "last_contact_timestamp": time.time()-10, "on_ground": False + } + + 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() \ No newline at end of file diff --git a/flightmonitor/map/map_canvas_manager.py b/flightmonitor/map/map_canvas_manager.py index 25c7265..daf7e36 100644 --- a/flightmonitor/map/map_canvas_manager.py +++ b/flightmonitor/map/map_canvas_manager.py @@ -1,58 +1,28 @@ -# flightmonitor/map/map_canvas_manager.py +# FlightMonitor/map/map_canvas_manager.py import tkinter as tk import math import time from typing import Optional, Tuple, List, Dict, Any from collections import deque -import queue # For Queue and Empty +import queue import threading -from . import map_utils +from . import map_utils # map_utils importa pyproj e mercantile import copy try: from PIL import Image, ImageTk, ImageDraw, ImageFont - PIL_IMAGE_LIB_AVAILABLE = True except ImportError: - Image, ImageTk, ImageDraw, ImageFont = None, None, None, None # type: ignore + Image, ImageTk, ImageDraw, ImageFont = None, None, None, None # type: ignore PIL_IMAGE_LIB_AVAILABLE = False import logging + logging.error("MapCanvasManager: Pillow (Image, ImageTk, ImageDraw, ImageFont) not found. Map disabled.") - logging.error( - "MapCanvasManager: Pillow (Image, ImageTk, ImageDraw, ImageFont) not found. Map disabled." - ) - -try: - import pyproj - - PYPROJ_MODULE_LOCALLY_AVAILABLE = True -except ImportError: - pyproj = None # type: ignore - PYPROJ_MODULE_LOCALLY_AVAILABLE = False - import logging - - logging.warning( - "MapCanvasManager: 'pyproj' not found. Geographic calculations impaired." - ) - -try: - import mercantile - - MERCANTILE_MODULE_LOCALLY_AVAILABLE = True -except ImportError: - mercantile = None # type: ignore - MERCANTILE_MODULE_LOCALLY_AVAILABLE = False - import logging - - logging.error( - "MapCanvasManager: 'mercantile' not found. Tile conversions fail, map unusable." - ) - +from .map_utils import PYPROJ_MODULE_LOCALLY_AVAILABLE, MERCANTILE_MODULE_LOCALLY_AVAILABLE from . import map_constants from ..data import config as app_config - from ..data.common_models import CanonicalFlightState from .map_services import BaseMapService, OpenStreetMapService from .map_tile_manager import MapTileManager @@ -66,15 +36,13 @@ from .map_utils import ( calculate_zoom_level_for_geographic_size, get_bounding_box_from_center_size, ) -from . import map_drawing # Importa il modulo map_drawing +from . import map_drawing try: from ..utils.logger import get_logger - logger = get_logger(__name__) except ImportError: import logging - logger = logging.getLogger(__name__) logger.warning("MapCanvasManager using fallback standard Python logger.") @@ -87,8 +55,8 @@ RESIZE_DEBOUNCE_DELAY_MS = 250 PAN_STEP_FRACTION = 0.25 DEFAULT_MAX_TRACK_POINTS = getattr( app_config, "DEFAULT_TRACK_HISTORY_POINTS", 20 -) # Legge da config -DEFAULT_MAX_TRACK_AGE_SECONDS = 300 # Puoi rendere anche questo configurabile se vuoi +) +DEFAULT_MAX_TRACK_AGE_SECONDS = 300 MAP_WORKER_QUEUE_TIMEOUT_S = 0.1 GUI_RESULT_POLL_INTERVAL_MS = 50 @@ -102,45 +70,43 @@ class MapCanvasManager: self, app_controller: Any, tk_canvas: tk.Canvas, - initial_bbox_dict: Dict[str, float], + initial_bbox_dict: Optional[Dict[str, float]], + is_detail_map: bool = False ): - logger.info(">>> MapCanvasManager __init__ STARTING <<<") + self.is_detail_map = is_detail_map + self.log_prefix = f"MCM (detail={self.is_detail_map})" # Store log_prefix + logger.info(f">>> {self.log_prefix} __init__ STARTING <<<") if ( not PIL_IMAGE_LIB_AVAILABLE or not MERCANTILE_MODULE_LOCALLY_AVAILABLE - or mercantile is None + or map_utils.mercantile is None # Check the imported module in map_utils ): - critical_msg = "MapCanvasManager critical dependencies missing: Pillow or Mercantile. Map disabled." + critical_msg = f"{self.log_prefix}: Critical dependencies missing: Pillow or Mercantile. Map disabled." logger.critical(critical_msg) - if app_controller and hasattr(app_controller, "show_error_message"): - try: - app_controller.show_error_message( - "Map Initialization Error", critical_msg - ) - except Exception: - pass + if app_controller and hasattr(app_controller, "main_window") and app_controller.main_window: + if hasattr(app_controller.main_window, "show_error_message"): + try: + app_controller.main_window.show_error_message( + "Map Initialization Error", critical_msg.replace(f"{self.log_prefix}: ", "") + ) + except Exception: pass raise ImportError(critical_msg) self.app_controller = app_controller self.canvas = tk_canvas self.canvas_width = self.canvas.winfo_width() - if self.canvas_width <= 1: - self.canvas_width = CANVAS_SIZE_HARD_FALLBACK_PX + if self.canvas_width <= 1: self.canvas_width = CANVAS_SIZE_HARD_FALLBACK_PX self.canvas_height = self.canvas.winfo_height() - if self.canvas_height <= 1: - self.canvas_height = getattr(app_config, "DEFAULT_CANVAS_HEIGHT", 600) - logger.info( - f"MCM __init__: Canvas dims {self.canvas_width}x{self.canvas_height}" - ) + if self.canvas_height <= 1: self.canvas_height = getattr(app_config, "DEFAULT_CANVAS_HEIGHT", 600) + + logger.info(f"{self.log_prefix} __init__: Canvas dims {self.canvas_width}x{self.canvas_height}") self._current_center_lat_gui: Optional[float] = None self._current_center_lon_gui: Optional[float] = None self._current_zoom_gui: int = map_constants.DEFAULT_INITIAL_ZOOM - self._current_map_geo_bounds_gui: Optional[ - Tuple[float, float, float, float] - ] = None + self._current_map_geo_bounds_gui: Optional[Tuple[float, float, float, float]] = None self._target_bbox_input_gui: Optional[Dict[str, float]] = None self._map_photo_image: Optional[ImageTk.PhotoImage] = None self._canvas_image_id: Optional[int] = None @@ -148,15 +114,13 @@ class MapCanvasManager: self.map_service: BaseMapService = OpenStreetMapService() self.tile_manager: MapTileManager = MapTileManager( map_service=self.map_service, - cache_root_directory=MAP_TILE_CACHE_DIR_HARD_FALLBACK, # Questo ora usa app_config + cache_root_directory=MAP_TILE_CACHE_DIR_HARD_FALLBACK, tile_pixel_size=self.map_service.tile_size, ) - logger.info(f"MCM __init__: MapTileManager initialized.") + logger.info(f"{self.log_prefix} __init__: MapTileManager initialized.") self._current_flights_to_display_gui: List[CanonicalFlightState] = [] - self.flight_tracks_gui: Dict[str, deque] = ( - {} - ) # Deque conterrà tuple (lat, lon, timestamp) - self.max_track_points: int = DEFAULT_MAX_TRACK_POINTS # Inizializzato da config + self.flight_tracks_gui: Dict[str, deque] = {} + self.max_track_points: int = DEFAULT_MAX_TRACK_POINTS self.max_track_age_seconds: float = DEFAULT_MAX_TRACK_AGE_SECONDS self._resize_debounce_job_id: Optional[str] = None self._map_render_request_queue: queue.Queue = queue.Queue(maxsize=5) @@ -166,80 +130,59 @@ class MapCanvasManager: self._gui_after_id_result_processor: Optional[str] = None self._last_render_request_id: int = 0 self._expected_render_id_gui: int = 0 - self._map_data_lock: threading.Lock = ( - threading.Lock() - ) # Lock per dati condivisi - logger.info("MCM __init__: All attributes initialized.") + self._map_data_lock: threading.Lock = threading.Lock() + logger.info(f"{self.log_prefix} __init__: All attributes initialized.") - logger.info("MCM __init__: Attempting to start map worker thread...") + logger.info(f"{self.log_prefix} __init__: Attempting to start map worker thread...") self._start_map_worker_thread() - logger.info(f"MCM __init__: Processing initial_bbox_dict: {initial_bbox_dict}") + logger.info(f"{self.log_prefix} __init__: Processing initial_bbox_dict: {initial_bbox_dict}") if initial_bbox_dict and _is_valid_bbox_dict(initial_bbox_dict): self._target_bbox_input_gui = initial_bbox_dict.copy() - logger.info( - f"MCM __init__: Valid initial_bbox_dict provided. Requesting render for bbox." - ) - self._request_map_render_for_bbox( - initial_bbox_dict, preserve_current_zoom_if_possible=False - ) - else: - logger.warning( - f"MCM __init__: Invalid or no initial_bbox_dict. Using default fallback view." - ) + logger.info(f"{self.log_prefix} __init__: Valid initial_bbox_dict provided. Requesting render for bbox.") + self._request_map_render_for_bbox(initial_bbox_dict, preserve_current_zoom_if_possible=False) + elif not self.is_detail_map: + logger.warning(f"{self.log_prefix} __init__ (MAIN MAP): Invalid or no initial_bbox_dict. Using default fallback view.") default_bbox_cfg = { - "lat_min": app_config.DEFAULT_BBOX_LAT_MIN, - "lon_min": app_config.DEFAULT_BBOX_LON_MIN, - "lat_max": app_config.DEFAULT_BBOX_LAT_MAX, - "lon_max": app_config.DEFAULT_BBOX_LON_MAX, + "lat_min": app_config.DEFAULT_BBOX_LAT_MIN, "lon_min": app_config.DEFAULT_BBOX_LON_MIN, + "lat_max": app_config.DEFAULT_BBOX_LAT_MAX, "lon_max": app_config.DEFAULT_BBOX_LON_MAX, } if _is_valid_bbox_dict(default_bbox_cfg): self._target_bbox_input_gui = default_bbox_cfg.copy() - logger.info( - f"MCM __init__: Using default config BBox. Requesting render for this bbox." - ) - self._request_map_render_for_bbox( - self._target_bbox_input_gui, preserve_current_zoom_if_possible=False - ) + logger.info(f"{self.log_prefix} __init__ (MAIN MAP): Using default config BBox. Requesting render.") + self._request_map_render_for_bbox(self._target_bbox_input_gui, preserve_current_zoom_if_possible=False) else: - logger.critical( - f"MCM __init__: Default fallback BBox from config is invalid: {default_bbox_cfg}. Map cannot initialize view with specific bbox." - ) + logger.critical(f"{self.log_prefix} __init__ (MAIN MAP): Default fallback BBox from config is invalid: {default_bbox_cfg}.") self._target_bbox_input_gui = None - self._current_center_lat_gui = getattr( - app_config, "DEFAULT_MAP_CENTER_LAT", 45.0 - ) - self._current_center_lon_gui = getattr( - app_config, "DEFAULT_MAP_CENTER_LON", 9.0 - ) + self._current_center_lat_gui = getattr(app_config, "DEFAULT_MAP_CENTER_LAT", 45.0) + self._current_center_lon_gui = getattr(app_config, "DEFAULT_MAP_CENTER_LON", 9.0) self._current_zoom_gui = map_constants.DEFAULT_INITIAL_ZOOM - logger.info( - f"MCM __init__: Using hardcoded default center/zoom. Requesting render." - ) - self._request_map_render( - self._current_center_lat_gui, - self._current_center_lon_gui, - self._current_zoom_gui, - ) + logger.info(f"{self.log_prefix} __init__ (MAIN MAP): Using hardcoded default center/zoom. Requesting render.") + self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui) + else: + logger.info(f"{self.log_prefix} __init__ (DETAIL MAP): No initial_bbox_dict. Map will be blank until track data is provided.") + self._display_placeholder_text("Detail Map\nAwaiting flight data...") - self._setup_event_bindings() - logger.info("MCM __init__: Event bindings set up.") + self._setup_event_bindings() # ORA QUESTO DOVREBBE FUNZIONARE + logger.info(f"{self.log_prefix} __init__: Event bindings set up.") - logger.info("MCM __init__: Attempting to start GUI result processing...") + logger.info(f"{self.log_prefix} __init__: Attempting to start GUI result processing...") self._start_gui_result_processing() - logger.info(">>> MapCanvasManager __init__ FINISHED <<<") + logger.info(f">>> {self.log_prefix} __init__ FINISHED <<<") def _start_map_worker_thread(self): if self._map_worker_thread is not None and self._map_worker_thread.is_alive(): - logger.warning("Map worker thread already running.") + logger.warning(f"{self.log_prefix} Map worker thread already running.") return self._map_worker_stop_event.clear() self._map_worker_thread = threading.Thread( - target=self._map_render_worker_target, name="MapRenderWorker", daemon=True + target=self._map_render_worker_target, + name=f"MapRenderWorker_detail_{self.is_detail_map}", + daemon=True ) self._map_worker_thread.start() - logger.info("MapRenderWorker thread started successfully.") + logger.info(f"{self.log_prefix} MapRenderWorker thread started successfully.") def _map_render_worker_target(self): worker_initial_settle_delay_seconds = 0.1 @@ -248,45 +191,28 @@ class MapCanvasManager: f"{thread_name}: Target loop initiated, starting initial settle delay ({worker_initial_settle_delay_seconds}s)..." ) - if self._map_worker_stop_event.wait( - timeout=worker_initial_settle_delay_seconds - ): - logger.info( - f"{thread_name}: Received stop signal during initial settle. Terminating worker target." - ) + if self._map_worker_stop_event.wait(timeout=worker_initial_settle_delay_seconds): + logger.info(f"{thread_name}: Received stop signal during initial settle. Terminating worker target.") return - logger.info( - f"{thread_name}: Initial settle delay complete. Worker entering main request loop." - ) + logger.info(f"{thread_name}: Initial settle delay complete. Worker entering main request loop.") while not self._map_worker_stop_event.is_set(): request_data = None request_id = -1 try: - logger.debug( - f"{thread_name}: Waiting for request from queue (timeout {MAP_WORKER_QUEUE_TIMEOUT_S}s)..." - ) - request_data = self._map_render_request_queue.get( - timeout=MAP_WORKER_QUEUE_TIMEOUT_S - ) + request_data = self._map_render_request_queue.get(timeout=MAP_WORKER_QUEUE_TIMEOUT_S) request_type = request_data.get("type") request_id = request_data.get("request_id", -1) - logger.info( - f"{thread_name}: Dequeued request. Type: '{request_type}', ID: {request_id}" - ) + logger.info(f"{thread_name}: Dequeued request. Type: '{request_type}', ID: {request_id}") if request_type == RENDER_REQUEST_TYPE_SHUTDOWN: - logger.info( - f"{thread_name}: Received RENDER_REQUEST_TYPE_SHUTDOWN. Exiting loop." - ) + logger.info(f"{thread_name}: Received RENDER_REQUEST_TYPE_SHUTDOWN. Exiting loop.") self._map_render_request_queue.task_done() break if request_type == RENDER_REQUEST_TYPE_MAP: - logger.info( - f"{thread_name}: Processing RENDER_REQUEST_TYPE_MAP for ID: {request_id}" - ) + logger.info(f"{thread_name}: Processing RENDER_REQUEST_TYPE_MAP for ID: {request_id}") center_lat = request_data.get("center_lat") center_lon = request_data.get("center_lon") zoom = request_data.get("zoom") @@ -294,107 +220,52 @@ class MapCanvasManager: canvas_h = request_data.get("canvas_height") if None in [center_lat, center_lon, zoom, canvas_w, canvas_h]: - logger.error( - f"{thread_name}: Missing critical parameters in request ID {request_id}. Aborting. Data: {request_data}" - ) - error_payload = { - "request_id": request_id, - "photo_image": None, - "map_geo_bounds": None, - "error": "Worker: Missing critical render parameters.", - } + logger.error(f"{thread_name}: Missing critical parameters in request ID {request_id}. Aborting. Data: {request_data}") + error_payload = {"request_id": request_id, "photo_image": None, "map_geo_bounds": None, "error": "Worker: Missing critical render parameters."} self._map_render_result_queue.put(error_payload) self._map_render_request_queue.task_done() continue - - logger.debug( - f"{thread_name}: Parameters for ID {request_id} - Center:({center_lat:.3f},{center_lon:.3f}), Z:{zoom}, Canvas:{canvas_w}x{canvas_h}" - ) - + target_bbox = request_data.get("target_bbox") - # flights_to_draw e tracks_to_draw vengono passati come copie flights_to_draw = request_data.get("flights", []) - tracks_to_draw = request_data.get( - "tracks", {} - ) # Questa è una dict[str, deque] - max_track_pts_from_req = request_data.get( - "max_track_points", DEFAULT_MAX_TRACK_POINTS - ) + tracks_to_draw = request_data.get("tracks", {}) + max_track_pts_from_req = request_data.get("max_track_points", DEFAULT_MAX_TRACK_POINTS) - photo_image_result, actual_map_bounds, error_message = ( - self._execute_render_pipeline( - center_lat, - center_lon, - zoom, - canvas_w, - canvas_h, - target_bbox, - flights_to_draw, - tracks_to_draw, - max_track_pts_from_req, - ) + photo_image_result, actual_map_bounds, error_message = self._execute_render_pipeline( + center_lat, center_lon, zoom, canvas_w, canvas_h, + target_bbox, flights_to_draw, tracks_to_draw, max_track_pts_from_req ) if self._map_worker_stop_event.is_set(): - logger.info( - f"{thread_name}: Stop event set after render pipeline for ID {request_id}. Discarding result." - ) - break + logger.info(f"{thread_name}: Stop event set after render pipeline for ID {request_id}. Discarding result.") + break result_payload = { - "request_id": request_id, - "photo_image": photo_image_result, - "map_geo_bounds": actual_map_bounds, - "error": error_message, + "request_id": request_id, "photo_image": photo_image_result, + "map_geo_bounds": actual_map_bounds, "error": error_message, } - logger.debug( - f"{thread_name}: Attempting to put result for ID {request_id} into queue (Queue size: {self._map_render_result_queue.qsize()})." - ) self._map_render_result_queue.put(result_payload) - logger.info( - f"{thread_name}: Successfully put result for ID {request_id} into queue." - ) + logger.info(f"{thread_name}: Successfully put result for ID {request_id} into queue.") else: - logger.warning( - f"{thread_name}: Received unknown request type '{request_type}' for ID {request_id}." - ) - + logger.warning(f"{thread_name}: Received unknown request type '{request_type}' for ID {request_id}.") + self._map_render_request_queue.task_done() except queue.Empty: - logger.debug( - f"{thread_name}: Request queue empty, looping to check stop event." - ) continue except Exception as e: - logger.exception( - f"{thread_name}: Unhandled exception in worker loop for request ID {request_id if request_data else 'N/A'}: {e}" - ) + logger.exception(f"{thread_name}: Unhandled exception in worker loop for request ID {request_id if request_data else 'N/A'}: {e}") if request_data and request_id != -1: - error_payload_exc = { - "request_id": request_id, - "photo_image": None, - "map_geo_bounds": None, - "error": f"Worker loop unhandled exception: {type(e).__name__}", - } + error_payload_exc = {"request_id": request_id, "photo_image": None, "map_geo_bounds": None, "error": f"Worker loop unhandled exception: {type(e).__name__}"} try: self._map_render_result_queue.put_nowait(error_payload_exc) - logger.info( - f"{thread_name}: Reported unhandled exception for ID {request_id} to result queue." - ) except queue.Full: - logger.error( - f"{thread_name}: Result queue full while trying to report worker unhandled exception." - ) + logger.error(f"{thread_name}: Result queue full while trying to report worker unhandled exception.") except Exception as e_put_err: - logger.error( - f"{thread_name}: Error putting unhandled exception report to result queue: {e_put_err}" - ) + logger.error(f"{thread_name}: Error putting unhandled exception report to result queue: {e_put_err}") if request_data: - try: - self._map_render_request_queue.task_done() - except ValueError: - pass + try: self._map_render_request_queue.task_done() + except ValueError: pass time.sleep(0.5) logger.info(f"{thread_name}: Worker thread target loop finished.") @@ -404,18 +275,14 @@ class MapCanvasManager: if self.canvas.winfo_exists(): self.canvas.after_cancel(self._gui_after_id_result_processor) except Exception as e_cancel: - logger.warning( - f"Error cancelling previous result processor: {e_cancel}" - ) + logger.warning(f"{self.log_prefix} Error cancelling previous result processor: {e_cancel}") if self.canvas.winfo_exists(): self._gui_after_id_result_processor = self.canvas.after( GUI_RESULT_POLL_INTERVAL_MS, self._process_map_render_results ) - logger.debug("GUI result processing scheduled.") + logger.debug(f"{self.log_prefix} GUI result processing scheduled.") else: - logger.warning( - "Canvas does not exist, cannot schedule GUI result processing." - ) + logger.warning(f"{self.log_prefix} Canvas does not exist, cannot schedule GUI result processing.") def _execute_render_pipeline( self, @@ -426,205 +293,133 @@ class MapCanvasManager: canvas_h: int, target_bbox_input: Optional[Dict[str, float]], current_flights_to_display: List[CanonicalFlightState], - flight_tracks: Dict[ - str, deque - ], # flight_tracks è Dict[str, deque[Tuple[float,float,float]]] - max_track_points_config: int, # Max track points dalla configurazione/GUI + flight_tracks: Dict[str, deque], + max_track_points_config: int, ) -> Tuple[ Optional[ImageTk.PhotoImage], Optional[Tuple[float, float, float, float]], Optional[str], ]: - logger.debug( - f"WorkerPipeline: Starting for Z{zoom_level}, Center ({center_lat:.4f},{center_lon:.4f}), Canvas {canvas_w}x{canvas_h}" - ) - if ( - not PIL_IMAGE_LIB_AVAILABLE - or not MERCANTILE_MODULE_LOCALLY_AVAILABLE - or mercantile is None - or Image is None - or ImageDraw is None - or ImageTk is None - ): - err_msg = "WorkerPipeline: Pillow/Mercantile/ImageTk dependencies missing." + log_prefix_pipeline = f"{self.log_prefix} WorkerPipeline" # Usa il log_prefix dell'istanza + logger.debug(f"{self.log_prefix} WorkerPipeline: Starting for Z{zoom_level}, Center ({center_lat:.4f},{center_lon:.4f}), Canvas {canvas_w}x{canvas_h}") + + + # >>> INIZIO BLOCCO DEBUG TRACCIA <<< + if self.is_detail_map and current_flights_to_display: + detail_icao = current_flights_to_display[0].icao24 # Assumiamo che ci sia un solo volo per la mappa dettaglio + track_for_icao_in_worker = flight_tracks.get(detail_icao) + logger.info(f"{log_prefix_pipeline}: DETAIL MAP for ICAO {detail_icao}.") + logger.info(f"{log_prefix_pipeline}: - current_flights_to_display (live point): {current_flights_to_display}") + if track_for_icao_in_worker: + logger.info(f"{log_prefix_pipeline}: - Track from flight_tracks (deque length): {len(track_for_icao_in_worker)}") + # Stampa i primi e ultimi punti della deque per un controllo + # if len(track_for_icao_in_worker) > 0: + # logger.info(f"{log_prefix_pipeline}: Track head: {list(track_for_icao_in_worker)[0]}") + # if len(track_for_icao_in_worker) > 1: + # logger.info(f"{log_prefix_pipeline}: Track tail: {list(track_for_icao_in_worker)[-1]}") + else: + logger.info(f"{log_prefix_pipeline}: - No track found in flight_tracks for ICAO {detail_icao}.") + # >>> FINE BLOCCO DEBUG TRACCIA <<< + + if (not PIL_IMAGE_LIB_AVAILABLE or not MERCANTILE_MODULE_LOCALLY_AVAILABLE or + map_utils.mercantile is None or Image is None or ImageDraw is None or ImageTk is None): + err_msg = f"{self.log_prefix} WorkerPipeline: Pillow/Mercantile/ImageTk dependencies missing." logger.error(err_msg) return None, None, err_msg if canvas_w <= 0 or canvas_h <= 0: - err_msg = ( - f"WorkerPipeline: Invalid canvas dimensions ({canvas_w}x{canvas_h})." - ) + err_msg = f"{self.log_prefix} WorkerPipeline: Invalid canvas dimensions ({canvas_w}x{canvas_h})." logger.error(err_msg) return None, None, err_msg canvas_geo_bbox = calculate_geographic_bbox_from_pixel_size_and_zoom( - center_lat, - center_lon, - canvas_w, - canvas_h, - zoom_level, - self.tile_manager.tile_size, + center_lat, center_lon, canvas_w, canvas_h, zoom_level, self.tile_manager.tile_size ) if not canvas_geo_bbox: - err_msg = "WorkerPipeline: Failed to calculate canvas geographic BBox." + err_msg = f"{self.log_prefix} WorkerPipeline: Failed to calculate canvas geographic BBox." logger.error(err_msg) return None, None, err_msg - logger.debug(f"WorkerPipeline: Calculated canvas_geo_bbox: {canvas_geo_bbox}") - + tile_xy_ranges = get_tile_ranges_for_bbox(canvas_geo_bbox, zoom_level) if not tile_xy_ranges: - err_msg = f"WorkerPipeline: Failed to get tile ranges for {canvas_geo_bbox} at Z{zoom_level}." + err_msg = f"{self.log_prefix} WorkerPipeline: Failed to get tile ranges for {canvas_geo_bbox} at Z{zoom_level}." logger.error(err_msg) try: - placeholder_img = Image.new( - "RGB", - (canvas_w, canvas_h), - map_constants.DEFAULT_PLACEHOLDER_COLOR_RGB, - ) + placeholder_img = Image.new("RGB", (canvas_w, canvas_h), map_constants.DEFAULT_PLACEHOLDER_COLOR_RGB) draw = ImageDraw.Draw(placeholder_img) - map_drawing._draw_text_on_placeholder( - draw, placeholder_img.size, err_msg.replace("WorkerPipeline: ", "") - ) - if ImageTk: - return ImageTk.PhotoImage(placeholder_img), None, err_msg - else: - return ( - None, - None, - "WorkerPipeline: ImageTk unavailable for placeholder.", - ) + map_drawing._draw_text_on_placeholder(draw, placeholder_img.size, err_msg.replace(f"{self.log_prefix} WorkerPipeline: ", "")) + if ImageTk: return ImageTk.PhotoImage(placeholder_img), None, err_msg + else: return None, None, f"{self.log_prefix} WorkerPipeline: ImageTk unavailable for placeholder." except Exception as e_ph: - err_msg_ph = f"WorkerPipeline: Tile range error AND placeholder creation failed: {e_ph}" - logger.error(err_msg_ph) - return None, None, err_msg_ph + return None, None, f"{self.log_prefix} WorkerPipeline: Tile range error AND placeholder creation failed: {e_ph}" - logger.debug(f"WorkerPipeline: Calculated tile_xy_ranges: {tile_xy_ranges}") - - stitched_map_pil = self.tile_manager.stitch_map_image( - zoom_level, tile_xy_ranges[0], tile_xy_ranges[1] - ) + stitched_map_pil = self.tile_manager.stitch_map_image(zoom_level, tile_xy_ranges[0], tile_xy_ranges[1]) if not stitched_map_pil: - err_msg = "WorkerPipeline: Failed to stitch map image from TileManager." + err_msg = f"{self.log_prefix} WorkerPipeline: Failed to stitch map image from TileManager." logger.error(err_msg) return None, None, err_msg - logger.debug( - f"WorkerPipeline: Successfully stitched base map image {stitched_map_pil.size}." - ) - actual_stitched_map_geo_bounds = self.tile_manager._get_bounds_for_tile_range( - zoom_level, tile_xy_ranges - ) + actual_stitched_map_geo_bounds = self.tile_manager._get_bounds_for_tile_range(zoom_level, tile_xy_ranges) if not actual_stitched_map_geo_bounds: - logger.warning( - "WorkerPipeline: Could not determine actual stitched map geo bounds. Using canvas_geo_bbox as fallback." - ) + logger.warning(f"{self.log_prefix} WorkerPipeline: Could not determine actual stitched map geo bounds. Using canvas_geo_bbox as fallback.") actual_stitched_map_geo_bounds = canvas_geo_bbox - logger.debug( - f"WorkerPipeline: Actual stitched map geo bounds: {actual_stitched_map_geo_bounds}" - ) - - if stitched_map_pil.mode != "RGBA": - image_to_draw_on = stitched_map_pil.convert("RGBA") - else: - image_to_draw_on = stitched_map_pil.copy() - img_shape = image_to_draw_on.size # (width, height) + + if stitched_map_pil.mode != "RGBA": image_to_draw_on = stitched_map_pil.convert("RGBA") + else: image_to_draw_on = stitched_map_pil.copy() + img_shape = image_to_draw_on.size draw = ImageDraw.Draw(image_to_draw_on) - logger.debug( - f"WorkerPipeline: Prepared image for drawing overlays, size {img_shape}." - ) if target_bbox_input and _is_valid_bbox_dict(target_bbox_input): - bbox_wesn = ( - target_bbox_input["lon_min"], - target_bbox_input["lat_min"], - target_bbox_input["lon_max"], - target_bbox_input["lat_max"], - ) + bbox_wesn = (target_bbox_input["lon_min"], target_bbox_input["lat_min"], target_bbox_input["lon_max"], target_bbox_input["lat_max"]) try: - map_drawing.draw_area_bounding_box( # Usa la funzione da map_drawing - image_to_draw_on, - bbox_wesn, - actual_stitched_map_geo_bounds, - img_shape, - color=map_constants.AREA_BOUNDARY_COLOR, - thickness=map_constants.AREA_BOUNDARY_THICKNESS_PX, + map_drawing.draw_area_bounding_box( + image_to_draw_on, bbox_wesn, actual_stitched_map_geo_bounds, img_shape, + color=map_constants.AREA_BOUNDARY_COLOR, thickness=map_constants.AREA_BOUNDARY_THICKNESS_PX ) - logger.debug(f"WorkerPipeline: Drew target BBox.") except Exception as e_bbox_draw_pipe: - logger.error( - f"WorkerPipeline: Error drawing target BBox: {e_bbox_draw_pipe}", - exc_info=False, - ) + logger.error(f"{self.log_prefix} WorkerPipeline: Error drawing target BBox: {e_bbox_draw_pipe}", exc_info=False) - # Disegna voli e le loro tracce if current_flights_to_display: - font_size = map_constants.DEM_TILE_LABEL_BASE_FONT_SIZE + ( - zoom_level - map_constants.DEM_TILE_LABEL_BASE_ZOOM - ) - label_font = map_drawing._load_label_font( - font_size - ) # Usa la funzione da map_drawing + font_size = map_constants.DEM_TILE_LABEL_BASE_FONT_SIZE + (zoom_level - map_constants.DEM_TILE_LABEL_BASE_ZOOM) + label_font = map_drawing._load_label_font(font_size) flights_drawn_count = 0 for flight in current_flights_to_display: if flight.latitude is not None and flight.longitude is not None: - pixel_coords_f = map_drawing._geo_to_pixel_on_unscaled_map( # Usa la funzione da map_drawing - flight.latitude, - flight.longitude, - actual_stitched_map_geo_bounds, - img_shape, + pixel_coords_f = map_drawing._geo_to_pixel_on_unscaled_map( + flight.latitude, flight.longitude, actual_stitched_map_geo_bounds, img_shape ) if pixel_coords_f: try: - current_flight_track_deque = flight_tracks.get( - flight.icao24 - ) # Prende la deque dal dict passato - map_drawing._draw_single_flight( # Usa la funzione da map_drawing - draw, - pixel_coords_f, - flight, - label_font, + current_flight_track_deque = flight_tracks.get(flight.icao24) + map_drawing._draw_single_flight( + draw, pixel_coords_f, flight, label_font, track_deque=current_flight_track_deque, current_map_geo_bounds=actual_stitched_map_geo_bounds, current_stitched_map_pixel_shape=img_shape, - # max_track_points_config e track_line_width sono ora letti globalmente in map_drawing ) flights_drawn_count += 1 except Exception as e_flight_draw_pipe: - logger.error( - f"WorkerPipeline: Error drawing flight {flight.icao24}: {e_flight_draw_pipe}", - exc_info=False, - ) - logger.debug( - f"WorkerPipeline: Drew {flights_drawn_count} flight markers and tracks." - ) + logger.error(f"{self.log_prefix} WorkerPipeline: Error drawing flight {flight.icao24}: {e_flight_draw_pipe}", exc_info=False) + logger.debug(f"{self.log_prefix} WorkerPipeline: Drew {flights_drawn_count} flight markers and tracks.") try: if ImageTk: final_photo_image = ImageTk.PhotoImage(image_to_draw_on) - logger.debug(f"WorkerPipeline: Successfully created PhotoImage.") + logger.debug(f"{self.log_prefix} WorkerPipeline: Successfully created PhotoImage.") return final_photo_image, actual_stitched_map_geo_bounds, None else: - return ( - None, - actual_stitched_map_geo_bounds, - "WorkerPipeline: ImageTk module not available.", - ) + return None, actual_stitched_map_geo_bounds, f"{self.log_prefix} WorkerPipeline: ImageTk module not available." except Exception as e_photo_pipe: - err_msg_photo = ( - f"WorkerPipeline: Failed to create PhotoImage: {e_photo_pipe}" - ) + err_msg_photo = f"{self.log_prefix} WorkerPipeline: Failed to create PhotoImage: {e_photo_pipe}" logger.error(err_msg_photo, exc_info=True) return None, actual_stitched_map_geo_bounds, err_msg_photo def _process_map_render_results(self): if not self.canvas.winfo_exists(): - logger.info("Canvas destroyed, stopping map render result processing.") + logger.info(f"{self.log_prefix} Canvas destroyed, stopping map render result processing.") self._gui_after_id_result_processor = None return - logger.debug( - f"GUI ResultsProcessor: Checking result queue. Expected ID >= {self._expected_render_id_gui}. Queue size: {self._map_render_result_queue.qsize()}" - ) processed_one = False try: while not self._map_render_result_queue.empty(): @@ -635,76 +430,39 @@ class MapCanvasManager: rendered_map_bounds = result_data.get("map_geo_bounds") error_message = result_data.get("error") - logger.info( - f"GUI ResultsProcessor: Dequeued result for ReqID {request_id}. Error: '{error_message}', Img Valid: {photo_image is not None}, Expected GUI ID: {self._expected_render_id_gui}" - ) + logger.info(f"{self.log_prefix} GUI ResultsProcessor: Dequeued result for ReqID {request_id}. Error: '{error_message}', Img Valid: {photo_image is not None}, Expected GUI ID: {self._expected_render_id_gui}") if request_id < self._expected_render_id_gui: - logger.warning( - f"GUI ResultsProcessor: Discarding STALE map render result ID {request_id} (expected >= {self._expected_render_id_gui})." - ) + logger.warning(f"{self.log_prefix} GUI ResultsProcessor: Discarding STALE map render result ID {request_id} (expected >= {self._expected_render_id_gui}).") self._map_render_result_queue.task_done() continue if error_message: - logger.error( - f"GUI ResultsProcessor: Received error from MapRenderWorker (ReqID {request_id}): {error_message}" - ) - self._display_placeholder_text( - f"Map Error (Worker):\n{error_message[:100]}" - ) - elif ( - photo_image - and ImageTk - and isinstance(photo_image, ImageTk.PhotoImage) - ): - logger.info( - f"GUI ResultsProcessor: Applying new map image from worker for ReqID {request_id}." - ) + logger.error(f"{self.log_prefix} GUI ResultsProcessor: Received error from Worker (ReqID {request_id}): {error_message}") + self._display_placeholder_text(f"Map Error (Worker):\n{error_message[:100]}") + elif photo_image and ImageTk and isinstance(photo_image, ImageTk.PhotoImage): + logger.info(f"{self.log_prefix} GUI ResultsProcessor: Applying new map image from worker for ReqID {request_id}.") self._clear_canvas_display_elements() self._map_photo_image = photo_image - self._canvas_image_id = self.canvas.create_image( - 0, 0, anchor=tk.NW, image=self._map_photo_image - ) + self._canvas_image_id = self.canvas.create_image(0, 0, anchor=tk.NW, image=self._map_photo_image) self._current_map_geo_bounds_gui = rendered_map_bounds - logger.debug( - f"GUI ResultsProcessor: Canvas updated with image for ReqID {request_id}. New map bounds: {self._current_map_geo_bounds_gui}" - ) else: - logger.warning( - f"GUI ResultsProcessor: Received invalid/empty result from worker for ReqID {request_id}. No PhotoImage. Error was: '{error_message}'" - ) - self._display_placeholder_text( - f"Map Update Failed\n(No Image Data for ReqID {request_id})" - ) - + logger.warning(f"{self.log_prefix} GUI ResultsProcessor: Received invalid/empty result from worker for ReqID {request_id}. Error: '{error_message}'") + self._display_placeholder_text(f"Map Update Failed\n(No Image Data for ReqID {request_id})") + self._map_render_result_queue.task_done() - if self.app_controller and hasattr( - self.app_controller, "update_general_map_info" - ): - logger.debug( - f"GUI ResultsProcessor: Requesting controller to update general map info after processing ReqID {request_id}." - ) - self.app_controller.update_general_map_info() - - except queue.Empty: - logger.debug("GUI ResultsProcessor: Result queue is empty.") - pass + if not self.is_detail_map: + if self.app_controller and hasattr(self.app_controller, "update_general_map_info"): + logger.debug(f"{self.log_prefix} GUI ResultsProcessor (MAIN MAP): Requesting controller to update general map info after ReqID {request_id}.") + self.app_controller.update_general_map_info() + + except queue.Empty: pass except Exception as e: - logger.exception( - f"GUI ResultsProcessor: Error processing map render results: {e}" - ) - + logger.exception(f"{self.log_prefix} GUI ResultsProcessor: Error processing map render results: {e}") + if self.canvas.winfo_exists(): - self._gui_after_id_result_processor = self.canvas.after( - GUI_RESULT_POLL_INTERVAL_MS, self._process_map_render_results - ) - logger.debug( - f"GUI ResultsProcessor: Rescheduled itself. Processed one this cycle: {processed_one}" - ) - else: - logger.info("GUI ResultsProcessor: Canvas gone, not rescheduling.") + self._gui_after_id_result_processor = self.canvas.after(GUI_RESULT_POLL_INTERVAL_MS, self._process_map_render_results) def _request_map_render( self, @@ -713,15 +471,10 @@ class MapCanvasManager: zoom_level: int, ensure_bbox_is_covered_dict: Optional[Dict[str, float]] = None, ): - logger.debug( - f"GUI _request_map_render: Input center=({center_lat:.4f},{center_lon:.4f}), zoom={zoom_level}" - ) - if ( - self._map_worker_stop_event.is_set() - or not self._map_worker_thread - or not self._map_worker_thread.is_alive() - ): - logger.warning("Map worker not running. Cannot queue render request.") + logger.debug(f"{self.log_prefix} GUI _request_map_render: Input center=({center_lat:.4f},{center_lon:.4f}), zoom={zoom_level}") + + if (self._map_worker_stop_event.is_set() or not self._map_worker_thread or not self._map_worker_thread.is_alive()): + logger.warning(f"{self.log_prefix} Map worker not running. Cannot queue render request.") self._display_placeholder_text("Map Worker Offline") return @@ -729,103 +482,59 @@ class MapCanvasManager: self._last_render_request_id += 1 current_request_id = self._last_render_request_id self._expected_render_id_gui = current_request_id - logger.info( - f"GUI _request_map_render: New request ID {current_request_id}. Expected GUI ID set to {self._expected_render_id_gui}." - ) - - # Copia i dati condivisi sotto lock + logger.info(f"{self.log_prefix} GUI _request_map_render: New request ID {current_request_id}. Expected GUI ID set to {self._expected_render_id_gui}.") flights_copy = [] - try: - flights_copy = copy.deepcopy(self._current_flights_to_display_gui) - except Exception as e_copy_f: - logger.error( - f"Error deepcopying flights for render request: {e_copy_f}" - ) + try: flights_copy = copy.deepcopy(self._current_flights_to_display_gui) + except Exception as e: logger.error(f"{self.log_prefix} Error deepcopying flights for render: {e}") + + # >>> NUOVO BLOCCO DEBUG <<< + if self.is_detail_map and flights_copy: + current_detail_icao = flights_copy[0].icao24 + logger.info(f"{self.log_prefix} _request_map_render: BEFORE deepcopy, content of self.flight_tracks_gui for ICAO {current_detail_icao}: {self.flight_tracks_gui.get(current_detail_icao)}") + # >>> FINE NUOVO BLOCCO DEBUG <<< tracks_copy = {} try: - tracks_copy = copy.deepcopy( - self.flight_tracks_gui - ) # Deepcopy delle code + tracks_copy = copy.deepcopy(self.flight_tracks_gui) + # >>> NUOVO BLOCCO DEBUG <<< + if self.is_detail_map and flights_copy: + current_detail_icao = flights_copy[0].icao24 + logger.info(f"{self.log_prefix} _request_map_render: AFTER deepcopy, content of tracks_copy for ICAO {current_detail_icao}: {tracks_copy.get(current_detail_icao)}") + # >>> FINE NUOVO BLOCCO DEBUG <<< except Exception as e_copy_t: - logger.error(f"Error deepcopying tracks for render request: {e_copy_t}") + logger.error(f"{self.log_prefix} Error deepcopying tracks for render: {e_copy_t}") target_bbox_to_send = None if ensure_bbox_is_covered_dict: - try: - target_bbox_to_send = copy.deepcopy(ensure_bbox_is_covered_dict) - except Exception as e_copy_b_ensure: - logger.error( - f"Error deepcopying ensure_bbox for render request: {e_copy_b_ensure}" - ) + try: target_bbox_to_send = copy.deepcopy(ensure_bbox_is_covered_dict) + except Exception as e: logger.error(f"{self.log_prefix} Error deepcopying ensure_bbox for render: {e}") elif self._target_bbox_input_gui: - try: - target_bbox_to_send = copy.deepcopy(self._target_bbox_input_gui) - except Exception as e_copy_b_target: - logger.error( - f"Error deepcopying target_bbox_input for render request: {e_copy_b_target}" - ) + try: target_bbox_to_send = copy.deepcopy(self._target_bbox_input_gui) + except Exception as e: logger.error(f"{self.log_prefix} Error deepcopying target_bbox_input for render: {e}") request_payload = { - "type": RENDER_REQUEST_TYPE_MAP, - "request_id": current_request_id, - "center_lat": center_lat, - "center_lon": center_lon, - "zoom": zoom_level, - "canvas_width": self.canvas_width, - "canvas_height": self.canvas_height, - "target_bbox": target_bbox_to_send, - "flights": flights_copy, - "tracks": tracks_copy, - "max_track_points": self.max_track_points, + "type": RENDER_REQUEST_TYPE_MAP, "request_id": current_request_id, + "center_lat": center_lat, "center_lon": center_lon, "zoom": zoom_level, + "canvas_width": self.canvas_width, "canvas_height": self.canvas_height, + "target_bbox": target_bbox_to_send, "flights": flights_copy, + "tracks": tracks_copy, "max_track_points": self.max_track_points, } - logger.debug( - f"GUI _request_map_render: Assembled payload for ReqID {current_request_id}: Center=({request_payload['center_lat']:.3f},{request_payload['center_lon']:.3f}), Z={request_payload['zoom']}, Canvas={request_payload['canvas_width']}x{request_payload['canvas_height']}, TargetBBox provided: {request_payload['target_bbox'] is not None}, NumFlights: {len(flights_copy)}, NumTracks: {len(tracks_copy)}" - ) - + try: self._clear_canvas_display_elements() loading_text = f"Loading Map...\nZ{zoom_level} @ ({center_lat:.2f}, {center_lon:.2f})\nReqID: {current_request_id}" - logger.debug( - f"GUI _request_map_render: Displaying placeholder: '{loading_text}'" - ) self._display_placeholder_text(loading_text) - logger.debug( - f"GUI _request_map_render: Attempting to put ReqID {current_request_id} in request queue (current qsize: {self._map_render_request_queue.qsize()})." - ) self._map_render_request_queue.put_nowait(request_payload) - logger.info( - f"GUI _request_map_render: Successfully queued map render request ID {current_request_id} (Z{zoom_level})." - ) + logger.info(f"{self.log_prefix} GUI _request_map_render: Successfully queued map render request ID {current_request_id} (Z{zoom_level}).") self._current_center_lat_gui = center_lat self._current_center_lon_gui = center_lon self._current_zoom_gui = zoom_level - logger.debug( - f"GUI _request_map_render: Updated _gui state vars: center=({self._current_center_lat_gui},{self._current_center_lon_gui}), zoom={self._current_zoom_gui}" - ) except queue.Full: - logger.warning( - f"Map render request queue FULL. Request ID {current_request_id} was DROPPED." - ) - with self._map_data_lock: - if ( - self._expected_render_id_gui == current_request_id - ): # Solo se era l'ultimo atteso - # Se la coda è piena, non possiamo garantire quale richiesta verrà elaborata. - # Non decrementare _expected_render_id_gui qui, perché la GUI si aspetta - # ancora questa richiesta. Il worker scarterà le richieste precedenti se ne arriva una nuova. - # Il problema è solo se la *coda* è piena. - pass # Non modificare _expected_render_id_gui, potrebbe essere ancora valido. - self._display_placeholder_text( - "Map Busy / Request Queue Full.\nPlease Try Action Again." - ) + logger.warning(f"{self.log_prefix} Map render request queue FULL. Request ID {current_request_id} was DROPPED.") + self._display_placeholder_text("Map Busy / Request Queue Full.\nPlease Try Action Again.") except Exception as e: - logger.exception( - f"GUI _request_map_render: Error queuing map render request ID {current_request_id}: {e}" - ) - self._display_placeholder_text( - f"Error Queuing Map Request:\n{type(e).__name__}" - ) + logger.exception(f"{self.log_prefix} GUI _request_map_render: Error queuing map render request ID {current_request_id}: {e}") + self._display_placeholder_text(f"Error Queuing Map Request:\n{type(e).__name__}") def _setup_event_bindings(self): self.canvas.bind("", self._on_canvas_resize) @@ -835,1229 +544,502 @@ class MapCanvasManager: self._drag_start_x_canvas: Optional[int] = None self._drag_start_y_canvas: Optional[int] = None self._is_left_button_pressed: bool = False - logger.debug("MCM Event bindings set up.") + logger.debug(f"{self.log_prefix} Event bindings set up.") def _on_canvas_resize(self, event: tk.Event): new_width, new_height = event.width, event.height - logger.debug( - f"MCM _on_canvas_resize: Event triggered with new_width={new_width}, new_height={new_height}. Current canvas_width={self.canvas_width}, canvas_height={self.canvas_height}" - ) - if ( - new_width > 1 - and new_height > 1 - and (self.canvas_width != new_width or self.canvas_height != new_height) - ): - logger.info( - f"MCM _on_canvas_resize: Canvas dimensions changed from {self.canvas_width}x{self.canvas_height} to {new_width}x{new_height}. Debouncing redraw." - ) + if (new_width > 1 and new_height > 1 and + (self.canvas_width != new_width or self.canvas_height != new_height)): + logger.info(f"{self.log_prefix} _on_canvas_resize: Canvas dimensions changed from {self.canvas_width}x{self.canvas_height} to {new_width}x{new_height}. Debouncing redraw.") if self._resize_debounce_job_id: try: - if self.canvas.winfo_exists(): - self.canvas.after_cancel(self._resize_debounce_job_id) - logger.debug( - f"MCM _on_canvas_resize: Cancelled previous debounce job: {self._resize_debounce_job_id}" - ) - except Exception as e_cancel_resize: - logger.warning( - f"MCM _on_canvas_resize: Error cancelling previous resize job: {e_cancel_resize}" - ) + if self.canvas.winfo_exists(): self.canvas.after_cancel(self._resize_debounce_job_id) + except Exception: pass if self.canvas.winfo_exists(): - self._resize_debounce_job_id = self.canvas.after( - RESIZE_DEBOUNCE_DELAY_MS, - self._perform_resize_redraw, - new_width, - new_height, - ) - logger.debug( - f"MCM _on_canvas_resize: Scheduled new debounce job: {self._resize_debounce_job_id} for {new_width}x{new_height}" - ) - else: - logger.warning( - "MCM _on_canvas_resize: Canvas does not exist, cannot schedule resize redraw." - ) - else: - logger.debug( - f"MCM _on_canvas_resize: Ignoring resize event (no change or invalid new dims: {new_width}x{new_height})." - ) + self._resize_debounce_job_id = self.canvas.after(RESIZE_DEBOUNCE_DELAY_MS, self._perform_resize_redraw, new_width, new_height) def _perform_resize_redraw(self, width: int, height: int): self._resize_debounce_job_id = None if not self.canvas.winfo_exists(): - logger.warning( - "MCM _perform_resize_redraw: Canvas does not exist. Aborting." - ) + logger.warning(f"{self.log_prefix} _perform_resize_redraw: Canvas does not exist. Aborting.") return - logger.info( - f"MCM _perform_resize_redraw: Executing debounced resize to {width}x{height}. Requesting new render." - ) + logger.info(f"{self.log_prefix} _perform_resize_redraw: Executing debounced resize to {width}x{height}. Requesting new render.") self.canvas_width, self.canvas_height = width, height - logger.debug( - f"MCM _perform_resize_redraw: Updated canvas_width={self.canvas_width}, canvas_height={self.canvas_height}" - ) - if ( - self._current_center_lat_gui is not None - and self._current_center_lon_gui is not None - and self._current_zoom_gui is not None - ): - logger.debug( - f"MCM _perform_resize_redraw: Requesting render with current GUI state: center=({self._current_center_lat_gui},{self._current_center_lon_gui}), zoom={self._current_zoom_gui}" - ) + if (self._current_center_lat_gui is not None and + self._current_center_lon_gui is not None and + self._current_zoom_gui is not None): self._request_map_render( - self._current_center_lat_gui, - self._current_center_lon_gui, - self._current_zoom_gui, - ensure_bbox_is_covered_dict=self._target_bbox_input_gui, + self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui, + ensure_bbox_is_covered_dict=self._target_bbox_input_gui ) else: - logger.warning( - "MCM _perform_resize_redraw: Cannot redraw on resize - current map center/zoom not set in GUI state." - ) + logger.warning(f"{self.log_prefix} _perform_resize_redraw: Cannot redraw on resize - current map center/zoom not set.") self._display_placeholder_text("Map State Error\n(Cannot redraw on resize)") + def _on_left_button_press(self, event: tk.Event): + if not self.canvas.winfo_exists(): return + logger.debug(f"{self.log_prefix} _on_left_button_press: CanvasX={event.x}, CanvasY={event.y}") + self._drag_start_x_canvas, self._drag_start_y_canvas = event.x, event.y + self._is_left_button_pressed = True + + def _on_left_button_release(self, event: tk.Event): + if not self.canvas.winfo_exists(): return + logger.debug(f"{self.log_prefix} Left Button Release: CanvasX={event.x}, CanvasY={event.y}") + + if not self._is_left_button_pressed: + self._drag_start_x_canvas, self._drag_start_y_canvas = None, None + return + self._is_left_button_pressed = False + + clicked_flight_icao: Optional[str] = None + if self._drag_start_x_canvas is not None and self._drag_start_y_canvas is not None: + drag_threshold_px = 10 + dx = abs(event.x - self._drag_start_x_canvas) + dy = abs(event.y - self._drag_start_y_canvas) + is_click = dx < drag_threshold_px and dy < drag_threshold_px + + if is_click: + logger.debug(f"{self.log_prefix} Left Button Release: Detected as a map click.") + if self._current_map_geo_bounds_gui is not None and self._map_photo_image is not None: + try: + map_pixel_shape = (self._map_photo_image.width(), self._map_photo_image.height()) + clicked_lon, clicked_lat = map_utils._pixel_to_geo( + event.x, event.y, self._current_map_geo_bounds_gui, map_pixel_shape + ) + if clicked_lon is not None and clicked_lat is not None: + logger.info(f"Map Left-Clicked ({self.log_prefix}) at Geo ({clicked_lat:.5f}, {clicked_lon:.5f})") + if not self.is_detail_map and self.app_controller: + if hasattr(self.app_controller, "on_map_left_click"): + self.app_controller.on_map_left_click(clicked_lat, clicked_lon, event.x_root, event.y_root) + + min_dist_sq_to_flight = float("inf") + flight_click_radius_px_sq = 15 * 15 + with self._map_data_lock: + flights_on_map = self._current_flights_to_display_gui + if flights_on_map: + for flight in flights_on_map: + if flight.latitude is not None and flight.longitude is not None: + flight_px_coords = map_drawing._geo_to_pixel_on_unscaled_map( + flight.latitude, flight.longitude, + self._current_map_geo_bounds_gui, map_pixel_shape + ) + if flight_px_coords: + dist_sq = (event.x - flight_px_coords[0])**2 + (event.y - flight_px_coords[1])**2 + if dist_sq < flight_click_radius_px_sq and dist_sq < min_dist_sq_to_flight: + min_dist_sq_to_flight = dist_sq + clicked_flight_icao = flight.icao24 + if clicked_flight_icao: + logger.info(f"Flight selected by click ({self.log_prefix}): {clicked_flight_icao}") + if hasattr(self.app_controller, "request_detailed_flight_info"): + self.app_controller.request_detailed_flight_info(clicked_flight_icao) + elif not self.is_detail_map: + logger.info(f"No specific flight selected by click on MAIN map. Clearing details.") + if hasattr(self.app_controller, "request_detailed_flight_info"): + self.app_controller.request_detailed_flight_info("") + else: + logger.warning(f"Failed to convert left click pixel ({event.x},{event.y}) to geo ({self.log_prefix}).") + if not self.is_detail_map and self.app_controller and hasattr(self.app_controller, "request_detailed_flight_info"): + self.app_controller.request_detailed_flight_info("") + except Exception as e_click_proc: + logger.error(f"Error during left click processing ({self.log_prefix}): {e_click_proc}", exc_info=True) + if not self.is_detail_map and self.app_controller and hasattr(self.app_controller, "request_detailed_flight_info"): + self.app_controller.request_detailed_flight_info("") + else: + logger.warning(f"Map context missing for left click ({self.log_prefix}).") + if not self.is_detail_map and self.app_controller and hasattr(self.app_controller, "request_detailed_flight_info"): + self.app_controller.request_detailed_flight_info("") + self._drag_start_x_canvas, self._drag_start_y_canvas = None, None + + def _on_right_click(self, event: tk.Event): + if not self.canvas.winfo_exists(): return + logger.debug(f"{self.log_prefix} _on_right_click: CanvasX={event.x}, CanvasY={event.y}") + if self._current_map_geo_bounds_gui is None or self._map_photo_image is None: + logger.warning(f"Map context missing for right click geo conversion ({self.log_prefix}).") + return + try: + map_pixel_shape = (self._map_photo_image.width(), self._map_photo_image.height()) + geo_lon, geo_lat = map_utils._pixel_to_geo( + event.x, event.y, self._current_map_geo_bounds_gui, map_pixel_shape + ) + if geo_lon is not None and geo_lat is not None: + logger.info(f"Map Right-Clicked ({self.log_prefix}) at Geo ({geo_lat:.5f}, {geo_lon:.5f})") + if not self.is_detail_map and self.app_controller: + if hasattr(self.app_controller, "on_map_right_click"): + self.app_controller.on_map_right_click(geo_lat, geo_lon, event.x_root, event.y_root) + else: + logger.warning(f"Failed to convert right click pixel ({event.x},{event.y}) to geo ({self.log_prefix}).") + except Exception as e_rclick_convert: + logger.error(f"Error during right click geo conversion ({self.log_prefix}): {e_rclick_convert}", exc_info=True) + def set_max_track_points(self, length: int): new_length = max(2, length) - logger.debug( - f"MCM set_max_track_points: Requested length {length}, effective new_length {new_length}. Current: {self.max_track_points}" - ) if self.max_track_points != new_length: - logger.info( - f"MapCanvasManager: Max track points updating from {self.max_track_points} to {new_length}" - ) - with self._map_data_lock: # Proteggere flight_tracks_gui - self.max_track_points = ( - new_length # Aggiorna l'attributo usato dal worker - ) - logger.debug( - f"MCM set_max_track_points: Trimming existing GUI tracks (count: {len(self.flight_tracks_gui)})." - ) - for icao in list( - self.flight_tracks_gui.keys() - ): # list() per evitare RuntimeError se si modifica il dict + logger.info(f"{self.log_prefix}: Max track points updating from {self.max_track_points} to {new_length}") + with self._map_data_lock: + self.max_track_points = new_length + for icao in list(self.flight_tracks_gui.keys()): track_deque = self.flight_tracks_gui.get(icao) if track_deque: - # Crea una nuova deque con il maxlen corretto e gli ultimi N elementi - # Questo è più sicuro che modificare la deque esistente se il maxlen è cambiato - new_deque = deque( - maxlen=self.max_track_points + 5 - ) # Usa il nuovo max_track_points per la nuova deque - # Prendi gli ultimi N elementi dalla vecchia deque, fino al nuovo self.max_track_points - num_to_keep = min(len(track_deque), self.max_track_points) - for i in range(num_to_keep): - new_deque.append( - track_deque[len(track_deque) - num_to_keep + i] - ) - - if not new_deque: # Se dopo il trim è vuota - if icao in self.flight_tracks_gui: - del self.flight_tracks_gui[icao] - logger.debug( - f"Removed empty track for {icao} after trimming due to max_track_points change." - ) + new_deque_for_icao = deque(maxlen=self.max_track_points + 5) + start_index = max(0, len(track_deque) - self.max_track_points) + for i in range(start_index, len(track_deque)): + new_deque_for_icao.append(track_deque[i]) + if not new_deque_for_icao: + if icao in self.flight_tracks_gui: del self.flight_tracks_gui[icao] else: - self.flight_tracks_gui[icao] = new_deque - logger.debug( - f"Replaced track for {icao} with new deque of length {len(new_deque)}." - ) - - if ( - self._current_center_lat_gui is not None - and self._current_center_lon_gui is not None - and self._current_zoom_gui is not None - ): - logger.info( - "MCM set_max_track_points: Requesting map re-render due to track length change." - ) - self._request_map_render( - self._current_center_lat_gui, - self._current_center_lon_gui, - self._current_zoom_gui, - ) - else: - logger.warning( - "MCM set_max_track_points: Cannot request re-render as current view state is not set." - ) - else: - logger.debug( - f"MapCanvasManager: Max track points already set to {new_length}. No change, no re-render requested by this method." - ) + self.flight_tracks_gui[icao] = new_deque_for_icao + if (self._current_center_lat_gui is not None and + self._current_center_lon_gui is not None and + self._current_zoom_gui is not None): + self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui) def set_target_bbox(self, new_bbox_dict: Dict[str, float]): - logger.info( - f"MCM set_target_bbox (GUI): Received new target BBox: {new_bbox_dict}" - ) + logger.info(f"{self.log_prefix} set_target_bbox (GUI): Received new target BBox: {new_bbox_dict}") if new_bbox_dict and _is_valid_bbox_dict(new_bbox_dict): - with self._map_data_lock: # Protegge _target_bbox_input_gui - self._target_bbox_input_gui = new_bbox_dict.copy() - logger.debug( - f"MCM set_target_bbox: _target_bbox_input_gui updated. Requesting render for this new bbox." - ) - self._request_map_render_for_bbox( - self._target_bbox_input_gui, preserve_current_zoom_if_possible=False - ) - if self.app_controller and hasattr( - self.app_controller, "update_bbox_gui_fields" - ): - self.app_controller.update_bbox_gui_fields(self._target_bbox_input_gui) - else: - logger.warning( - f"MCM set_target_bbox: Invalid/empty new_bbox_dict: {new_bbox_dict}. Clearing _target_bbox_input_gui." - ) with self._map_data_lock: - self._target_bbox_input_gui = None - if ( - self._current_center_lat_gui is not None - and self._current_center_lon_gui is not None - and self._current_zoom_gui is not None - ): - logger.info( - f"MCM set_target_bbox: Target BBox cleared. Re-rendering at current view: center=({self._current_center_lat_gui},{self._current_center_lon_gui}), zoom={self._current_zoom_gui}" - ) - self._request_map_render( - self._current_center_lat_gui, - self._current_center_lon_gui, - self._current_zoom_gui, - ) - else: - logger.warning( - "MCM set_target_bbox: Target BBox cleared, but no current view to re-render. Clearing map display." - ) - self.clear_map_display() # Mostrerà placeholder - if self.app_controller and hasattr( - self.app_controller, "update_bbox_gui_fields" - ): - self.app_controller.update_bbox_gui_fields( - {} - ) # Invia dict vuoto per resettare i campi GUI + self._target_bbox_input_gui = new_bbox_dict.copy() + logger.debug(f"{self.log_prefix} set_target_bbox: _target_bbox_input_gui updated. Requesting render.") + self._request_map_render_for_bbox(self._target_bbox_input_gui, preserve_current_zoom_if_possible=False) + if not self.is_detail_map: + if self.app_controller and hasattr(self.app_controller, "update_bbox_gui_fields"): + self.app_controller.update_bbox_gui_fields(self._target_bbox_input_gui) + else: + logger.warning(f"{self.log_prefix} set_target_bbox: Invalid/empty new_bbox_dict: {new_bbox_dict}. Clearing.") + with self._map_data_lock: self._target_bbox_input_gui = None + if (self._current_center_lat_gui is not None and self._current_center_lon_gui is not None and self._current_zoom_gui is not None): + self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui) + else: self.clear_map_display() + if not self.is_detail_map: + if self.app_controller and hasattr(self.app_controller, "update_bbox_gui_fields"): + self.app_controller.update_bbox_gui_fields({}) def _request_map_render_for_bbox( self, target_bbox_dict: Dict[str, float], preserve_current_zoom_if_possible: bool = False, ): - logger.debug( - f"MCM _request_map_render_for_bbox: target_bbox={target_bbox_dict}, preserve_zoom={preserve_current_zoom_if_possible}" - ) + logger.debug(f"{self.log_prefix} _request_map_render_for_bbox: target_bbox={target_bbox_dict}, preserve_zoom={preserve_current_zoom_if_possible}") if not target_bbox_dict or not _is_valid_bbox_dict(target_bbox_dict): - logger.warning( - "_request_map_render_for_bbox called with invalid/no target BBox. Aborting." - ) + logger.warning(f"{self.log_prefix} _request_map_render_for_bbox called with invalid/no target BBox. Aborting.") + if not self.is_detail_map: self._display_placeholder_text("Invalid BBox for Map Render") return - lat_min, lon_min, lat_max, lon_max = ( - target_bbox_dict["lat_min"], - target_bbox_dict["lon_min"], - target_bbox_dict["lat_max"], - target_bbox_dict["lon_max"], - ) - view_center_lat, view_center_lon = (lat_min + lat_max) / 2.0, ( - lon_min + lon_max - ) / 2.0 - if ( - lon_min > lon_max - ): # Gestisce BBox che attraversano l'antimeridiano (per il centro) + lat_min, lon_min, lat_max, lon_max = (target_bbox_dict["lat_min"], target_bbox_dict["lon_min"], target_bbox_dict["lat_max"], target_bbox_dict["lon_max"]) + view_center_lat, view_center_lon = (lat_min + lat_max) / 2.0, (lon_min + lon_max) / 2.0 + if lon_min > lon_max: view_center_lon = (lon_min + (lon_max + 360.0)) / 2.0 - if view_center_lon >= 180.0: - view_center_lon -= 360.0 - logger.debug( - f"MCM _request_map_render_for_bbox: Calculated BBox center: ({view_center_lat:.4f}, {view_center_lon:.4f})" - ) - - zoom_to_use = self._current_zoom_gui # Default alla zoom corrente - logger.debug( - f"MCM _request_map_render_for_bbox: Initial zoom_to_use (from _current_zoom_gui): {zoom_to_use}" - ) - + if view_center_lon >= 180.0: view_center_lon -= 360.0 + + zoom_to_use = self._current_zoom_gui if not preserve_current_zoom_if_possible or zoom_to_use is None: - logger.debug( - f"MCM _request_map_render_for_bbox: Calculating new zoom (preserve_current_zoom_if_possible={preserve_current_zoom_if_possible}, zoom_to_use_is_None={zoom_to_use is None})" - ) - patch_width_km, patch_height_km = calculate_geographic_bbox_size_km( - (lon_min, lat_min, lon_max, lat_max) # W,S,E,N - ) or (None, None) - logger.debug( - f"MCM _request_map_render_for_bbox: BBox size: {patch_width_km}km x {patch_height_km}km" - ) - if ( - patch_width_km - and patch_height_km - and self.canvas_width > 0 - and self.canvas_height > 0 - ): - zoom_w = calculate_zoom_level_for_geographic_size( - view_center_lat, - patch_width_km * 1000, - self.canvas_width, - self.tile_manager.tile_size, - ) - zoom_h = calculate_zoom_level_for_geographic_size( - view_center_lat, - patch_height_km * 1000, - self.canvas_height, - self.tile_manager.tile_size, - ) - logger.debug( - f"MCM _request_map_render_for_bbox: Calculated zoom_w={zoom_w}, zoom_h={zoom_h}" - ) - if zoom_w is not None and zoom_h is not None: - new_calc_zoom = min(zoom_w, zoom_h) - elif zoom_w is not None: - new_calc_zoom = zoom_w - elif zoom_h is not None: - new_calc_zoom = zoom_h - else: - new_calc_zoom = map_constants.DEFAULT_INITIAL_ZOOM - logger.warning( - f"MCM _request_map_render_for_bbox: Both zoom_w and zoom_h are None. Using default zoom {new_calc_zoom}." - ) + patch_width_km, patch_height_km = calculate_geographic_bbox_size_km((lon_min, lat_min, lon_max, lat_max)) or (None, None) + if patch_width_km and patch_height_km and self.canvas_width > 0 and self.canvas_height > 0: + zoom_w = calculate_zoom_level_for_geographic_size(view_center_lat, patch_width_km * 1000, self.canvas_width, self.tile_manager.tile_size) + zoom_h = calculate_zoom_level_for_geographic_size(view_center_lat, patch_height_km * 1000, self.canvas_height, self.tile_manager.tile_size) + if zoom_w is not None and zoom_h is not None: new_calc_zoom = min(zoom_w, zoom_h) + elif zoom_w is not None: new_calc_zoom = zoom_w + elif zoom_h is not None: new_calc_zoom = zoom_h + else: new_calc_zoom = map_constants.DEFAULT_INITIAL_ZOOM zoom_to_use = new_calc_zoom - logger.info( - f"MCM _request_map_render_for_bbox: Calculated new zoom_to_use: {zoom_to_use}" - ) + logger.info(f"{self.log_prefix} _request_map_render_for_bbox: Calculated new zoom_to_use: {zoom_to_use}") else: - logger.warning( - "MCM _request_map_render_for_bbox: Could not calculate BBox dimensions or canvas size invalid. Using default zoom." - ) + logger.warning(f"{self.log_prefix} _request_map_render_for_bbox: Could not calculate BBox dims or canvas size invalid. Using default zoom.") zoom_to_use = map_constants.DEFAULT_INITIAL_ZOOM - else: - logger.debug( - f"MCM _request_map_render_for_bbox: Preserving current zoom: {zoom_to_use}" - ) - - max_zoom_svc = ( - self.map_service.max_zoom - if self.map_service - else map_constants.DEFAULT_MAX_ZOOM_FALLBACK - ) + + max_zoom_svc = self.map_service.max_zoom if self.map_service else map_constants.DEFAULT_MAX_ZOOM_FALLBACK zoom_to_use = max(map_constants.MIN_ZOOM_LEVEL, min(zoom_to_use, max_zoom_svc)) - logger.debug( - f"MCM _request_map_render_for_bbox: Final zoom_to_use after clamping: {zoom_to_use}" - ) - - self._request_map_render( - view_center_lat, - view_center_lon, - zoom_to_use, - ensure_bbox_is_covered_dict=target_bbox_dict, - ) + self._request_map_render(view_center_lat, view_center_lon, zoom_to_use, ensure_bbox_is_covered_dict=target_bbox_dict) def _clear_canvas_display_elements(self): - logger.debug( - "MCM _clear_canvas_display_elements: Clearing canvas image and placeholder text." - ) if self._canvas_image_id is not None and self.canvas.winfo_exists(): - try: - self.canvas.delete(self._canvas_image_id) - except Exception as e_del_img: - logger.warning( - f"Error deleting canvas image ID {self._canvas_image_id}: {e_del_img}" - ) - finally: - self._canvas_image_id = None - if self._map_photo_image is not None: - self._map_photo_image = None # Rimuove riferimento all'immagine + try: self.canvas.delete(self._canvas_image_id) + except Exception: pass + finally: self._canvas_image_id = None + if self._map_photo_image is not None: self._map_photo_image = None if self._placeholder_text_id is not None and self.canvas.winfo_exists(): - try: - self.canvas.delete(self._placeholder_text_id) - except Exception as e_del_text: - logger.warning( - f"Error deleting placeholder text ID {self._placeholder_text_id}: {e_del_text}" - ) - finally: - self._placeholder_text_id = None - logger.debug("MCM _clear_canvas_display_elements: Done.") + try: self.canvas.delete(self._placeholder_text_id) + except Exception: pass + finally: self._placeholder_text_id = None def _display_placeholder_text(self, text: str): - logger.debug(f"MCM _display_placeholder_text: Displaying '{text[:50]}...'") + logger.debug(f"{self.log_prefix} _display_placeholder_text: Displaying '{text[:50]}...'") if not self.canvas.winfo_exists(): - logger.warning("MCM _display_placeholder_text: Canvas does not exist.") + logger.warning(f"{self.log_prefix} _display_placeholder_text: Canvas does not exist.") return - self._clear_canvas_display_elements() # Assicura che il canvas sia pulito prima di disegnare il testo + self._clear_canvas_display_elements() try: - canvas_w_disp, canvas_h_disp = ( - self.canvas.winfo_width(), - self.canvas.winfo_height(), - ) - if canvas_w_disp <= 1: - canvas_w_disp = self.canvas_width # Fallback - if canvas_h_disp <= 1: - canvas_h_disp = self.canvas_height # Fallback - logger.debug( - f"MCM _display_placeholder_text: Using canvas_dims {canvas_w_disp}x{canvas_h_disp}" - ) + canvas_w_disp, canvas_h_disp = self.canvas.winfo_width(), self.canvas.winfo_height() + if canvas_w_disp <= 1: canvas_w_disp = self.canvas_width + if canvas_h_disp <= 1: canvas_h_disp = self.canvas_height if canvas_w_disp > 1 and canvas_h_disp > 1: - bg_color_to_use = getattr( - map_constants, "DEFAULT_PLACEHOLDER_COLOR_RGB_TK", "gray90" - ) - self.canvas.configure( - bg=bg_color_to_use - ) # Imposta il colore di sfondo del canvas + bg_color_to_use = getattr(map_constants, "DEFAULT_PLACEHOLDER_COLOR_RGB_TK", "gray90") + self.canvas.configure(bg=bg_color_to_use) self._placeholder_text_id = self.canvas.create_text( - canvas_w_disp / 2, - canvas_h_disp / 2, - text=text, - fill="gray10", - font=("Arial", 11, "normal"), - justify=tk.CENTER, - width=max(100, canvas_w_disp - 40), + canvas_w_disp / 2, canvas_h_disp / 2, text=text, fill="gray10", + font=("Arial", 11, "normal"), justify=tk.CENTER, width=max(100, canvas_w_disp - 40) ) - logger.debug( - f"MCM _display_placeholder_text: Placeholder text ID {self._placeholder_text_id} created." - ) - else: - logger.warning( - f"MCM _display_placeholder_text: Cannot draw placeholder text, canvas dims invalid ({canvas_w_disp}x{canvas_h_disp})." - ) - except tk.TclError as e_tcl_placeholder: - logger.warning( - f"MCM _display_placeholder_text: TclError displaying placeholder text (canvas might be gone): {e_tcl_placeholder}" - ) - except Exception as e_placeholder: - logger.error( - f"MCM _display_placeholder_text: Unexpected error: {e_placeholder}", - exc_info=True, - ) + except tk.TclError: pass + except Exception as e: logger.error(f"{self.log_prefix} _display_placeholder_text: Unexpected error: {e}", exc_info=True) def clear_map_display(self): - logger.info( - "MCM clear_map_display: Clearing all map content and resetting view state." - ) + logger.info(f"{self.log_prefix} clear_map_display: Clearing all map content and resetting view state.") self._clear_canvas_display_elements() self._display_placeholder_text("Map Cleared / Awaiting Action") - with self._map_data_lock: self._current_flights_to_display_gui = [] self.flight_tracks_gui.clear() - - self._current_map_geo_bounds_gui = None # L'immagine della mappa è sparita, quindi i suoi bounds non sono più validi - - logger.debug( - "MCM clear_map_display: Internal data cleared (flights, tracks, map_bounds)." - ) - - # Forza un re-render della mappa base (senza voli/tracce) se la vista era impostata - if ( - self._current_center_lat_gui is not None - and self._current_center_lon_gui is not None - and self._current_zoom_gui is not None - ): - logger.info( - "MCM clear_map_display: Requesting re-render of clean base map." - ) + self._current_map_geo_bounds_gui = None + + if (self._current_center_lat_gui is not None and + self._current_center_lon_gui is not None and + self._current_zoom_gui is not None): + logger.info(f"{self.log_prefix} clear_map_display: Requesting re-render of clean base map.") self._request_map_render( - self._current_center_lat_gui, - self._current_center_lon_gui, - self._current_zoom_gui, - ensure_bbox_is_covered_dict=self._target_bbox_input_gui, # Mantiene il BBox di monitoraggio se era impostato + self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui, + ensure_bbox_is_covered_dict=self._target_bbox_input_gui ) - else: - # Se non c'è uno stato di vista precedente, il placeholder è sufficiente. - # L'utente dovrà interagire (es. impostare un BBox) per vedere una mappa. - logger.warning( - "MCM clear_map_display: No current view state to request clean render, placeholder remains." - ) - # Assicuriamoci che le info sulla mappa vengano aggiornate per riflettere lo stato "pulito" - if self.app_controller and hasattr( - self.app_controller, "update_general_map_info" - ): + elif not self.is_detail_map: + if self.app_controller and hasattr(self.app_controller, "update_general_map_info"): self.app_controller.update_general_map_info() - def _on_left_button_press(self, event: tk.Event): - if not self.canvas.winfo_exists(): - return - logger.debug(f"MCM _on_left_button_press: CanvasX={event.x}, CanvasY={event.y}") - self._drag_start_x_canvas, self._drag_start_y_canvas = event.x, event.y - self._is_left_button_pressed = True - - def _on_left_button_release(self, event: tk.Event): - if not self.canvas.winfo_exists(): - return - logger.debug( - f"MCM Left Button Release: CanvasX={event.x}, CanvasY={event.y}" - ) # Usa module_logger - - if not self._is_left_button_pressed: # Verifica se il bottone era stato premuto - logger.debug( - "MCM Left Button Release: Button was not previously pressed. Ignoring." - ) - self._drag_start_x_canvas, self._drag_start_y_canvas = ( - None, - None, - ) # Resetta per sicurezza - return - self._is_left_button_pressed = False # Resetta lo stato del bottone - - clicked_flight_icao: Optional[str] = ( - None # Per memorizzare l'ICAO del volo cliccato - ) - - if ( - self._drag_start_x_canvas is not None - and self._drag_start_y_canvas is not None - ): - drag_threshold_px = 10 # Soglia per distinguere click da drag - dx = abs(event.x - self._drag_start_x_canvas) - dy = abs(event.y - self._drag_start_y_canvas) - - is_click = dx < drag_threshold_px and dy < drag_threshold_px - logger.debug( - f"MCM Left Button Release: dx={dx}, dy={dy}. Is click: {is_click}" - ) - - if is_click: - logger.debug("MCM Left Button Release: Detected as a map click.") - if ( - self._current_map_geo_bounds_gui is not None - and self._map_photo_image is not None - ): - try: - map_pixel_shape = ( - self._map_photo_image.width(), - self._map_photo_image.height(), - ) - clicked_lon, clicked_lat = ( - map_utils._pixel_to_geo( # Usa map_drawing._pixel_to_geo - event.x, - event.y, - self._current_map_geo_bounds_gui, - map_pixel_shape, - ) - ) - - if clicked_lon is not None and clicked_lat is not None: - logger.info( - f"Map Left-Clicked at Geo ({clicked_lat:.5f}, {clicked_lon:.5f}) from Canvas ({event.x},{event.y})" - ) - if self.app_controller and hasattr( - self.app_controller, "on_map_left_click" - ): - self.app_controller.on_map_left_click( - clicked_lat, clicked_lon, event.x_root, event.y_root - ) - - # --- Logica per identificare il volo cliccato --- - min_dist_sq_to_flight = float("inf") - # Soglia di click in pixel per selezionare un aereo, al quadrato per evitare sqrt - flight_click_radius_px_sq = 15 * 15 - - with self._map_data_lock: # Accedi in modo sicuro alla lista dei voli - flights_on_map = self._current_flights_to_display_gui - - if flights_on_map: # Solo se ci sono voli da controllare - for flight in flights_on_map: - if ( - flight.latitude is not None - and flight.longitude is not None - ): - # Converte la posizione del volo in pixel sulla mappa attuale - flight_px_coords = map_drawing._geo_to_pixel_on_unscaled_map( - flight.latitude, - flight.longitude, - self._current_map_geo_bounds_gui, # Bounds della mappa visualizzata - map_pixel_shape, # Dimensioni in pixel della mappa visualizzata - ) - if flight_px_coords: - # Calcola la distanza al quadrato tra il click e l'aereo - dist_sq = ( - event.x - flight_px_coords[0] - ) ** 2 + ( - event.y - flight_px_coords[1] - ) ** 2 - if ( - dist_sq < flight_click_radius_px_sq - and dist_sq < min_dist_sq_to_flight - ): - min_dist_sq_to_flight = dist_sq - clicked_flight_icao = flight.icao24 - - if clicked_flight_icao: - logger.info( - f"Flight selected by click: {clicked_flight_icao}" - ) - if self.app_controller and hasattr( - self.app_controller, "request_detailed_flight_info" - ): - self.app_controller.request_detailed_flight_info( - clicked_flight_icao - ) - else: - logger.info( - "No specific flight selected by click (click was on map but not near an aircraft). Clearing details." - ) - if self.app_controller and hasattr( - self.app_controller, "request_detailed_flight_info" - ): - self.app_controller.request_detailed_flight_info( - "" - ) # Invia ICAO vuoto per pulire il pannello - - else: # clicked_lon/lat is None - logger.warning( - f"Failed to convert left click pixel ({event.x},{event.y}) to geo coordinates." - ) - if self.app_controller and hasattr( - self.app_controller, "request_detailed_flight_info" - ): - self.app_controller.request_detailed_flight_info( - "" - ) # Pulisci dettagli se il click non è valido - - except Exception as e_click_processing: - logger.error( - f"Error during left click processing (geo conversion or flight selection): {e_click_processing}", - exc_info=True, - ) - if self.app_controller and hasattr( - self.app_controller, "request_detailed_flight_info" - ): - self.app_controller.request_detailed_flight_info( - "" - ) # Pulisci in caso di errore - - else: # Manca il contesto della mappa (_current_map_geo_bounds_gui o _map_photo_image) - logger.warning( - "Map context missing for left click geo conversion or flight selection." - ) - if self.app_controller and hasattr( - self.app_controller, "request_detailed_flight_info" - ): - self.app_controller.request_detailed_flight_info("") - else: # Era un drag - logger.debug( - "MCM Left Button Release: Detected as a drag. Panning logic (if any) would go here." - ) - # Se implementi il drag-to-pan, qui non faresti nulla per la selezione dei voli. - - # Resetta le coordinate di inizio drag - self._drag_start_x_canvas, self._drag_start_y_canvas = None, None - - def _on_right_click(self, event: tk.Event): - if not self.canvas.winfo_exists(): - return - logger.debug(f"MCM _on_right_click: CanvasX={event.x}, CanvasY={event.y}") - if self._current_map_geo_bounds_gui is None or self._map_photo_image is None: - logger.warning("Map context missing for right click geo conversion.") - return - try: - map_pixel_shape = ( - self._map_photo_image.width(), - self._map_photo_image.height(), - ) - geo_lon, geo_lat = _pixel_to_geo( - event.x, event.y, self._current_map_geo_bounds_gui, map_pixel_shape - ) - if geo_lon is not None and geo_lat is not None: - logger.info(f"Map Right-Clicked at Geo ({geo_lat:.5f}, {geo_lon:.5f})") - if self.app_controller and hasattr( - self.app_controller, "on_map_right_click" - ): # Modificato in on_map_right_click - self.app_controller.on_map_right_click( - geo_lat, geo_lon, event.x_root, event.y_root - ) - else: - logger.warning( - f"Failed to convert right click pixel ({event.x},{event.y}) to geo." - ) - except Exception as e_rclick_convert: - logger.error( - f"Error during right click geo conversion: {e_rclick_convert}", - exc_info=True, - ) - def update_flights_on_map(self, flight_states: List[CanonicalFlightState]): - logger.info( - f"MCM update_flights_on_map (GUI): Received {len(flight_states)} flight states." - ) - - with self._map_data_lock: # Protegge _current_flights_to_display_gui e flight_tracks_gui - self._current_flights_to_display_gui = ( - flight_states # Sostituisce la lista dei voli - ) - - if not flight_states: # Monitoraggio fermato o nessun volo - logger.info( - "MCM update_flights_on_map: Received empty flight_states. Clearing all flight tracks." - ) - self.flight_tracks_gui.clear() - else: # Ci sono voli, aggiorna le tracce + logger.info(f"{self.log_prefix} update_flights_on_map (GUI): Received {len(flight_states)} flight states.") + with self._map_data_lock: + self._current_flights_to_display_gui = flight_states + if not flight_states: self.flight_tracks_gui.clear() + else: current_time = time.time() active_icao_this_update = set() for state in flight_states: - 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: active_icao_this_update.add(state.icao24) if state.icao24 not in self.flight_tracks_gui: - # Usa self.max_track_points (che è letto da config) per il maxlen - self.flight_tracks_gui[state.icao24] = deque( - maxlen=self.max_track_points + 5 - ) # +5 per buffer - self.flight_tracks_gui[state.icao24].append( - (state.latitude, state.longitude, state.timestamp) - ) - + self.flight_tracks_gui[state.icao24] = deque(maxlen=self.max_track_points + 5) + self.flight_tracks_gui[state.icao24].append((state.latitude, state.longitude, state.timestamp)) tracks_to_remove_gui = [] for icao, track_deque in self.flight_tracks_gui.items(): - if ( - not track_deque - ): # Rimuovi se per qualche motivo la deque è vuota - tracks_to_remove_gui.append(icao) - continue - - if ( - icao not in active_icao_this_update - ): # Volo non più attivo nell'update corrente + if not track_deque: tracks_to_remove_gui.append(icao); continue + if icao not in active_icao_this_update: last_point_time = track_deque[-1][2] if current_time - last_point_time > self.max_track_age_seconds: - tracks_to_remove_gui.append( - icao - ) # Rimuovi se anche troppo vecchia - + tracks_to_remove_gui.append(icao) if tracks_to_remove_gui: - logger.debug( - f"MCM update_flights_on_map: Removing {len(tracks_to_remove_gui)} old/inactive GUI tracks." - ) for icao in tracks_to_remove_gui: - if icao in self.flight_tracks_gui: - del self.flight_tracks_gui[icao] - - logger.debug( - f"MCM update_flights_on_map: Updated _current_flights_to_display_gui with {len(self._current_flights_to_display_gui)} states. Total tracks: {len(self.flight_tracks_gui)}." - ) - - # Richiedi un re-render della mappa con i nuovi dati dei voli/tracce - if ( - self._current_center_lat_gui is not None - and self._current_center_lon_gui is not None - and self._current_zoom_gui is not None - ): - logger.info( - "MCM update_flights_on_map: Requesting map re-render due to flight update." - ) - self._request_map_render( - self._current_center_lat_gui, - self._current_center_lon_gui, - self._current_zoom_gui, - ) - else: - logger.warning( - "MCM update_flights_on_map: Cannot request map render for flight update - current map view state not set." - ) + if icao in self.flight_tracks_gui: del self.flight_tracks_gui[icao] + + if (self._current_center_lat_gui is not None and + self._current_center_lon_gui is not None and + self._current_zoom_gui is not None): + logger.info(f"{self.log_prefix} update_flights_on_map: Requesting map re-render due to flight update.") + self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui) def get_current_map_info(self) -> Dict[str, Any]: - logger.debug("MCM get_current_map_info (GUI) called.") + logger.debug(f"{self.log_prefix} get_current_map_info (GUI) called.") map_size_km_w, map_size_km_h = None, None - if ( - PYPROJ_MODULE_LOCALLY_AVAILABLE - and pyproj - and self._current_map_geo_bounds_gui - ): + if (PYPROJ_MODULE_LOCALLY_AVAILABLE and map_utils.pyproj and self._current_map_geo_bounds_gui): try: - size_km_tuple = calculate_geographic_bbox_size_km( - self._current_map_geo_bounds_gui - ) - if size_km_tuple: - map_size_km_w, map_size_km_h = size_km_tuple + size_km_tuple = calculate_geographic_bbox_size_km(self._current_map_geo_bounds_gui) + if size_km_tuple: map_size_km_w, map_size_km_h = size_km_tuple except Exception as e_map_size_info: - logger.warning( - f"Error calculating current map geo size for info panel: {e_map_size_info}", - exc_info=False, - ) + logger.warning(f"Error calculating current map geo size for info panel ({self.log_prefix}): {e_map_size_info}", exc_info=False) - with self._map_data_lock: # Accedi a dati condivisi in modo sicuro + with self._map_data_lock: flight_count = len(self._current_flights_to_display_gui) - target_bbox_copy = ( - copy.deepcopy(self._target_bbox_input_gui) - if self._target_bbox_input_gui - else None - ) + target_bbox_copy = copy.deepcopy(self._target_bbox_input_gui) if self._target_bbox_input_gui else None info = { - "center_lat": self._current_center_lat_gui, - "center_lon": self._current_center_lon_gui, - "zoom": self._current_zoom_gui, - "map_geo_bounds": self._current_map_geo_bounds_gui, - "target_bbox_input": target_bbox_copy, - "canvas_width": self.canvas_width, - "canvas_height": self.canvas_height, - "map_size_km_w": map_size_km_w, - "map_size_km_h": map_size_km_h, - "flight_count": flight_count, + "center_lat": self._current_center_lat_gui, "center_lon": self._current_center_lon_gui, + "zoom": self._current_zoom_gui, "map_geo_bounds": self._current_map_geo_bounds_gui, + "target_bbox_input": target_bbox_copy, "canvas_width": self.canvas_width, + "canvas_height": self.canvas_height, "map_size_km_w": map_size_km_w, + "map_size_km_h": map_size_km_h, "flight_count": flight_count, } - logger.debug( - f"MCM get_current_map_info returning: Flights={info['flight_count']}, Zoom={info['zoom']}, Center=({info['center_lat']},{info['center_lon']})" - ) + logger.debug(f"{self.log_prefix} get_current_map_info returning: Flights={info['flight_count']}, Zoom={info['zoom']}, Center=({info['center_lat']},{info['center_lon']})") return info def show_map_context_menu_from_gui( self, latitude: float, longitude: float, screen_x: int, screen_y: int ): - logger.info( - f"MCM show_map_context_menu_from_gui: Lat {latitude:.4f}, Lon {longitude:.4f}" - ) + if self.is_detail_map: + logger.warning("show_map_context_menu_from_gui called on a detail map. Ignoring.") + return + logger.info(f"MCM show_map_context_menu_from_gui: Lat {latitude:.4f}, Lon {longitude:.4f}") if not self.canvas.winfo_exists(): logger.warning("MCM show_map_context_menu_from_gui: Canvas does not exist.") return - root_widget = self.canvas.winfo_toplevel() # Prendi la finestra principale + root_widget = self.canvas.winfo_toplevel() try: context_menu = tk.Menu(root_widget, tearoff=0) decimals = getattr(map_constants, "COORDINATE_DECIMAL_PLACES", 5) - context_menu.add_command( - label=f"Context @ {latitude:.{decimals}f},{longitude:.{decimals}f}", - state=tk.DISABLED, - ) + context_menu.add_command(label=f"Context @ {latitude:.{decimals}f},{longitude:.{decimals}f}", state=tk.DISABLED) context_menu.add_separator() - if self.app_controller: # Verifica se il controller è impostato + if self.app_controller: if hasattr(self.app_controller, "recenter_map_at_coords"): - context_menu.add_command( - label="Center map here", - command=lambda: self.app_controller.recenter_map_at_coords( - latitude, longitude - ), - ) + context_menu.add_command(label="Center map here", command=lambda: self.app_controller.recenter_map_at_coords(latitude, longitude)) if hasattr(self.app_controller, "set_bbox_around_coords"): - area_km_cfg_name = "DEFAULT_CLICK_AREA_SIZE_KM" # Nome della costante nel controller - area_km = getattr( - self.app_controller, area_km_cfg_name, 50.0 - ) # Usa getattr per fallback - context_menu.add_command( - label=f"Set {area_km:.0f}km Mon. Area", - command=lambda: self.app_controller.set_bbox_around_coords( - latitude, longitude, area_km - ), - ) + area_km_cfg_name = "DEFAULT_CLICK_AREA_SIZE_KM" + area_km = getattr(self.app_controller, area_km_cfg_name, 50.0) + context_menu.add_command(label=f"Set {area_km:.0f}km Mon. Area", command=lambda: self.app_controller.set_bbox_around_coords(latitude, longitude, area_km)) context_menu.tk_popup(screen_x, screen_y) - logger.debug("MCM show_map_context_menu_from_gui: Context menu popped up.") - except tk.TclError as e_menu_tcl_ctx: - logger.warning( - f"TclError showing MapCanvasManager context menu: {e_menu_tcl_ctx}." - ) - except Exception as e_menu_ctx: - logger.error( - f"Error creating/showing MapCanvasManager context menu: {e_menu_ctx}", - exc_info=True, - ) + except tk.TclError as e: logger.warning(f"TclError showing MapCanvasManager context menu: {e}.") + except Exception as e: logger.error(f"Error creating/showing MapCanvasManager context menu: {e}", exc_info=True) def recenter_map_at_coords(self, lat: float, lon: float): - logger.info( - f"MCM recenter_map_at_coords (GUI): Request to recenter map @ Geo ({lat:.4f}, {lon:.4f})" - ) + logger.info(f"{self.log_prefix} recenter_map_at_coords (GUI): Request to recenter map @ Geo ({lat:.4f}, {lon:.4f})") if self._current_zoom_gui is not None and self.canvas.winfo_exists(): self._current_center_lat_gui = lat self._current_center_lon_gui = lon - logger.debug( - f"MCM recenter_map_at_coords: Updated GUI center to ({lat:.4f},{lon:.4f}). Requesting render." - ) self._request_map_render(lat, lon, self._current_zoom_gui) else: - logger.warning( - "MCM recenter_map_at_coords: Cannot recenter map - missing current zoom or canvas not available." - ) + logger.warning(f"{self.log_prefix} recenter_map_at_coords: Cannot recenter map - missing current zoom or canvas not available.") - def set_bbox_around_coords( - self, center_lat: float, center_lon: float, area_size_km: float - ): - logger.info( - f"MCM set_bbox_around_coords (GUI): Request to set BBox around Geo ({center_lat:.4f}, {center_lon:.4f}), size {area_size_km:.1f}km." - ) - if not PYPROJ_MODULE_LOCALLY_AVAILABLE or pyproj is None: - logger.error( - "MCM set_bbox_around_coords: Cannot set BBox - pyproj library not available." - ) - if self.app_controller and hasattr( - self.app_controller, "show_error_message" - ): - self.app_controller.show_error_message( - "Map Error", "Cannot calculate BBox: Geographic libraries missing." - ) + def set_bbox_around_coords(self, center_lat: float, center_lon: float, area_size_km: float): + if self.is_detail_map: + logger.warning(f"{self.log_prefix} set_bbox_around_coords called on a DETAIL map. This action is likely for the main map.") + return + + logger.info(f"{self.log_prefix} set_bbox_around_coords (MAIN MAP): Request to set BBox around Geo ({center_lat:.4f}, {center_lon:.4f}), size {area_size_km:.1f}km.") + if not PYPROJ_MODULE_LOCALLY_AVAILABLE or map_utils.pyproj is None: + logger.error(f"{self.log_prefix} set_bbox_around_coords: Cannot set BBox - pyproj library not available.") + if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"): + self.app_controller.main_window.show_error_message("Map Error", "Cannot calculate BBox: Geographic libraries missing.") return try: - bbox_tuple_wesn = get_bounding_box_from_center_size( - center_lat, center_lon, area_size_km - ) # W,S,E,N + bbox_tuple_wesn = get_bounding_box_from_center_size(center_lat, center_lon, area_size_km) if bbox_tuple_wesn: - bbox_dict = { - "lon_min": bbox_tuple_wesn[0], - "lat_min": bbox_tuple_wesn[1], - "lon_max": bbox_tuple_wesn[2], - "lat_max": bbox_tuple_wesn[3], - } + bbox_dict = {"lon_min": bbox_tuple_wesn[0], "lat_min": bbox_tuple_wesn[1], "lon_max": bbox_tuple_wesn[2], "lat_max": bbox_tuple_wesn[3]} if _is_valid_bbox_dict(bbox_dict): - logger.debug( - f"MCM set_bbox_around_coords: Calculated valid BBox: {bbox_dict}. Calling set_target_bbox." - ) - self.set_target_bbox( - bbox_dict - ) # Questo chiamerà _request_map_render_for_bbox + self.set_target_bbox(bbox_dict) else: - logger.error( - f"MCM set_bbox_around_coords: Calculated BBox is invalid: {bbox_dict}." - ) - if self.app_controller and hasattr( - self.app_controller, "show_error_message" - ): - self.app_controller.show_error_message( - "Map Error", "Calculated BBox is invalid." - ) + logger.error(f"{self.log_prefix} set_bbox_around_coords: Calculated BBox is invalid: {bbox_dict}.") + if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"): + self.app_controller.main_window.show_error_message("Map Error", "Calculated BBox is invalid.") else: - logger.error( - f"MCM set_bbox_around_coords: Failed to calculate BBox around coords ({center_lat}, {center_lon}, {area_size_km}km)." - ) - if self.app_controller and hasattr( - self.app_controller, "show_error_message" - ): - self.app_controller.show_error_message( - "Map Error", "Failed to calculate BBox around coordinates." - ) - except Exception as e_set_bbox_calc: - logger.exception( - f"MCM set_bbox_around_coords: Unexpected error calculating BBox: {e_set_bbox_calc}" - ) - if self.app_controller and hasattr( - self.app_controller, "show_error_message" - ): - self.app_controller.show_error_message( - "Map Error", - f"An unexpected error occurred calculating BBox: {e_set_bbox_calc}", - ) + logger.error(f"{self.log_prefix} set_bbox_around_coords: Failed to calculate BBox around coords.") + if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"): + self.app_controller.main_window.show_error_message("Map Error", "Failed to calculate BBox around coordinates.") + except Exception as e: + logger.exception(f"{self.log_prefix} set_bbox_around_coords: Unexpected error calculating BBox: {e}") + if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"): + self.app_controller.main_window.show_error_message("Map Error", f"An unexpected error occurred calculating BBox: {e}") def zoom_in_at_center(self): - logger.debug( - f"MCM zoom_in_at_center (GUI) called. Current zoom: {self._current_zoom_gui}" - ) - if ( - self._current_zoom_gui is None - or self._current_center_lat_gui is None - or self._current_center_lon_gui is None - ): - logger.warning( - "MCM zoom_in_at_center: Cannot zoom in - current map state (zoom/center) is not defined." - ) + logger.debug(f"{self.log_prefix} zoom_in_at_center (GUI) called. Current zoom: {self._current_zoom_gui}") + if (self._current_zoom_gui is None or self._current_center_lat_gui is None or self._current_center_lon_gui is None): + logger.warning(f"{self.log_prefix} zoom_in_at_center: Cannot zoom in - current map state is not defined.") return - if not self.canvas.winfo_exists(): - logger.warning( - "MCM zoom_in_at_center: Cannot zoom in - canvas not available." - ) - return - max_zoom_svc = ( - self.map_service.max_zoom - if self.map_service - else map_constants.DEFAULT_MAX_ZOOM_FALLBACK - ) + if not self.canvas.winfo_exists(): return + max_zoom_svc = self.map_service.max_zoom if self.map_service else map_constants.DEFAULT_MAX_ZOOM_FALLBACK new_zoom = min(self._current_zoom_gui + 1, max_zoom_svc) - logger.debug( - f"MCM zoom_in_at_center: new_zoom calculated as {new_zoom} (max_svc_zoom={max_zoom_svc})" - ) if new_zoom != self._current_zoom_gui: - logger.info( - f"MCM zoom_in_at_center: Zooming in from {self._current_zoom_gui} to {new_zoom}." - ) self._current_zoom_gui = new_zoom - self._request_map_render( - self._current_center_lat_gui, self._current_center_lon_gui, new_zoom - ) - else: - logger.debug( - f"MCM zoom_in_at_center: Already at max zoom ({self._current_zoom_gui}). Cannot zoom in further." - ) + self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, new_zoom) def zoom_out_at_center(self): - logger.debug( - f"MCM zoom_out_at_center (GUI) called. Current zoom: {self._current_zoom_gui}" - ) - if ( - self._current_zoom_gui is None - or self._current_center_lat_gui is None - or self._current_center_lon_gui is None - ): - logger.warning( - "MCM zoom_out_at_center: Cannot zoom out - current map state (zoom/center) is not defined." - ) - return - if not self.canvas.winfo_exists(): - logger.warning( - "MCM zoom_out_at_center: Cannot zoom out - canvas not available." - ) + logger.debug(f"{self.log_prefix} zoom_out_at_center (GUI) called. Current zoom: {self._current_zoom_gui}") + if (self._current_zoom_gui is None or self._current_center_lat_gui is None or self._current_center_lon_gui is None): + logger.warning(f"{self.log_prefix} zoom_out_at_center: Cannot zoom out - current map state is not defined.") return + if not self.canvas.winfo_exists(): return new_zoom = max(map_constants.MIN_ZOOM_LEVEL, self._current_zoom_gui - 1) - logger.debug( - f"MCM zoom_out_at_center: new_zoom calculated as {new_zoom} (min_level={map_constants.MIN_ZOOM_LEVEL})" - ) if new_zoom != self._current_zoom_gui: - logger.info( - f"MCM zoom_out_at_center: Zooming out from {self._current_zoom_gui} to {new_zoom}." - ) self._current_zoom_gui = new_zoom - self._request_map_render( - self._current_center_lat_gui, self._current_center_lon_gui, new_zoom - ) - else: - logger.debug( - f"MCM zoom_out_at_center: Already at min zoom ({self._current_zoom_gui}). Cannot zoom out further." - ) + self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, new_zoom) - def pan_map_fixed_step( - self, direction: str, step_fraction: float = PAN_STEP_FRACTION - ): - logger.debug( - f"MCM pan_map_fixed_step (GUI) called. Direction: {direction}, Current center: ({self._current_center_lat_gui},{self._current_center_lon_gui}), Zoom: {self._current_zoom_gui}" - ) - if ( - self._current_center_lat_gui is None - or self._current_center_lon_gui is None - or self._current_zoom_gui is None - ): - logger.warning( - "MCM pan_map_fixed_step: Cannot pan map - current map state (center/zoom) not fully defined." - ) + def pan_map_fixed_step(self, direction: str, step_fraction: float = PAN_STEP_FRACTION): + logger.debug(f"{self.log_prefix} pan_map_fixed_step (GUI) called. Direction: {direction}") + if (self._current_center_lat_gui is None or self._current_center_lon_gui is None or self._current_zoom_gui is None): + logger.warning(f"{self.log_prefix} pan_map_fixed_step: Cannot pan map - current map state not fully defined.") return - if ( - not self.canvas.winfo_exists() - or not PYPROJ_MODULE_LOCALLY_AVAILABLE - or pyproj is None - ): - logger.warning( - "MCM pan_map_fixed_step: Cannot pan map - canvas or PyProj library not available." - ) + if (not self.canvas.winfo_exists() or not PYPROJ_MODULE_LOCALLY_AVAILABLE or map_utils.pyproj is None): + logger.warning(f"{self.log_prefix} pan_map_fixed_step: Cannot pan map - canvas or PyProj library not available.") return delta_x_pixels, delta_y_pixels = 0, 0 pan_step_px_w = int(self.canvas_width * step_fraction) pan_step_px_h = int(self.canvas_height * step_fraction) - logger.debug( - f"MCM pan_map_fixed_step: Pan step pixels W={pan_step_px_w}, H={pan_step_px_h}" - ) - if direction == "left": - delta_x_pixels = ( - -pan_step_px_w - ) # Muove la mappa a sinistra, quindi il centro si sposta a destra (+lon) - elif direction == "right": - delta_x_pixels = pan_step_px_w # Muove la mappa a destra, centro si sposta a sinistra (-lon) - elif direction == "up": - delta_y_pixels = ( - -pan_step_px_h - ) # Muove mappa su, centro si sposta giù (-lat) - elif direction == "down": - delta_y_pixels = ( - pan_step_px_h # Muove mappa giù, centro si sposta su (+lat) - ) - else: - logger.warning( - f"MCM pan_map_fixed_step: Unknown pan direction: {direction}" - ) - return + if direction == "left": delta_x_pixels = -pan_step_px_w + elif direction == "right": delta_x_pixels = pan_step_px_w + elif direction == "up": delta_y_pixels = -pan_step_px_h + elif direction == "down": delta_y_pixels = pan_step_px_h + else: logger.warning(f"{self.log_prefix} pan_map_fixed_step: Unknown pan direction: {direction}"); return - logger.debug( - f"MCM pan_map_fixed_step: Pixel deltas dx={delta_x_pixels}, dy={delta_y_pixels} (relative to current center)" - ) - - res_m_px = calculate_meters_per_pixel( - self._current_center_lat_gui, - self._current_zoom_gui, - self.tile_manager.tile_size, - ) + res_m_px = calculate_meters_per_pixel(self._current_center_lat_gui, self._current_zoom_gui, self.tile_manager.tile_size) if res_m_px is None or res_m_px <= 1e-9: - logger.error( - "MCM pan_map_fixed_step: Could not calculate valid resolution for panning. Cannot pan." - ) + logger.error(f"{self.log_prefix} pan_map_fixed_step: Could not calculate valid resolution for panning. Cannot pan.") return - logger.debug( - f"MCM pan_map_fixed_step: Resolution for panning: {res_m_px:.2f} m/px" - ) - delta_meters_x = delta_x_pixels * res_m_px # Delta in metri verso Est - delta_meters_y = ( - -delta_y_pixels * res_m_px - ) # Delta in metri verso Nord (NB: -dy perché pixel Y aumenta verso il basso) - logger.debug( - f"MCM pan_map_fixed_step: Meter deltas dMx={delta_meters_x:.2f} (East), dMy={delta_meters_y:.2f} (North)" - ) + delta_meters_x = delta_x_pixels * res_m_px + delta_meters_y = -delta_y_pixels * res_m_px + geod = map_utils.pyproj.Geod(ellps="WGS84") + current_calc_lon, current_calc_lat = self._current_center_lon_gui, self._current_center_lat_gui - geod = pyproj.Geod(ellps="WGS84") - current_calc_lon, current_calc_lat = ( - self._current_center_lon_gui, - self._current_center_lat_gui, - ) - - # Sposta prima in longitudine (Est/Ovest) if abs(delta_meters_x) > 1e-9: - azimuth_lon = 90.0 if delta_meters_x > 0 else 270.0 # 90 Est, 270 Ovest - # pyproj.Geod.fwd aspetta la distanza positiva - new_lon, _, _ = geod.fwd( - current_calc_lon, current_calc_lat, azimuth_lon, abs(delta_meters_x) - ) + azimuth_lon = 90.0 if delta_meters_x > 0 else 270.0 + new_lon, _, _ = geod.fwd(current_calc_lon, current_calc_lat, azimuth_lon, abs(delta_meters_x)) current_calc_lon = new_lon - logger.debug( - f"MCM pan_map_fixed_step: After lon shift (az={azimuth_lon}, dist={abs(delta_meters_x):.1f}m), new lon={current_calc_lon:.4f}" - ) - - # Poi sposta in latitudine (Nord/Sud) partendo dalla nuova longitudine if abs(delta_meters_y) > 1e-9: - azimuth_lat = 0.0 if delta_meters_y > 0 else 180.0 # 0 Nord, 180 Sud - _, new_lat, _ = geod.fwd( - current_calc_lon, current_calc_lat, azimuth_lat, abs(delta_meters_y) - ) + azimuth_lat = 0.0 if delta_meters_y > 0 else 180.0 + _, new_lat, _ = geod.fwd(current_calc_lon, current_calc_lat, azimuth_lat, abs(delta_meters_y)) current_calc_lat = new_lat - logger.debug( - f"MCM pan_map_fixed_step: After lat shift (az={azimuth_lat}, dist={abs(delta_meters_y):.1f}m), new lat={current_calc_lat:.4f}" - ) - MAX_MERCATOR_LAT = 85.05112878 # Limite per la proiezione Web Mercator - final_new_center_lat = max( - -MAX_MERCATOR_LAT, min(MAX_MERCATOR_LAT, current_calc_lat) - ) - # Normalizza longitudine a [-180, 180] + MAX_MERCATOR_LAT = 85.05112878 + final_new_center_lat = max(-MAX_MERCATOR_LAT, min(MAX_MERCATOR_LAT, current_calc_lat)) final_new_center_lon = (current_calc_lon + 180) % 360 - 180 - if ( - final_new_center_lon == -180 and current_calc_lon > 0 - ): # Edge case for 180 meridian - final_new_center_lon = 180.0 - - logger.debug( - f"MCM pan_map_fixed_step: Clamped & Normalized new center: ({final_new_center_lat:.4f}, {final_new_center_lon:.4f})" - ) + if final_new_center_lon == -180 and current_calc_lon > 0: final_new_center_lon = 180.0 self._current_center_lat_gui = final_new_center_lat self._current_center_lon_gui = final_new_center_lon - logger.info( - f"MCM pan_map_fixed_step: Panning map content '{direction}'. Updated GUI center to ({final_new_center_lat:.4f}, {final_new_center_lon:.4f}). Requesting render." - ) - self._request_map_render( - final_new_center_lat, final_new_center_lon, self._current_zoom_gui - ) + self._request_map_render(final_new_center_lat, final_new_center_lon, self._current_zoom_gui) - def center_map_and_fit_patch( - self, center_lat: float, center_lon: float, patch_size_km: float - ): - logger.info( - f"MCM center_map_and_fit_patch (GUI): Request to center map at ({center_lat:.4f}, {center_lon:.4f}) and fit patch of {patch_size_km}km." - ) - if ( - not self.canvas.winfo_exists() - or self.canvas_width <= 0 - or self.canvas_height <= 0 - ): - logger.error( - "MCM center_map_and_fit_patch: Cannot fit patch - canvas not ready or invalid dimensions." - ) - if self.app_controller and hasattr( - self.app_controller, "show_error_message" - ): - self.app_controller.show_error_message( - "Map Error", "Canvas not ready to fit patch." - ) + def center_map_and_fit_patch(self, center_lat: float, center_lon: float, patch_size_km: float): + logger.info(f"{self.log_prefix} center_map_and_fit_patch (GUI): Request to center map at ({center_lat:.4f}, {center_lon:.4f}) and fit patch of {patch_size_km}km.") + if (not self.canvas.winfo_exists() or self.canvas_width <= 0 or self.canvas_height <= 0): + logger.error(f"{self.log_prefix} center_map_and_fit_patch: Cannot fit patch - canvas not ready or invalid dimensions.") + if not self.is_detail_map and self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"): + self.app_controller.main_window.show_error_message("Map Error", "Canvas not ready to fit patch.") return - zoom_for_width = calculate_zoom_level_for_geographic_size( - center_lat, - patch_size_km * 1000, - self.canvas_width, - self.tile_manager.tile_size, - ) - zoom_for_height = calculate_zoom_level_for_geographic_size( - center_lat, - patch_size_km * 1000, - self.canvas_height, - self.tile_manager.tile_size, - ) - logger.debug( - f"MCM center_map_and_fit_patch: Zoom for width={zoom_for_width}, zoom for height={zoom_for_height}" - ) + zoom_for_width = calculate_zoom_level_for_geographic_size(center_lat, patch_size_km * 1000, self.canvas_width, self.tile_manager.tile_size) + zoom_for_height = calculate_zoom_level_for_geographic_size(center_lat, patch_size_km * 1000, self.canvas_height, self.tile_manager.tile_size) if zoom_for_width is None or zoom_for_height is None: - logger.error( - f"MCM center_map_and_fit_patch: Could not calculate zoom to fit patch of {patch_size_km}km. Using current or default zoom." - ) - new_zoom = ( - self._current_zoom_gui - if self._current_zoom_gui is not None - else map_constants.DEFAULT_INITIAL_ZOOM - ) - else: - new_zoom = min( - zoom_for_width, zoom_for_height - ) # Prendi il più "lontano" per farci stare tutto + logger.error(f"{self.log_prefix} center_map_and_fit_patch: Could not calculate zoom to fit patch. Using current or default zoom.") + new_zoom = self._current_zoom_gui if self._current_zoom_gui is not None else map_constants.DEFAULT_INITIAL_ZOOM + else: new_zoom = min(zoom_for_width, zoom_for_height) - max_zoom_limit_svc = ( - self.map_service.max_zoom - if self.map_service - else map_constants.DEFAULT_MAX_ZOOM_FALLBACK - ) + max_zoom_limit_svc = self.map_service.max_zoom if self.map_service else map_constants.DEFAULT_MAX_ZOOM_FALLBACK new_zoom = max(map_constants.MIN_ZOOM_LEVEL, min(new_zoom, max_zoom_limit_svc)) - logger.info( - f"MCM center_map_and_fit_patch: Calculated final zoom: {new_zoom}. Target center: ({center_lat:.4f}, {center_lon:.4f})" - ) - self._current_center_lat_gui = center_lat self._current_center_lon_gui = center_lon self._current_zoom_gui = new_zoom - logger.debug( - f"MCM center_map_and_fit_patch: Updated GUI state. Requesting render." - ) self._request_map_render(center_lat, center_lon, new_zoom) def shutdown_worker(self): - logger.info("MapCanvasManager: Shutdown worker requested.") - self._map_worker_stop_event.set() # Segnala al worker di fermarsi + logger.info(f"{self.log_prefix} Shutdown worker requested.") + self._map_worker_stop_event.set() - # Cancella il processore dei risultati nella GUI if self._gui_after_id_result_processor and self.canvas.winfo_exists(): - try: - self.canvas.after_cancel(self._gui_after_id_result_processor) - logger.debug("MapCanvasManager: Cancelled GUI result processor task.") - except Exception as e_cancel_gui_proc: - logger.warning( - f"MapCanvasManager: Error cancelling GUI result processor: {e_cancel_gui_proc}" - ) + try: self.canvas.after_cancel(self._gui_after_id_result_processor) + except Exception: pass self._gui_after_id_result_processor = None - # Invia un messaggio di shutdown speciale al worker per sbloccarlo se è in attesa sulla coda - try: - self._map_render_request_queue.put_nowait( - {"type": RENDER_REQUEST_TYPE_SHUTDOWN, "request_id": -999} - ) - logger.debug("MapCanvasManager: Sent shutdown signal to worker queue.") - except queue.Full: - logger.warning( - "MapCanvasManager: Worker request queue full, cannot send shutdown signal via queue. Worker might take longer to stop." - ) - except Exception as e_put_shutdown: - logger.error( - f"MapCanvasManager: Error sending shutdown signal to worker queue: {e_put_shutdown}" - ) + try: self._map_render_request_queue.put_nowait({"type": RENDER_REQUEST_TYPE_SHUTDOWN, "request_id": -999}) + except queue.Full: logger.warning(f"{self.log_prefix} Worker request queue full, cannot send shutdown signal via queue.") + except Exception as e: logger.error(f"{self.log_prefix} Error sending shutdown signal to worker queue: {e}") if self._map_worker_thread and self._map_worker_thread.is_alive(): - logger.info("MapCanvasManager: Waiting for map worker thread to join...") - self._map_worker_thread.join( - timeout=2.0 - ) # Aspetta per un massimo di 2 secondi - if self._map_worker_thread.is_alive(): - logger.warning( - "MapCanvasManager: Map worker thread did not join in time." - ) - else: - logger.info("MapCanvasManager: Map worker thread joined successfully.") - else: - logger.info( - "MapCanvasManager: Map worker thread was not running or already stopped." - ) - + logger.info(f"{self.log_prefix} Waiting for map worker thread to join...") + self._map_worker_thread.join(timeout=2.0) + if self._map_worker_thread.is_alive(): logger.warning(f"{self.log_prefix} Map worker thread did not join in time.") + else: logger.info(f"{self.log_prefix} Map worker thread joined successfully.") self._map_worker_thread = None - # Pulisci le code while not self._map_render_request_queue.empty(): - try: - self._map_render_request_queue.get_nowait() - self._map_render_request_queue.task_done() - except Exception: - break + try: self._map_render_request_queue.get_nowait(); self._map_render_request_queue.task_done() + except Exception: break while not self._map_render_result_queue.empty(): - try: - self._map_render_result_queue.get_nowait() - self._map_render_result_queue.task_done() - except Exception: - break - - logger.info("MapCanvasManager: Worker shutdown sequence complete.") + try: self._map_render_result_queue.get_nowait(); self._map_render_result_queue.task_done() + except Exception: break + logger.info(f"{self.log_prefix} Worker shutdown sequence complete.") \ No newline at end of file diff --git a/flightmonitor/map/map_drawing.py b/flightmonitor/map/map_drawing.py index 308253f..a89b305 100644 --- a/flightmonitor/map/map_drawing.py +++ b/flightmonitor/map/map_drawing.py @@ -740,6 +740,10 @@ def _draw_single_flight( # Determine base color for this flight (triangle and its track) 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] + + 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) --- if track_deque and len(track_deque) > 1: @@ -768,6 +772,7 @@ def _draw_single_flight( # pixel_track_points are now ordered from most recent (closest to aircraft) to oldest 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): start_point_px = pixel_track_points[i] end_point_px = pixel_track_points[i + 1] @@ -775,6 +780,8 @@ def _draw_single_flight( faded_color = _calculate_faded_color( 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: try: draw.line(