From 6c99c4cc081274cfbddae2966bc1dc2ecfc2b91e Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Tue, 3 Jun 2025 12:47:43 +0200 Subject: [PATCH] change gui details --- flightmonitor/controller/app_controller.py | 1232 ++++++++++++----- .../gui/dialogs/full_flight_details_window.py | 833 ++++------- 2 files changed, 1185 insertions(+), 880 deletions(-) diff --git a/flightmonitor/controller/app_controller.py b/flightmonitor/controller/app_controller.py index e0377d1..0be04c7 100644 --- a/flightmonitor/controller/app_controller.py +++ b/flightmonitor/controller/app_controller.py @@ -35,6 +35,7 @@ from ..utils.gui_utils import ( GUI_STATUS_FETCHING, GUI_STATUS_UNKNOWN, ) + # MODIFIED: Import _is_valid_bbox_dict from map_utils for bounding_box validation from ..map.map_utils import _is_valid_bbox_dict @@ -70,20 +71,29 @@ class AppController: self.data_storage = DataStorage() module_logger.info("DataStorage initialized successfully by AppController.") except Exception as e: - module_logger.critical(f"CRITICAL: Failed to initialize DataStorage: {e}", exc_info=True) + module_logger.critical( + f"CRITICAL: Failed to initialize DataStorage: {e}", exc_info=True + ) self.data_storage = None try: self.aircraft_db_manager = AircraftDatabaseManager() - module_logger.info("AircraftDatabaseManager initialized successfully by AppController.") + module_logger.info( + "AircraftDatabaseManager initialized successfully by AppController." + ) except Exception as e: - module_logger.critical(f"CRITICAL: Failed to initialize AircraftDatabaseManager: {e}", exc_info=True) + module_logger.critical( + f"CRITICAL: Failed to initialize AircraftDatabaseManager: {e}", + exc_info=True, + ) self.aircraft_db_manager = None module_logger.info("AppController initialized.") def set_main_window(self, main_window_instance: "MainWindow"): self.main_window = main_window_instance - module_logger.debug(f"Main window instance ({type(main_window_instance)}) set in AppController.") + module_logger.debug( + f"Main window instance ({type(main_window_instance)}) set in AppController." + ) initial_status_msg = "System Initialized. Ready." initial_status_level = GUI_STATUS_OK if not self.data_storage: @@ -100,21 +110,32 @@ class AppController: else: initial_status_msg += f" {err_msg_adb}" initial_status_level = GUI_STATUS_ERROR - - if (self.main_window and hasattr(self.main_window, "root") and - self.main_window.root.winfo_exists() and - hasattr(self.main_window, "update_semaphore_and_status")): - self.main_window.update_semaphore_and_status(initial_status_level, initial_status_msg) + + if ( + self.main_window + and hasattr(self.main_window, "root") + and self.main_window.root.winfo_exists() + and hasattr(self.main_window, "update_semaphore_and_status") + ): + self.main_window.update_semaphore_and_status( + initial_status_level, initial_status_msg + ) else: - module_logger.error("Main window not set or lacks update_semaphore_and_status during set_main_window.") + module_logger.error( + "Main window not set or lacks update_semaphore_and_status during set_main_window." + ) def _process_flight_data_queue(self): if not self.flight_data_queue: return - if not (self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists()): + if not ( + self.main_window + and hasattr(self.main_window, "root") + and self.main_window.root.winfo_exists() + ): self._gui_after_id = None return - + flight_payloads_this_cycle: List[CanonicalFlightState] = [] try: @@ -125,88 +146,153 @@ class AppController: except QueueEmpty: break except Exception as e_q_get: - module_logger.warning(f"Error getting from flight_data_queue: {e_q_get}") + module_logger.warning( + f"Error getting from flight_data_queue: {e_q_get}" + ) continue - + if message is None: continue try: message_type = message.get("type") if message_type == MSG_TYPE_FLIGHT_DATA: - flight_states_payload_chunk: Optional[List[CanonicalFlightState]] = message.get("payload") + flight_states_payload_chunk: Optional[ + List[CanonicalFlightState] + ] = message.get("payload") if flight_states_payload_chunk is not None: - flight_payloads_this_cycle.extend(flight_states_payload_chunk) + flight_payloads_this_cycle.extend( + flight_states_payload_chunk + ) if self.data_storage: saved_count = 0 for state in flight_states_payload_chunk: if not isinstance(state, CanonicalFlightState): - module_logger.warning(f"Skipping non-CanonicalFlightState object in payload: {type(state)}") + module_logger.warning( + f"Skipping non-CanonicalFlightState object in payload: {type(state)}" + ) continue try: flight_id = self.data_storage.add_or_update_flight_daily( - icao24=state.icao24, callsign=state.callsign, - origin_country=state.origin_country, detection_timestamp=state.timestamp, + icao24=state.icao24, + callsign=state.callsign, + origin_country=state.origin_country, + detection_timestamp=state.timestamp, ) if flight_id: - pos_id = self.data_storage.add_position_daily(flight_id, state) - if pos_id: saved_count += 1 + pos_id = ( + self.data_storage.add_position_daily( + flight_id, state + ) + ) + if pos_id: + saved_count += 1 except Exception as e_db_add: - module_logger.error(f"Error saving flight/position to DB for ICAO {state.icao24}: {e_db_add}", exc_info=False) + module_logger.error( + f"Error saving flight/position to DB for ICAO {state.icao24}: {e_db_add}", + exc_info=False, + ) if saved_count > 0: - module_logger.info(f"Saved {saved_count} position updates to DB from this chunk.") + module_logger.info( + f"Saved {saved_count} position updates to DB from this chunk." + ) - if (hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance is not None and - hasattr(self.main_window.map_manager_instance, "update_flights_on_map") and - self.is_live_monitoring_active and self._active_bounding_box): - self.main_window.map_manager_instance.update_flights_on_map(flight_states_payload_chunk) - - gui_message = (f"Live data: {len(flight_states_payload_chunk)} aircraft in chunk." if flight_states_payload_chunk else "Live data: No aircraft in area (chunk).") + if ( + hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance is not None + and hasattr( + self.main_window.map_manager_instance, + "update_flights_on_map", + ) + and self.is_live_monitoring_active + and self._active_bounding_box + ): + self.main_window.map_manager_instance.update_flights_on_map( + flight_states_payload_chunk + ) + + gui_message = ( + f"Live data: {len(flight_states_payload_chunk)} aircraft in chunk." + if flight_states_payload_chunk + else "Live data: No aircraft in area (chunk)." + ) if hasattr(self.main_window, "update_semaphore_and_status"): try: - self.main_window.update_semaphore_and_status(GUI_STATUS_OK, gui_message) - except tk.TclError: self._gui_after_id = None; return + self.main_window.update_semaphore_and_status( + GUI_STATUS_OK, gui_message + ) + except tk.TclError: + self._gui_after_id = None + return else: if hasattr(self.main_window, "update_semaphore_and_status"): try: - self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Received empty data payload from adapter.") - except tk.TclError: self._gui_after_id = None; return - + self.main_window.update_semaphore_and_status( + GUI_STATUS_WARNING, + "Received empty data payload from adapter.", + ) + except tk.TclError: + self._gui_after_id = None + return + elif message_type == MSG_TYPE_ADAPTER_STATUS: status_code = message.get("status_code") - gui_message_from_adapter = message.get("message", f"Adapter status: {status_code}") - gui_status_level_to_set = GUI_STATUS_UNKNOWN + gui_message_from_adapter = message.get( + "message", f"Adapter status: {status_code}" + ) + gui_status_level_to_set = GUI_STATUS_UNKNOWN action_required = None - - if status_code == STATUS_PERMANENT_FAILURE: action_required = "STOP_MONITORING" - elif status_code == STATUS_API_ERROR_TEMPORARY: gui_status_level_to_set = GUI_STATUS_ERROR - elif status_code == STATUS_RATE_LIMITED: gui_status_level_to_set = GUI_STATUS_WARNING - elif status_code == STATUS_FETCHING: gui_status_level_to_set = GUI_STATUS_FETCHING - elif status_code in [STATUS_STARTING, STATUS_RECOVERED, STATUS_STOPPED]: gui_status_level_to_set = GUI_STATUS_OK - + + if status_code == STATUS_PERMANENT_FAILURE: + action_required = "STOP_MONITORING" + elif status_code == STATUS_API_ERROR_TEMPORARY: + gui_status_level_to_set = GUI_STATUS_ERROR + elif status_code == STATUS_RATE_LIMITED: + gui_status_level_to_set = GUI_STATUS_WARNING + elif status_code == STATUS_FETCHING: + gui_status_level_to_set = GUI_STATUS_FETCHING + elif status_code in [ + STATUS_STARTING, + STATUS_RECOVERED, + STATUS_STOPPED, + ]: + gui_status_level_to_set = GUI_STATUS_OK + if hasattr(self.main_window, "update_semaphore_and_status"): try: - self.main_window.update_semaphore_and_status(gui_status_level_to_set, gui_message_from_adapter) - except tk.TclError: self._gui_after_id = None; return - + self.main_window.update_semaphore_and_status( + gui_status_level_to_set, gui_message_from_adapter + ) + except tk.TclError: + self._gui_after_id = None + return + if action_required == "STOP_MONITORING": self.stop_live_monitoring(from_error=True) break - + except Exception as e_msg_proc: - module_logger.error(f"Error processing adapter message: {e_msg_proc}", exc_info=True) + module_logger.error( + f"Error processing adapter message: {e_msg_proc}", exc_info=True + ) finally: - try: self.flight_data_queue.task_done() - except ValueError: pass - except Exception as e_task_done: module_logger.error(f"Error calling task_done on flight_data_queue: {e_task_done}") - + try: + self.flight_data_queue.task_done() + except ValueError: + pass + except Exception as e_task_done: + module_logger.error( + f"Error calling task_done on flight_data_queue: {e_task_done}" + ) + # MODIFIED: Live update for detail window - if (self.active_detail_window_ref and - self.active_detail_window_icao and - self.active_detail_window_ref.winfo_exists()): - + if ( + self.active_detail_window_ref + and self.active_detail_window_icao + and self.active_detail_window_ref.winfo_exists() + ): + flight_of_interest_updated_this_cycle = False latest_live_data_for_detail_icao: Optional[Dict[str, Any]] = None @@ -215,50 +301,77 @@ class AppController: flight_of_interest_updated_this_cycle = True latest_live_data_for_detail_icao = state_obj.to_dict() # Consider this the "most live" data for the detail view from this batch - break - + break + if flight_of_interest_updated_this_cycle: - module_logger.info(f"AppController: Flight {self.active_detail_window_icao} in detail view was in the latest data batch. Refreshing detail window.") - + module_logger.info( + f"AppController: Flight {self.active_detail_window_icao} in detail view was in the latest data batch. Refreshing detail window." + ) + static_data_upd: Optional[Dict[str, Any]] = None if self.aircraft_db_manager: - static_data_upd = self.aircraft_db_manager.get_aircraft_details(self.active_detail_window_icao) - + static_data_upd = self.aircraft_db_manager.get_aircraft_details( + self.active_detail_window_icao + ) + full_track_data_list_upd: List[Dict[str, Any]] = [] if self.data_storage: try: - current_utc_date = datetime.now(timezone.utc) # Or a date range if history is more complex - track_states_upd = self.data_storage.get_flight_track_for_icao_on_date( - self.active_detail_window_icao, current_utc_date + current_utc_date = datetime.now( + timezone.utc + ) # Or a date range if history is more complex + track_states_upd = ( + self.data_storage.get_flight_track_for_icao_on_date( + self.active_detail_window_icao, current_utc_date + ) ) if track_states_upd: - full_track_data_list_upd = [s.to_dict() for s in track_states_upd] + full_track_data_list_upd = [ + s.to_dict() for s in track_states_upd + ] except Exception as e_track_upd: - module_logger.error(f"Error retrieving updated track for detail view {self.active_detail_window_icao}: {e_track_upd}") - + module_logger.error( + f"Error retrieving updated track for detail view {self.active_detail_window_icao}: {e_track_upd}" + ) + try: self.active_detail_window_ref.update_details( - static_data_upd, latest_live_data_for_detail_icao, full_track_data_list_upd + static_data_upd, + latest_live_data_for_detail_icao, + full_track_data_list_upd, ) except tk.TclError: - module_logger.warning(f"AppController: TclError trying to update detail window for {self.active_detail_window_icao}, likely closed.") - self.details_window_closed(self.active_detail_window_icao) + module_logger.warning( + f"AppController: TclError trying to update detail window for {self.active_detail_window_icao}, likely closed." + ) + self.details_window_closed(self.active_detail_window_icao) except Exception as e_upd_detail_win: - module_logger.error(f"AppController: Error updating detail window for {self.active_detail_window_icao}: {e_upd_detail_win}", exc_info=True) + module_logger.error( + f"AppController: Error updating detail window for {self.active_detail_window_icao}: {e_upd_detail_win}", + exc_info=True, + ) except Exception as e_outer: - module_logger.error(f"Outer error in _process_flight_data_queue: {e_outer}", exc_info=True) + module_logger.error( + f"Outer error in _process_flight_data_queue: {e_outer}", exc_info=True + ) finally: - if (self.is_live_monitoring_active and - self.main_window and hasattr(self.main_window, "root") and - self.main_window.root.winfo_exists()): + if ( + self.is_live_monitoring_active + and self.main_window + and hasattr(self.main_window, "root") + and self.main_window.root.winfo_exists() + ): try: self._gui_after_id = self.main_window.root.after( GUI_QUEUE_CHECK_INTERVAL_MS, self._process_flight_data_queue ) - except tk.TclError: self._gui_after_id = None + except tk.TclError: + self._gui_after_id = None except Exception as e_after_schedule: - module_logger.error(f"Error rescheduling _process_flight_data_queue: {e_after_schedule}") + module_logger.error( + f"Error rescheduling _process_flight_data_queue: {e_after_schedule}" + ) self._gui_after_id = None else: self._gui_after_id = None @@ -274,30 +387,40 @@ class AppController: self.main_window._reset_gui_to_stopped_state(f"Start failed: {err_msg}") return - if not self.data_storage and hasattr(self.main_window, "update_semaphore_and_status"): + if not self.data_storage and hasattr( + self.main_window, "update_semaphore_and_status" + ): self.main_window.update_semaphore_and_status( GUI_STATUS_WARNING, "DataStorage N/A. History will not be saved." ) - + if self.is_live_monitoring_active: - module_logger.warning("Live monitoring requested but already active or stop in progress.") + module_logger.warning( + "Live monitoring requested but already active or stop in progress." + ) if hasattr(self.main_window, "_reset_gui_to_stopped_state"): - self.main_window._reset_gui_to_stopped_state("Monitoring stop in progress or already active.") + self.main_window._reset_gui_to_stopped_state( + "Monitoring stop in progress or already active." + ) return - module_logger.info(f"Controller: Starting live monitoring for bbox: {bounding_box}") + module_logger.info( + f"Controller: Starting live monitoring for bbox: {bounding_box}" + ) self._active_bounding_box = bounding_box - if (hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance and - hasattr(self.main_window.map_manager_instance, "set_target_bbox")): + if ( + hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + and hasattr(self.main_window.map_manager_instance, "set_target_bbox") + ): try: self.main_window.map_manager_instance.set_target_bbox(bounding_box) except Exception as e_map: module_logger.error(f"Error setting map BBox: {e_map}", exc_info=True) else: if hasattr(self.main_window, "clear_all_views_data"): - self.main_window.clear_all_views_data() + self.main_window.clear_all_views_data() if self.flight_data_queue is None: self.flight_data_queue = Queue(maxsize=200) @@ -306,8 +429,10 @@ class AppController: try: self.flight_data_queue.get_nowait() self.flight_data_queue.task_done() - except QueueEmpty: break - except Exception: break + except QueueEmpty: + break + except Exception: + break if self.live_adapter_thread and self.live_adapter_thread.is_alive(): module_logger.info("Old adapter thread found alive. Stopping it first.") @@ -319,10 +444,12 @@ class AppController: if self.live_adapter_thread.is_alive(): module_logger.warning("Old adapter thread did not stop in time.") except Exception as e_join: - module_logger.error(f"Error stopping old adapter: {e_join}", exc_info=True) + module_logger.error( + f"Error stopping old adapter: {e_join}", exc_info=True + ) finally: self.live_adapter_thread = None - + self.live_adapter_thread = OpenSkyLiveAdapter( output_queue=self.flight_data_queue, bounding_box=self._active_bounding_box, @@ -331,97 +458,160 @@ class AppController: self.is_live_monitoring_active = True self.live_adapter_thread.start() - if self._gui_after_id and self.main_window and self.main_window.root.winfo_exists(): - try: self.main_window.root.after_cancel(self._gui_after_id) - except: pass - finally: self._gui_after_id = None - + if ( + self._gui_after_id + and self.main_window + and self.main_window.root.winfo_exists() + ): + try: + self.main_window.root.after_cancel(self._gui_after_id) + except: + pass + finally: + self._gui_after_id = None + if self.main_window and self.main_window.root.winfo_exists(): - self._gui_after_id = self.main_window.root.after(100, self._process_flight_data_queue) + self._gui_after_id = self.main_window.root.after( + 100, self._process_flight_data_queue + ) else: - module_logger.error("Cannot schedule queue processor: MainWindow or root missing.") + module_logger.error( + "Cannot schedule queue processor: MainWindow or root missing." + ) self.is_live_monitoring_active = False if self.live_adapter_thread and self.live_adapter_thread.is_alive(): self.live_adapter_thread.stop() if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state("Start failed: GUI error.") - def stop_live_monitoring(self, from_error: bool = False): - if not self.is_live_monitoring_active and not (from_error and self.live_adapter_thread and self.live_adapter_thread.is_alive()): - if hasattr(self.main_window, "_reset_gui_to_stopped_state") and not from_error: - self.main_window._reset_gui_to_stopped_state("Monitoring already stopped.") + if not self.is_live_monitoring_active and not ( + from_error + and self.live_adapter_thread + and self.live_adapter_thread.is_alive() + ): + if ( + hasattr(self.main_window, "_reset_gui_to_stopped_state") + and not from_error + ): + self.main_window._reset_gui_to_stopped_state( + "Monitoring already stopped." + ) return - module_logger.info(f"Controller: Stopping live monitoring (from_error={from_error}).") + module_logger.info( + f"Controller: Stopping live monitoring (from_error={from_error})." + ) self.is_live_monitoring_active = False - if (self._gui_after_id and self.main_window and - hasattr(self.main_window, "root") and self.main_window.root.winfo_exists()): - try: self.main_window.root.after_cancel(self._gui_after_id) - except tk.TclError: pass - except Exception: pass - finally: self._gui_after_id = None - + if ( + self._gui_after_id + and self.main_window + and hasattr(self.main_window, "root") + and self.main_window.root.winfo_exists() + ): + try: + self.main_window.root.after_cancel(self._gui_after_id) + except tk.TclError: + pass + except Exception: + pass + finally: + self._gui_after_id = None + if self.live_adapter_thread and self.live_adapter_thread.is_alive(): try: self.live_adapter_thread.stop() if self.main_window and self.main_window.root.winfo_exists(): - self.main_window.root.update_idletasks() + self.main_window.root.update_idletasks() self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS) if self.live_adapter_thread.is_alive(): - module_logger.warning("Adapter thread did not stop in time on stop_live_monitoring.") + module_logger.warning( + "Adapter thread did not stop in time on stop_live_monitoring." + ) except Exception as e_join: module_logger.error(f"Error stopping adapter: {e_join}", exc_info=True) finally: self.live_adapter_thread = None - - if (self.flight_data_queue and self.main_window and - hasattr(self.main_window, "root") and self.main_window.root.winfo_exists()): + + if ( + self.flight_data_queue + and self.main_window + and hasattr(self.main_window, "root") + and self.main_window.root.winfo_exists() + ): try: # Process remaining queue items. Temporarily set active to True to allow one last run. original_active_state = self.is_live_monitoring_active - self.is_live_monitoring_active = True # Allow one last processing + self.is_live_monitoring_active = True # Allow one last processing self._process_flight_data_queue() - self.is_live_monitoring_active = original_active_state # Restore state + self.is_live_monitoring_active = original_active_state # Restore state except Exception as e_final_q: - module_logger.error(f"Error in final queue processing: {e_final_q}", exc_info=True) - + module_logger.error( + f"Error in final queue processing: {e_final_q}", exc_info=True + ) + if hasattr(self.main_window, "clear_all_views_data"): self.main_window.clear_all_views_data() - + if hasattr(self.main_window, "_reset_gui_to_stopped_state"): - msg = "Monitoring stopped due to an error." if from_error else "Monitoring stopped." - try: self.main_window._reset_gui_to_stopped_state(msg) - except Exception as e_reset: module_logger.error(f"Error resetting GUI: {e_reset}", exc_info=False) - + msg = ( + "Monitoring stopped due to an error." + if from_error + else "Monitoring stopped." + ) + try: + self.main_window._reset_gui_to_stopped_state(msg) + except Exception as e_reset: + module_logger.error(f"Error resetting GUI: {e_reset}", exc_info=False) + self._active_bounding_box = None - def on_application_exit(self): - module_logger.info("Controller: Application exit requested. Cleaning up resources.") - - if self.active_detail_window_ref and self.active_detail_window_ref.winfo_exists(): + module_logger.info( + "Controller: Application exit requested. Cleaning up resources." + ) + + if ( + self.active_detail_window_ref + and self.active_detail_window_ref.winfo_exists() + ): try: - module_logger.info(f"Closing active detail window for {self.active_detail_window_icao} on app exit.") - self.active_detail_window_ref.destroy() + module_logger.info( + f"Closing active detail window for {self.active_detail_window_icao} on app exit." + ) + self.active_detail_window_ref.destroy() except Exception as e_close_detail: - module_logger.error(f"Error closing detail window on app exit: {e_close_detail}") - finally: # Ensure these are cleared even if destroy fails for some reason + module_logger.error( + f"Error closing detail window on app exit: {e_close_detail}" + ) + finally: # Ensure these are cleared even if destroy fails for some reason self.active_detail_window_ref = None self.active_detail_window_icao = None - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance is not None): + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance is not None + ): map_manager = self.main_window.map_manager_instance - if hasattr(map_manager, "shutdown_worker") and callable(map_manager.shutdown_worker): + if hasattr(map_manager, "shutdown_worker") and callable( + map_manager.shutdown_worker + ): try: map_manager.shutdown_worker() - module_logger.info("Controller: Main MapCanvasManager worker shutdown requested.") + module_logger.info( + "Controller: Main MapCanvasManager worker shutdown requested." + ) except Exception as e_map_shutdown: - module_logger.error(f"Controller: Error during Main MapCanvasManager worker shutdown: {e_map_shutdown}", exc_info=True) - - is_adapter_considered_running = (self.live_adapter_thread and self.live_adapter_thread.is_alive()) or self.is_live_monitoring_active + module_logger.error( + f"Controller: Error during Main MapCanvasManager worker shutdown: {e_map_shutdown}", + exc_info=True, + ) + + is_adapter_considered_running = ( + self.live_adapter_thread and self.live_adapter_thread.is_alive() + ) or self.is_live_monitoring_active if is_adapter_considered_running: self.stop_live_monitoring(from_error=False) @@ -430,17 +620,24 @@ class AppController: self.data_storage.close_connection() module_logger.info("DataStorage connection closed.") except Exception as e_db_close: - module_logger.error(f"Error closing DataStorage: {e_db_close}", exc_info=True) - finally: self.data_storage = None - + module_logger.error( + f"Error closing DataStorage: {e_db_close}", exc_info=True + ) + finally: + self.data_storage = None + if self.aircraft_db_manager: try: self.aircraft_db_manager.close_connection() module_logger.info("AircraftDatabaseManager connection closed.") except Exception as e_ac_db_close: - module_logger.error(f"Error closing AircraftDatabaseManager: {e_ac_db_close}", exc_info=True) - finally: self.aircraft_db_manager = None - + module_logger.error( + f"Error closing AircraftDatabaseManager: {e_ac_db_close}", + exc_info=True, + ) + finally: + self.aircraft_db_manager = None + module_logger.info("Controller: Cleanup on application exit finished.") def start_history_monitoring(self): @@ -452,45 +649,82 @@ class AppController: if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, err_msg) if hasattr(self.main_window, "_reset_gui_to_stopped_state"): - self.main_window._reset_gui_to_stopped_state(f"History start failed: {err_msg}") + self.main_window._reset_gui_to_stopped_state( + f"History start failed: {err_msg}" + ) return - + if hasattr(self.main_window, "update_semaphore_and_status"): - self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "History mode active (placeholder).") + self.main_window.update_semaphore_and_status( + GUI_STATUS_OK, "History mode active (placeholder)." + ) module_logger.info("History monitoring started (placeholder).") - def stop_history_monitoring(self): - if not self.main_window: return + if not self.main_window: + return if hasattr(self.main_window, "update_semaphore_and_status"): - self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "History monitoring stopped.") + self.main_window.update_semaphore_and_status( + GUI_STATUS_OK, "History monitoring stopped." + ) module_logger.info("History monitoring stopped (placeholder).") if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state("History monitoring stopped.") - - def on_map_left_click(self, latitude: float, longitude: float, screen_x: int, screen_y: int): - module_logger.debug(f"Controller: Map left-clicked at Geo ({latitude:.5f}, {longitude:.5f})") + def on_map_left_click( + self, latitude: float, longitude: float, screen_x: int, screen_y: int + ): + module_logger.debug( + f"Controller: Map left-clicked at Geo ({latitude:.5f}, {longitude:.5f})" + ) if self.main_window and hasattr(self.main_window, "update_clicked_map_info"): lat_dms_str, lon_dms_str = "N/A", "N/A" try: from ..map.map_utils import deg_to_dms_string - lat_dms_str = deg_to_dms_string(latitude, "lat") if latitude is not None else "N/A" - lon_dms_str = deg_to_dms_string(longitude, "lon") if longitude is not None else "N/A" - except ImportError: module_logger.warning("map_utils.deg_to_dms_string N/A for left click.") - except Exception as e_dms: module_logger.warning(f"Error DMS for left click: {e_dms}", exc_info=False); lat_dms_str=lon_dms_str="N/A (CalcErr)" - - try: - self.main_window.update_clicked_map_info(lat_deg=latitude, lon_deg=longitude, lat_dms=lat_dms_str, lon_dms=lon_dms_str) - except tk.TclError as e_tcl: module_logger.warning(f"TclError updating map clicked info panel: {e_tcl}. GUI closing.") - except Exception as e_update: module_logger.error(f"Error updating map clicked info panel: {e_update}", exc_info=False) + lat_dms_str = ( + deg_to_dms_string(latitude, "lat") + if latitude is not None + else "N/A" + ) + lon_dms_str = ( + deg_to_dms_string(longitude, "lon") + if longitude is not None + else "N/A" + ) + except ImportError: + module_logger.warning("map_utils.deg_to_dms_string N/A for left click.") + except Exception as e_dms: + module_logger.warning( + f"Error DMS for left click: {e_dms}", exc_info=False + ) + lat_dms_str = lon_dms_str = "N/A (CalcErr)" + + try: + self.main_window.update_clicked_map_info( + lat_deg=latitude, + lon_deg=longitude, + lat_dms=lat_dms_str, + lon_dms=lon_dms_str, + ) + except tk.TclError as e_tcl: + module_logger.warning( + f"TclError updating map clicked info panel: {e_tcl}. GUI closing." + ) + except Exception as e_update: + module_logger.error( + f"Error updating map clicked info panel: {e_update}", exc_info=False + ) def request_detailed_flight_info(self, icao24: str): - normalized_icao24 = icao24.lower().strip() # Normalize here as well for safety - module_logger.info(f"Controller: Detailed info request for ICAO24: {normalized_icao24}") + normalized_icao24 = icao24.lower().strip() # Normalize here as well for safety + module_logger.info( + f"Controller: Detailed info request for ICAO24: {normalized_icao24}" + ) if not self.main_window: - module_logger.error("Controller: MainWindow not set, cannot update flight details panel.") + module_logger.error( + "Controller: MainWindow not set, cannot update flight details panel." + ) return if not normalized_icao24: @@ -502,10 +736,15 @@ class AppController: static_data_for_panel: Optional[Dict[str, Any]] = None combined_details_for_panel: Dict[str, Any] = {"icao24": normalized_icao24} - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance and - hasattr(self.main_window.map_manager_instance, "_current_flights_to_display_gui") and - hasattr(self.main_window.map_manager_instance, "_map_data_lock")): + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + and hasattr( + self.main_window.map_manager_instance, "_current_flights_to_display_gui" + ) + and hasattr(self.main_window.map_manager_instance, "_map_data_lock") + ): map_mgr = self.main_window.map_manager_instance with map_mgr._map_data_lock: for state in map_mgr._current_flights_to_display_gui: @@ -516,32 +755,45 @@ class AppController: combined_details_for_panel.update(live_data_for_panel) if self.aircraft_db_manager: - static_data_for_panel = self.aircraft_db_manager.get_aircraft_details(normalized_icao24) + static_data_for_panel = self.aircraft_db_manager.get_aircraft_details( + normalized_icao24 + ) if static_data_for_panel: for k, v in static_data_for_panel.items(): if k not in combined_details_for_panel: combined_details_for_panel[k] = v - + if hasattr(self.main_window, "update_selected_flight_details"): self.main_window.update_selected_flight_details(combined_details_for_panel) - - def import_aircraft_database_from_file_with_progress(self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"): - module_logger.info(f"Controller: Requesting aircraft DB import with progress from: {csv_filepath}") + def import_aircraft_database_from_file_with_progress( + self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog" + ): + module_logger.info( + f"Controller: Requesting aircraft DB import with progress from: {csv_filepath}" + ) if not self.aircraft_db_manager: - module_logger.error("AircraftDatabaseManager not initialized. Cannot import.") + module_logger.error( + "AircraftDatabaseManager not initialized. Cannot import." + ) if progress_dialog_ref and progress_dialog_ref.winfo_exists(): - progress_dialog_ref.import_finished(False, "Error: Database manager not active.") + progress_dialog_ref.import_finished( + False, "Error: Database manager not active." + ) elif self.main_window and hasattr(self.main_window, "show_error_message"): - self.main_window.show_error_message("Database Error", "Aircraft database manager not active.") + self.main_window.show_error_message( + "Database Error", "Aircraft database manager not active." + ) return if not self.main_window or not self.main_window.root.winfo_exists(): module_logger.error("MainWindow not available to start import thread.") if progress_dialog_ref and progress_dialog_ref.winfo_exists(): - progress_dialog_ref.import_finished(False, "Error: Main application window not available.") + progress_dialog_ref.import_finished( + False, "Error: Main application window not available." + ) return - + if progress_dialog_ref and progress_dialog_ref.winfo_exists(): progress_dialog_ref.import_started() @@ -556,62 +808,112 @@ class AppController: def _count_csv_rows(self, csv_filepath: str) -> Optional[int]: try: current_limit = csv.field_size_limit() - new_limit_target = 10 * 1024 * 1024 + new_limit_target = 10 * 1024 * 1024 if new_limit_target > current_limit: csv.field_size_limit(new_limit_target) - except Exception: pass # Ignore errors setting limit + except Exception: + pass # Ignore errors setting limit try: with open(csv_filepath, "r", encoding="utf-8-sig") as f: reader = csv.reader(f) try: - next(reader) - except StopIteration: return 0 + next(reader) + except StopIteration: + return 0 return sum(1 for _ in reader) - except FileNotFoundError: return None - except csv.Error: return None - except Exception: return None + except FileNotFoundError: + return None + except csv.Error: + return None + except Exception: + return None - def _perform_db_import_with_progress_threaded(self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"): + def _perform_db_import_with_progress_threaded( + self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog" + ): if not self.aircraft_db_manager: module_logger.error("AircraftDBManager N/A in import thread.") - if (progress_dialog_ref and progress_dialog_ref.winfo_exists() and - self.main_window and self.main_window.root.winfo_exists()): - self.main_window.root.after(0, lambda: progress_dialog_ref.import_finished(False, "Internal Error: DB Manager missing.")) + if ( + progress_dialog_ref + and progress_dialog_ref.winfo_exists() + and self.main_window + and self.main_window.root.winfo_exists() + ): + self.main_window.root.after( + 0, + lambda: progress_dialog_ref.import_finished( + False, "Internal Error: DB Manager missing." + ), + ) return - + module_logger.info(f"Import thread: Starting row count for: {csv_filepath}") def schedule_gui_update(callable_func: Callable, *args: Any): - if (self.main_window and hasattr(self.main_window, "root") and - self.main_window.root.winfo_exists()): + if ( + self.main_window + and hasattr(self.main_window, "root") + and self.main_window.root.winfo_exists() + ): try: self.main_window.root.after(0, lambda: callable_func(*args)) except Exception as e_after: - module_logger.error(f"Error scheduling GUI update from import thread: {e_after}") + module_logger.error( + f"Error scheduling GUI update from import thread: {e_after}" + ) total_data_rows = self._count_csv_rows(csv_filepath) if total_data_rows is None: if progress_dialog_ref and progress_dialog_ref.winfo_exists(): - schedule_gui_update(progress_dialog_ref.import_finished, False, f"Error: Could not read/count rows in '{os.path.basename(csv_filepath)}'. Check logs.") + schedule_gui_update( + progress_dialog_ref.import_finished, + False, + f"Error: Could not read/count rows in '{os.path.basename(csv_filepath)}'. Check logs.", + ) return if total_data_rows == 0: if progress_dialog_ref and progress_dialog_ref.winfo_exists(): - schedule_gui_update(progress_dialog_ref.update_progress, 0, 0, 0, f"File '{os.path.basename(csv_filepath)}' is empty or header-only.") - schedule_gui_update(progress_dialog_ref.import_finished, True, "Import complete: No data rows to import.") + schedule_gui_update( + progress_dialog_ref.update_progress, + 0, + 0, + 0, + f"File '{os.path.basename(csv_filepath)}' is empty or header-only.", + ) + schedule_gui_update( + progress_dialog_ref.import_finished, + True, + "Import complete: No data rows to import.", + ) return - - if progress_dialog_ref and progress_dialog_ref.winfo_exists(): - schedule_gui_update(progress_dialog_ref.update_progress, 0, 0, total_data_rows, f"Found {total_data_rows} data rows. Starting import from '{os.path.basename(csv_filepath)}'...") - def import_progress_update_for_dialog_from_controller(processed_csv_rows, imported_db_rows, total_for_cb): + if progress_dialog_ref and progress_dialog_ref.winfo_exists(): + schedule_gui_update( + progress_dialog_ref.update_progress, + 0, + 0, + total_data_rows, + f"Found {total_data_rows} data rows. Starting import from '{os.path.basename(csv_filepath)}'...", + ) + + def import_progress_update_for_dialog_from_controller( + processed_csv_rows, imported_db_rows, total_for_cb + ): if progress_dialog_ref and progress_dialog_ref.winfo_exists(): - schedule_gui_update(progress_dialog_ref.update_progress, processed_csv_rows, imported_db_rows, total_for_cb, f"Importing CSV row {processed_csv_rows}...") + schedule_gui_update( + progress_dialog_ref.update_progress, + processed_csv_rows, + imported_db_rows, + total_for_cb, + f"Importing CSV row {processed_csv_rows}...", + ) processed_final, imported_final = self.aircraft_db_manager.import_from_csv( - csv_filepath, replace_existing=True, + csv_filepath, + replace_existing=True, progress_callback=import_progress_update_for_dialog_from_controller, - total_rows_for_callback=total_data_rows + total_rows_for_callback=total_data_rows, ) final_message = f"DB Import complete. Processed: {processed_final}, Imported/Updated: {imported_final}." @@ -621,183 +923,354 @@ class AppController: elif imported_final == 0 and processed_final == 0 and total_data_rows > 0: final_message = f"DB Import failed. Could not process rows from '{os.path.basename(csv_filepath)}'." success = False - + if progress_dialog_ref and progress_dialog_ref.winfo_exists(): - schedule_gui_update(progress_dialog_ref.update_progress, processed_final, imported_final, total_data_rows, "Finalizing import...") - schedule_gui_update(progress_dialog_ref.import_finished, success, final_message) + schedule_gui_update( + progress_dialog_ref.update_progress, + processed_final, + imported_final, + total_data_rows, + "Finalizing import...", + ) + schedule_gui_update( + progress_dialog_ref.import_finished, success, final_message + ) def request_and_show_full_flight_details(self, icao24: str): normalized_icao24 = icao24.lower().strip() - module_logger.info(f"Controller: Requesting to show full details for ICAO24: {normalized_icao24}") + module_logger.info( + f"Controller: Requesting to show full details for ICAO24: {normalized_icao24}" + ) - if not self.main_window or not hasattr(self.main_window, "root") or not self.main_window.root.winfo_exists(): - module_logger.error("Controller: MainWindow not available to show full flight details.") + if ( + not self.main_window + or not hasattr(self.main_window, "root") + or not self.main_window.root.winfo_exists() + ): + module_logger.error( + "Controller: MainWindow not available to show full flight details." + ) return - + if not normalized_icao24: module_logger.warning("Controller: Empty ICAO24 for full details request.") if hasattr(self.main_window, "show_info_message"): - self.main_window.show_info_message("Flight Details", "No ICAO24 provided for full details.") + self.main_window.show_info_message( + "Flight Details", "No ICAO24 provided for full details." + ) return # MODIFIED: Close existing detail window before opening a new one - if self.active_detail_window_ref and self.active_detail_window_ref.winfo_exists(): + if ( + self.active_detail_window_ref + and self.active_detail_window_ref.winfo_exists() + ): # Do not destroy if it's for the same ICAO and already open (optional behavior) # For now, always destroy and recreate for simplicity of state. if self.active_detail_window_icao == normalized_icao24: - module_logger.info(f"Detail window for {normalized_icao24} already open. Re-focusing/Re-populating.") - self.active_detail_window_ref.lift() - self.active_detail_window_ref.focus_set() - # No need to re-create, just update its content with potentially newer data + module_logger.info( + f"Detail window for {normalized_icao24} already open. Re-focusing/Re-populating." + ) + self.active_detail_window_ref.lift() + self.active_detail_window_ref.focus_set() + # No need to re-create, just update its content with potentially newer data else: - module_logger.info(f"Closing existing detail window for {self.active_detail_window_icao} before opening new one for {normalized_icao24}.") + module_logger.info( + f"Closing existing detail window for {self.active_detail_window_icao} before opening new one for {normalized_icao24}." + ) try: - self.active_detail_window_ref.destroy() - except tk.TclError: pass + self.active_detail_window_ref.destroy() + except tk.TclError: + pass # details_window_closed will clear self.active_detail_window_ref & icao static_data: Optional[Dict[str, Any]] = None if self.aircraft_db_manager: - static_data = self.aircraft_db_manager.get_aircraft_details(normalized_icao24) + static_data = self.aircraft_db_manager.get_aircraft_details( + normalized_icao24 + ) live_data: Optional[Dict[str, Any]] = None - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance and - hasattr(self.main_window.map_manager_instance, "_current_flights_to_display_gui") and - hasattr(self.main_window.map_manager_instance, "_map_data_lock")): + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + and hasattr( + self.main_window.map_manager_instance, "_current_flights_to_display_gui" + ) + and hasattr(self.main_window.map_manager_instance, "_map_data_lock") + ): map_mgr = self.main_window.map_manager_instance with map_mgr._map_data_lock: for state in map_mgr._current_flights_to_display_gui: if state.icao24 == normalized_icao24: live_data = state.to_dict() break - + full_track_data_list: List[Dict[str, Any]] = [] if self.data_storage: try: current_utc_date = datetime.now(timezone.utc) - track_states = self.data_storage.get_flight_track_for_icao_on_date(normalized_icao24, current_utc_date) + track_states = self.data_storage.get_flight_track_for_icao_on_date( + normalized_icao24, current_utc_date + ) if track_states: full_track_data_list = [state.to_dict() for state in track_states] except Exception as e_track: - module_logger.error(f"FullDetails: Error retrieving historical track for {normalized_icao24}: {e_track}", exc_info=True) - + module_logger.error( + f"FullDetails: Error retrieving historical track for {normalized_icao24}: {e_track}", + exc_info=True, + ) + try: from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow # If we decided to re-focus/re-populate, and the window still exists - if self.active_detail_window_ref and self.active_detail_window_icao == normalized_icao24 and self.active_detail_window_ref.winfo_exists(): + if ( + self.active_detail_window_ref + and self.active_detail_window_icao == normalized_icao24 + and self.active_detail_window_ref.winfo_exists() + ): details_win = self.active_detail_window_ref - else: # Create new - details_win = FullFlightDetailsWindow(self.main_window.root, normalized_icao24, self) + else: # Create new + details_win = FullFlightDetailsWindow( + self.main_window.root, normalized_icao24, self + ) self.active_detail_window_ref = details_win self.active_detail_window_icao = normalized_icao24 - if self.main_window: self.main_window.full_flight_details_window = details_win + if self.main_window: + self.main_window.full_flight_details_window = details_win details_win.update_details(static_data, live_data, full_track_data_list) except ImportError: - module_logger.error("FullFlightDetailsWindow class not found. Cannot display full details.") + module_logger.error( + "FullFlightDetailsWindow class not found. Cannot display full details." + ) if hasattr(self.main_window, "show_error_message"): - self.main_window.show_error_message("UI Error", "Could not open full details window (import error).") + self.main_window.show_error_message( + "UI Error", "Could not open full details window (import error)." + ) except Exception as e_show_details: - module_logger.error(f"Error showing full flight details window for {normalized_icao24}: {e_show_details}", exc_info=True) + module_logger.error( + f"Error showing full flight details window for {normalized_icao24}: {e_show_details}", + exc_info=True, + ) if hasattr(self.main_window, "show_error_message"): - self.main_window.show_error_message("Error", f"Could not display full details: {e_show_details}") - self.active_detail_window_ref = None # Clear refs if creation/update failed + self.main_window.show_error_message( + "Error", f"Could not display full details: {e_show_details}" + ) + self.active_detail_window_ref = None # Clear refs if creation/update failed self.active_detail_window_icao = None - if self.main_window: self.main_window.full_flight_details_window = None - + if self.main_window: + self.main_window.full_flight_details_window = None def details_window_closed(self, closed_icao24: str): normalized_closed_icao24 = closed_icao24.lower().strip() # Important: check if the closed ICAO matches the one we are actively tracking if self.active_detail_window_icao == normalized_closed_icao24: - module_logger.info(f"AppController: Detail window for {normalized_closed_icao24} reported closed. Clearing references.") + module_logger.info( + f"AppController: Detail window for {normalized_closed_icao24} reported closed. Clearing references." + ) self.active_detail_window_ref = None self.active_detail_window_icao = None # Also clear MainWindow's convenience reference if it matches - if (self.main_window and hasattr(self.main_window, "full_flight_details_window") and - self.main_window.full_flight_details_window and - not self.main_window.full_flight_details_window.winfo_exists()): # Check if already destroyed - self.main_window.full_flight_details_window = None + if ( + self.main_window + and hasattr(self.main_window, "full_flight_details_window") + and self.main_window.full_flight_details_window + and not self.main_window.full_flight_details_window.winfo_exists() + ): # Check if already destroyed + self.main_window.full_flight_details_window = None else: # This case can happen if a new detail window was opened before the old one finished its close notification - module_logger.debug(f"AppController: A detail window for {normalized_closed_icao24} closed, but it was not the currently tracked active one ({self.active_detail_window_icao}). No action on active_detail references.") + module_logger.debug( + f"AppController: A detail window for {normalized_closed_icao24} closed, but it was not the currently tracked active one ({self.active_detail_window_icao}). No action on active_detail references." + ) - - def on_map_right_click(self, latitude: float, longitude: float, screen_x: int, screen_y: int ): - module_logger.debug(f"Controller: Map right-clicked at Geo ({latitude:.5f}, {longitude:.5f})") + def on_map_right_click( + self, latitude: float, longitude: float, screen_x: int, screen_y: int + ): + module_logger.debug( + f"Controller: Map right-clicked at Geo ({latitude:.5f}, {longitude:.5f})" + ) if self.main_window: if hasattr(self.main_window, "update_clicked_map_info"): lat_dms_str, lon_dms_str = "N/A", "N/A" try: from ..map.map_utils import deg_to_dms_string - lat_dms_str = deg_to_dms_string(latitude, "lat") if latitude is not None else "N/A" - lon_dms_str = deg_to_dms_string(longitude, "lon") if longitude is not None else "N/A" - except ImportError: module_logger.warning("map_utils.deg_to_dms_string N/A for right click.") - except Exception as e_dms: module_logger.warning(f"Error DMS for right click: {e_dms}", exc_info=False); lat_dms_str=lon_dms_str="N/A (CalcErr)" + + lat_dms_str = ( + deg_to_dms_string(latitude, "lat") + if latitude is not None + else "N/A" + ) + lon_dms_str = ( + deg_to_dms_string(longitude, "lon") + if longitude is not None + else "N/A" + ) + except ImportError: + module_logger.warning( + "map_utils.deg_to_dms_string N/A for right click." + ) + except Exception as e_dms: + module_logger.warning( + f"Error DMS for right click: {e_dms}", exc_info=False + ) + lat_dms_str = lon_dms_str = "N/A (CalcErr)" try: - self.main_window.update_clicked_map_info(lat_deg=latitude, lon_deg=longitude, lat_dms=lat_dms_str, lon_dms=lon_dms_str) - except tk.TclError as e_tcl: module_logger.warning(f"TclError updating map clicked info (right): {e_tcl}. GUI closing.") - except Exception as e_update: module_logger.error(f"Error updating map clicked info (right): {e_update}", exc_info=False) + self.main_window.update_clicked_map_info( + lat_deg=latitude, + lon_deg=longitude, + lat_dms=lat_dms_str, + lon_dms=lon_dms_str, + ) + except tk.TclError as e_tcl: + module_logger.warning( + f"TclError updating map clicked info (right): {e_tcl}. GUI closing." + ) + except Exception as e_update: + module_logger.error( + f"Error updating map clicked info (right): {e_update}", + exc_info=False, + ) if hasattr(self.main_window, "show_map_context_menu"): try: - self.main_window.show_map_context_menu(latitude, longitude, screen_x, screen_y) - except tk.TclError as e_tcl_menu: module_logger.warning(f"TclError showing map context menu: {e_tcl_menu}. GUI closing.") - except Exception as e_menu: module_logger.error(f"Error showing map context menu: {e_menu}", exc_info=False) + self.main_window.show_map_context_menu( + latitude, longitude, screen_x, screen_y + ) + except tk.TclError as e_tcl_menu: + module_logger.warning( + f"TclError showing map context menu: {e_tcl_menu}. GUI closing." + ) + except Exception as e_menu: + module_logger.error( + f"Error showing map context menu: {e_menu}", exc_info=False + ) else: module_logger.warning("Main window N/A for right click handling.") - def on_map_context_menu_request(self, latitude: float, longitude: float, screen_x: int, screen_y: int): - module_logger.debug(f"Controller received context menu request for ({latitude:.5f}, {longitude:.5f}) at screen ({screen_x}, {screen_y}).") + def on_map_context_menu_request( + self, latitude: float, longitude: float, screen_x: int, screen_y: int + ): + module_logger.debug( + f"Controller received context menu request for ({latitude:.5f}, {longitude:.5f}) at screen ({screen_x}, {screen_y})." + ) if self.main_window and hasattr(self.main_window, "show_map_context_menu"): try: - self.main_window.show_map_context_menu(latitude, longitude, screen_x, screen_y) - except tk.TclError as e_tcl: module_logger.warning(f"TclError delegating show_map_context_menu: {e_tcl}. GUI closing.") - except Exception as e_menu: module_logger.error(f"Error delegating show_map_context_menu: {e_menu}", exc_info=False) + self.main_window.show_map_context_menu( + latitude, longitude, screen_x, screen_y + ) + except tk.TclError as e_tcl: + module_logger.warning( + f"TclError delegating show_map_context_menu: {e_tcl}. GUI closing." + ) + except Exception as e_menu: + module_logger.error( + f"Error delegating show_map_context_menu: {e_menu}", exc_info=False + ) else: module_logger.warning("Main window N/A to show context menu.") def recenter_map_at_coords(self, lat: float, lon: float): - module_logger.info(f"Controller request: recenter map at ({lat:.5f}, {lon:.5f}).") - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance): + module_logger.info( + f"Controller request: recenter map at ({lat:.5f}, {lon:.5f})." + ) + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance if hasattr(map_manager, "recenter_map_at_coords"): - try: map_manager.recenter_map_at_coords(lat, lon) - except Exception as e_recenter: module_logger.error(f"Error map_manager.recenter_map_at_coords: {e_recenter}", exc_info=False) - else: module_logger.warning("MapCanvasManager missing 'recenter_map_at_coords' method.") - else: module_logger.warning("Main window or MapCanvasManager N/A to recenter map.") + try: + map_manager.recenter_map_at_coords(lat, lon) + except Exception as e_recenter: + module_logger.error( + f"Error map_manager.recenter_map_at_coords: {e_recenter}", + exc_info=False, + ) + else: + module_logger.warning( + "MapCanvasManager missing 'recenter_map_at_coords' method." + ) + else: + module_logger.warning( + "Main window or MapCanvasManager N/A to recenter map." + ) - def set_bbox_around_coords(self, center_lat: float, center_lon: float, area_size_km: float = DEFAULT_CLICK_AREA_SIZE_KM): - module_logger.info(f"Controller request: set BBox ({area_size_km:.1f}km) around ({center_lat:.5f}, {center_lon:.5f}).") - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance): + def set_bbox_around_coords( + self, + center_lat: float, + center_lon: float, + area_size_km: float = DEFAULT_CLICK_AREA_SIZE_KM, + ): + module_logger.info( + f"Controller request: set BBox ({area_size_km:.1f}km) around ({center_lat:.5f}, {center_lon:.5f})." + ) + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance if hasattr(map_manager, "set_bbox_around_coords"): - try: map_manager.set_bbox_around_coords(center_lat, center_lon, area_size_km) - except Exception as e_set_bbox: module_logger.error(f"Error map_manager.set_bbox_around_coords: {e_set_bbox}", exc_info=False) - else: module_logger.warning("MapCanvasManager missing 'set_bbox_around_coords' method.") - else: module_logger.warning("Main window or MapCanvasManager N/A to set BBox.") + try: + map_manager.set_bbox_around_coords( + center_lat, center_lon, area_size_km + ) + except Exception as e_set_bbox: + module_logger.error( + f"Error map_manager.set_bbox_around_coords: {e_set_bbox}", + exc_info=False, + ) + else: + module_logger.warning( + "MapCanvasManager missing 'set_bbox_around_coords' method." + ) + else: + module_logger.warning("Main window or MapCanvasManager N/A to set BBox.") def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): - module_logger.debug(f"Controller request: update BBox GUI fields with: {bbox_dict}") + module_logger.debug( + f"Controller request: update BBox GUI fields with: {bbox_dict}" + ) if self.main_window and hasattr(self.main_window, "update_bbox_gui_fields"): - try: self.main_window.update_bbox_gui_fields(bbox_dict) - except tk.TclError as e_tcl: module_logger.warning(f"TclError updating BBox GUI fields: {e_tcl}. GUI closing.") - except Exception as e_update: module_logger.error(f"Error updating BBox GUI fields: {e_update}", exc_info=False) - else: module_logger.warning("Main window N/A to update BBox GUI fields.") + try: + self.main_window.update_bbox_gui_fields(bbox_dict) + except tk.TclError as e_tcl: + module_logger.warning( + f"TclError updating BBox GUI fields: {e_tcl}. GUI closing." + ) + except Exception as e_update: + module_logger.error( + f"Error updating BBox GUI fields: {e_update}", exc_info=False + ) + else: + module_logger.warning("Main window N/A to update BBox GUI fields.") def update_general_map_info(self): module_logger.debug("Controller: Request to update general map info.") - if not (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance): - if self.main_window and hasattr(self.main_window, "update_general_map_info_display"): + if not ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + ): + if self.main_window and hasattr( + self.main_window, "update_general_map_info_display" + ): try: - self.main_window.update_general_map_info_display(zoom=None, map_size_str="N/A", map_geo_bounds=None, target_bbox_input=None, flight_count=None) - except Exception: pass + self.main_window.update_general_map_info_display( + zoom=None, + map_size_str="N/A", + map_geo_bounds=None, + target_bbox_input=None, + flight_count=None, + ) + except Exception: + pass return map_manager: "MapCanvasManager" = self.main_window.map_manager_instance @@ -816,78 +1289,169 @@ class AppController: try: decimals = getattr(app_config, "MAP_SIZE_KM_DECIMAL_PLACES", 1) map_size_str = f"{map_size_km_w:.{decimals}f}km x {map_size_km_h:.{decimals}f}km" - except Exception as e_format: module_logger.warning(f"Error formatting map size: {e_format}. Using N/A.", exc_info=False); map_size_str = "N/A (FormatErr)" - + except Exception as e_format: + module_logger.warning( + f"Error formatting map size: {e_format}. Using N/A.", + exc_info=False, + ) + map_size_str = "N/A (FormatErr)" + if hasattr(self.main_window, "update_general_map_info_display"): try: self.main_window.update_general_map_info_display( - zoom=zoom, map_size_str=map_size_str, map_geo_bounds=map_geo_bounds, - target_bbox_input=target_bbox_input_from_map_info, flight_count=flight_count + zoom=zoom, + map_size_str=map_size_str, + map_geo_bounds=map_geo_bounds, + target_bbox_input=target_bbox_input_from_map_info, + flight_count=flight_count, + ) + except tk.TclError as e_tcl: + module_logger.warning( + f"TclError updating general map info panel: {e_tcl}. GUI closing." + ) + except Exception as e_update: + module_logger.error( + f"Error updating general map info panel: {e_update}", + exc_info=False, ) - except tk.TclError as e_tcl: module_logger.warning(f"TclError updating general map info panel: {e_tcl}. GUI closing.") - except Exception as e_update: module_logger.error(f"Error updating general map info panel: {e_update}", exc_info=False) except Exception as e_get_info: - module_logger.error(f"Error getting map info from map manager in AppController: {e_get_info}", exc_info=True) + module_logger.error( + f"Error getting map info from map manager in AppController: {e_get_info}", + exc_info=True, + ) if hasattr(self.main_window, "update_general_map_info_display"): - try: self.main_window.update_general_map_info_display(zoom=None, map_size_str="N/A", map_geo_bounds=None, target_bbox_input=None, flight_count=None) - except Exception: pass + try: + self.main_window.update_general_map_info_display( + zoom=None, + map_size_str="N/A", + map_geo_bounds=None, + target_bbox_input=None, + flight_count=None, + ) + except Exception: + pass else: - module_logger.warning("MapCanvasManager missing 'get_current_map_info' method.") + module_logger.warning( + "MapCanvasManager missing 'get_current_map_info' method." + ) def map_zoom_in(self): module_logger.debug("Controller: Map Zoom In requested.") - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance): + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance if hasattr(map_manager, "zoom_in_at_center"): - try: map_manager.zoom_in_at_center() - except Exception as e: module_logger.error(f"Error map_manager.zoom_in_at_center: {e}", exc_info=False) - else: module_logger.warning("MapCanvasManager missing 'zoom_in_at_center' method.") - else: module_logger.warning("Map manager N/A for zoom in.") + try: + map_manager.zoom_in_at_center() + except Exception as e: + module_logger.error( + f"Error map_manager.zoom_in_at_center: {e}", exc_info=False + ) + else: + module_logger.warning( + "MapCanvasManager missing 'zoom_in_at_center' method." + ) + else: + module_logger.warning("Map manager N/A for zoom in.") def map_zoom_out(self): module_logger.debug("Controller: Map Zoom Out requested.") - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance): + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance if hasattr(map_manager, "zoom_out_at_center"): - try: map_manager.zoom_out_at_center() - except Exception as e: module_logger.error(f"Error map_manager.zoom_out_at_center: {e}", exc_info=False) - else: module_logger.warning("MapCanvasManager missing 'zoom_out_at_center' method.") - else: module_logger.warning("Map manager N/A for zoom out.") + try: + map_manager.zoom_out_at_center() + except Exception as e: + module_logger.error( + f"Error map_manager.zoom_out_at_center: {e}", exc_info=False + ) + else: + module_logger.warning( + "MapCanvasManager missing 'zoom_out_at_center' method." + ) + else: + module_logger.warning("Map manager N/A for zoom out.") def map_pan_direction(self, direction: str): module_logger.debug(f"Controller: Map Pan '{direction}' requested.") - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance): + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance if hasattr(map_manager, "pan_map_fixed_step"): - try: map_manager.pan_map_fixed_step(direction) - except Exception as e: module_logger.error(f"Error map_manager.pan_map_fixed_step('{direction}'): {e}", exc_info=False) - else: module_logger.warning("MapCanvasManager missing 'pan_map_fixed_step' method.") - else: module_logger.warning(f"Map manager N/A for pan {direction}.") + try: + map_manager.pan_map_fixed_step(direction) + except Exception as e: + module_logger.error( + f"Error map_manager.pan_map_fixed_step('{direction}'): {e}", + exc_info=False, + ) + else: + module_logger.warning( + "MapCanvasManager missing 'pan_map_fixed_step' method." + ) + else: + module_logger.warning(f"Map manager N/A for pan {direction}.") - def map_center_on_coords_and_fit_patch(self, lat: float, lon: float, patch_size_km: float): - module_logger.debug(f"Controller: Center map at ({lat:.4f},{lon:.4f}) and fit {patch_size_km}km patch.") - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance): + def map_center_on_coords_and_fit_patch( + self, lat: float, lon: float, patch_size_km: float + ): + module_logger.debug( + f"Controller: Center map at ({lat:.4f},{lon:.4f}) and fit {patch_size_km}km patch." + ) + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance + ): map_manager: "MapCanvasManager" = self.main_window.map_manager_instance if hasattr(map_manager, "center_map_and_fit_patch"): - try: map_manager.center_map_and_fit_patch(lat, lon, patch_size_km) - except Exception as e: module_logger.error(f"Error map_manager.center_map_and_fit_patch: {e}", exc_info=False) - else: module_logger.warning("MapCanvasManager missing 'center_map_and_fit_patch' method.") - else: module_logger.warning("Map manager N/A for center and fit patch.") + try: + map_manager.center_map_and_fit_patch(lat, lon, patch_size_km) + except Exception as e: + module_logger.error( + f"Error map_manager.center_map_and_fit_patch: {e}", + exc_info=False, + ) + else: + module_logger.warning( + "MapCanvasManager missing 'center_map_and_fit_patch' method." + ) + else: + module_logger.warning("Map manager N/A for center and fit patch.") def set_map_track_length(self, length: int): - module_logger.info(f"Controller: Request to set map track length to {length} points.") - if (self.main_window and hasattr(self.main_window, "map_manager_instance") and - self.main_window.map_manager_instance is not None and - hasattr(self.main_window.map_manager_instance, "set_max_track_points")): + module_logger.info( + f"Controller: Request to set map track length to {length} points." + ) + if ( + self.main_window + and hasattr(self.main_window, "map_manager_instance") + and self.main_window.map_manager_instance is not None + and hasattr(self.main_window.map_manager_instance, "set_max_track_points") + ): try: self.main_window.map_manager_instance.set_max_track_points(length) except Exception as e: - module_logger.error(f"Error calling map_manager.set_max_track_points({length}): {e}", exc_info=True) + module_logger.error( + f"Error calling map_manager.set_max_track_points({length}): {e}", + exc_info=True, + ) if hasattr(self.main_window, "show_error_message"): - self.main_window.show_error_message("Map Configuration Error", f"Failed to set track length on map: {e}") + self.main_window.show_error_message( + "Map Configuration Error", + f"Failed to set track length on map: {e}", + ) else: - module_logger.warning("MapCanvasManager or 'set_max_track_points' N/A to set track length.") \ No newline at end of file + module_logger.warning( + "MapCanvasManager or 'set_max_track_points' N/A to set track length." + ) diff --git a/flightmonitor/gui/dialogs/full_flight_details_window.py b/flightmonitor/gui/dialogs/full_flight_details_window.py index 42d1c48..655f367 100644 --- a/flightmonitor/gui/dialogs/full_flight_details_window.py +++ b/flightmonitor/gui/dialogs/full_flight_details_window.py @@ -6,24 +6,27 @@ from datetime import datetime, timezone 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 +# Assumendo che MapCanvasManager e le utility siano nei percorsi corretti +# Se questi import falliscono, MapCanvasManager non sarà disponibile +try: + from ...map.map_canvas_manager import MapCanvasManager + MAP_MANAGER_AVAILABLE = True +except ImportError: + MapCanvasManager = None # type: ignore + MAP_MANAGER_AVAILABLE = False + +from ...map.map_utils import _is_valid_bbox_dict # Usato per validare BBox from ...data.common_models import CanonicalFlightState from collections import deque try: from ...utils.logger import get_logger - logger = get_logger(__name__) except ImportError: import logging - logger = logging.getLogger(__name__) - if not logger.hasHandlers(): - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s: %(message)s", - ) + if not logger.hasHandlers(): # Configurazione di base se il logger dell'app non è disponibile + 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 @@ -32,98 +35,69 @@ DEFAULT_SINGLE_POINT_PATCH_KM = 20.0 class FullFlightDetailsWindow(tk.Toplevel): def __init__(self, parent, icao24: str, controller: Optional[Any] = None): super().__init__(parent) - - self.icao24 = icao24.lower().strip() + + self.icao24 = icao24.lower().strip() self.title(f"Full Details - {self.icao24.upper()}") self.parent = parent - self.controller = controller # Store the controller instance + self.controller = controller 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) + self.geometry("1100x700") + self.minsize(850, 550) - main_v_pane = ttk.PanedWindow(self, orient=tk.VERTICAL) - main_v_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + self.main_h_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL) + self.main_h_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - top_info_frame = ttk.Frame(main_v_pane, padding=5) - main_v_pane.add(top_info_frame, weight=30) + # --- Colonna Sinistra (Info testuali e Immagine) --- + left_column_frame = ttk.Frame(self.main_h_pane, padding=5) + self.main_h_pane.add(left_column_frame, weight=4) # 40% circa - top_info_frame.columnconfigure(0, weight=3) - top_info_frame.columnconfigure(1, weight=2) + static_details_container = ttk.LabelFrame(left_column_frame, text=f"Aircraft Information ({self.icao24.upper()})", padding=10) + static_details_container.pack(fill=tk.X, expand=False, pady=(0,10), anchor='n') - 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] = {} + self._create_details_layout(static_details_container) - self.detail_labels: Dict[str, ttk.Label] = {} - self._create_details_layout(static_details_container) - - image_link_container = ttk.Frame(top_info_frame) - image_link_container.grid(row=0, column=1, sticky="nsew", padx=(5, 0)) - image_link_container.rowconfigure(0, weight=1) - image_link_container.rowconfigure(1, weight=0) - 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, 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=(0, 0)) + image_link_container = ttk.Frame(left_column_frame, padding=(0,5,0,10)) + image_link_container.pack(fill=tk.X, expand=False, pady=(5,10), anchor='n') + self.image_placeholder_label = ttk.Label(image_link_container, text="[ Aircraft Image Area ]", relief="groove", anchor="center", borderwidth=2, padding=10, takefocus=False) + # Per dare una dimensione minima al placeholder dell'immagine + #self.image_placeholder_label.configure(height=8) # Altezza in linee di testo, aggiusta se necessario + self.image_placeholder_label.pack(fill=tk.X, expand=False, 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.pack(fill=tk.X) self.jetphotos_link_label.bind("", self._open_jetphotos_link_action) self.current_jetphotos_url: Optional[str] = None - self.jetphotos_link_label.grid_remove() + self.jetphotos_link_label.pack_forget() - 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 = ttk.LabelFrame(left_column_frame, text="Current Flight Status", padding=10) + live_data_container.pack(fill=tk.X, expand=False, pady=(10,0), anchor='n') self._create_live_details_layout(live_data_container) - bottom_track_frame_container = ttk.Frame(main_v_pane, padding=5) - main_v_pane.add(bottom_track_frame_container, weight=70) - self.track_notebook = ttk.Notebook(bottom_track_frame_container) + # --- Colonna Destra (Notebook con Mappa e Tabella) --- + right_column_frame = ttk.Frame(self.main_h_pane, padding=5) + self.main_h_pane.add(right_column_frame, weight=6) # 60% circa + + self.track_notebook = ttk.Notebook(right_column_frame) self.track_notebook.pack(fill=tk.BOTH, expand=True) - + self.track_map_tab = ttk.Frame(self.track_notebook) self.track_notebook.add(self.track_map_tab, text="Flight Track Map") - - self.track_map_canvas = tk.Canvas( - self.track_map_tab, bg="gray70", highlightthickness=0 - ) - self.track_map_canvas.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.track_map_canvas = tk.Canvas(self.track_map_tab, bg="gray70", highlightthickness=0) + self.track_map_canvas.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) 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") - table_frame = ttk.Frame(self.track_table_tab) - table_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + table_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) self.track_table_cols = { "timestamp": {"text": "Timestamp (UTC)", "width": 150, "anchor": tk.W}, @@ -140,29 +114,15 @@ class FullFlightDetailsWindow(tk.Toplevel): table_frame, columns=list(self.track_table_cols.keys()), show="headings", - selectmode="browse", + selectmode="browse" ) - for col_key, col_props in self.track_table_cols.items(): - self.track_table.heading( - col_key, - text=col_props["text"], - anchor=col_props.get("heading_anchor", tk.CENTER), - ) - self.track_table.column( - col_key, - width=col_props["width"], - anchor=col_props["anchor"], - stretch=tk.YES, - ) + self.track_table.heading(col_key, text=col_props["text"], anchor=col_props.get("heading_anchor", tk.CENTER)) + self.track_table.column(col_key, width=col_props["width"], anchor=col_props["anchor"], stretch=tk.YES) - vsb = ttk.Scrollbar( - table_frame, orient="vertical", command=self.track_table.yview - ) + vsb = ttk.Scrollbar(table_frame, orient="vertical", command=self.track_table.yview) vsb.pack(side=tk.RIGHT, fill=tk.Y) - hsb = ttk.Scrollbar( - table_frame, orient="horizontal", command=self.track_table.xview - ) + hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=self.track_table.xview) hsb.pack(side=tk.BOTTOM, fill=tk.X) self.track_table.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) self.track_table.pack(fill=tk.BOTH, expand=True) @@ -172,144 +132,148 @@ class FullFlightDetailsWindow(tk.Toplevel): self.after(100, self._initialize_detail_map_manager) self.protocol("WM_DELETE_WINDOW", self._on_closing_details_window) + + # Imposta la posizione del sash dopo un breve ritardo + self.after(50, self._set_initial_sashpos) + + + def _set_initial_sashpos(self): + if self.winfo_exists() and self.main_h_pane.winfo_exists(): + try: + self.update_idletasks() + total_width = self.main_h_pane.winfo_width() + + if total_width > 100: + desired_left_pane_width = int(total_width * 0.40) # 40% + self.main_h_pane.sashpos(0, desired_left_pane_width) + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Sash position set for left pane to: {desired_left_pane_width} (40% of {total_width})") + else: + logger.warning(f"FullDetailsWindow ({self.icao24.upper()}): PanedWindow width ({total_width}) not ready for sashpos, retrying.") + self.after(100, self._set_initial_sashpos) + except tk.TclError as e: + logger.warning(f"FullDetailsWindow ({self.icao24.upper()}): TclError setting sash position: {e}. Will retry if window still exists.") + if self.winfo_exists(): + self.after(150, self._set_initial_sashpos) + except Exception as e_sash: + logger.error(f"FullDetailsWindow ({self.icao24.upper()}): Unexpected error setting sash position: {e_sash}", exc_info=True) + else: + logger.warning(f"FullDetailsWindow ({self.icao24.upper()}): Window or PanedWindow destroyed before sashpos could be set.") def _initialize_detail_map_manager(self): + if not MAP_MANAGER_AVAILABLE: # Controlla se MapCanvasManager è stato importato + logger.error(f"FullDetailsWindow ({self.icao24.upper()}): MapCanvasManager not available. Cannot initialize detail map.") + if self.track_map_canvas.winfo_exists() and not 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 functionality is unavailable\n(Import Error)", + fill="orange", font=("Arial", 10), justify=tk.CENTER + ) + self._map_init_error_displayed = True + return + if not self.track_map_canvas.winfo_exists(): - logger.warning( - f"FullDetailsWindow ({self.icao24.upper()}): 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: - logger.info( - f"FullDetailsWindow ({self.icao24.upper()}): Detail map canvas not yet sized. Retrying initialization later." - ) + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Detail map canvas not yet sized. Retrying initialization later.") self.after(200, self._initialize_detail_map_manager) return - logger.info( - f"FullDetailsWindow ({self.icao24.upper()}): 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, - is_detail_map=True, - ) - logger.info( - f"FullDetailsWindow ({self.icao24.upper()}): MapCanvasManager for detail window initialized." + is_detail_map=True ) + 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 - ) + 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..." - ) + if self.detail_map_manager: # Assicura che sia stato creato + self.detail_map_manager._display_placeholder_text("Awaiting flight data...") except Exception as e: - 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, + 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, + fill="red", font=("Arial", 10), justify=tk.CENTER ) - self._map_init_error_displayed = True + self._map_init_error_displayed = True def _create_details_layout(self, parent_frame: ttk.LabelFrame): - parent_frame.columnconfigure(1, weight=1) - parent_frame.columnconfigure(3, weight=1) - fields = [ - ("icao24", "ICAO24:"), - ("registration", "Registration:"), - ("manufacturername", "Manufacturer:"), - ("model", "Model:"), - ("typecode", "Type Code:"), - ("serialnumber", "Serial No.:"), - ("operator", "Operator:"), - ("operatorcallsign", "Op. Callsign:"), - ("operatoricao", "Op. ICAO:"), - ("operatoriata", "Op. IATA:"), - ("country", "Country (Reg):"), - ("categorydescription", "Category:"), - ("built_year", "Built Year:"), - ("engines", "Engines:"), - ("icaoclass", "ICAO Class:"), - ("linenumber", "Line No.:"), - ("owner", "Owner:"), - ("notes", "Notes:"), - ("status", "Status (DB):"), - ("firstflightdate", "First Flight:"), - ("timestamp_metadata", "DB Record Time:"), + parent_frame.columnconfigure(1, weight=1) + parent_frame.columnconfigure(3, weight=1) + + fields_static = [ + ("icao24", "ICAO24:"), ("registration", "Registration:"), + ("manufacturername", "Manufacturer:"), ("model", "Model:"), + ("typecode", "Type Code:"), ("serialnumber", "Serial No.:"), + ("operator", "Operator:"), ("operatorcallsign", "Op. Callsign:"), + ("operatoricao", "Op. ICAO:"), ("operatoriata", "Op. IATA:"), + ("country", "Country (Reg):"), ("categorydescription", "Category:"), + ("built_year", "Built Year:"), ("engines", "Engines:"), + ("icaoclass", "ICAO Class:"), ("linenumber", "Line No.:"), + ("owner", "Owner:"), ("notes", "Notes:"), + ("status", "Status (DB):"), ("firstflightdate", "First Flight:"), + ("timestamp_metadata", "DB Record Time:") ] - row_idx, col_idx, max_cols_per_row = 0, 0, 2 - for key, text_label in fields: - lbl_text = text_label - if key == "icao24": - 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)) - 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) - ) + + row_idx = 0 + col_idx = 0 + for key, text_label in fields_static: + lbl = ttk.Label(parent_frame, text=text_label, font="-weight bold" if key == "icao24" else None) + lbl.grid(row=row_idx, column=col_idx * 2, sticky=tk.W, pady=1, padx=(0 if col_idx == 0 else 10, 3)) + + val_lbl = ttk.Label(parent_frame, text="N/A", wraplength=150) + val_lbl.grid(row=row_idx, column=col_idx * 2 + 1, sticky=tk.W, pady=1, padx=(0, 5)) self.detail_labels[key] = val_lbl - col_idx += 1 - if col_idx >= max_cols_per_row: - col_idx, row_idx = 0, row_idx + 1 - if col_idx != 0: - ttk.Frame(parent_frame).grid( - row=row_idx, - column=col_idx * 2, - columnspan=(max_cols_per_row - col_idx) * 2, - ) + + if col_idx == 0: + col_idx = 1 + else: + col_idx = 0 + row_idx += 1 + + if col_idx == 1: # If odd number of fields, last one is in first col + ttk.Frame(parent_frame).grid(row=row_idx, column=2, columnspan=2) # Dummy to push next section down if needed + def _create_live_details_layout(self, parent_frame: ttk.LabelFrame): parent_frame.columnconfigure(1, weight=1) + parent_frame.columnconfigure(3, weight=1) + live_fields = [ - ("callsign", "Callsign (Live):"), - ("baro_altitude_m", "Altitude (Baro):"), - ("geo_altitude_m", "Altitude (Geo):"), - ("velocity_mps", "Speed (GS):"), - ("vertical_rate_mps", "Vertical Rate:"), - ("true_track_deg", "Track (°):"), - ("on_ground", "On Ground:"), - ("squawk", "Squawk:"), - ("spi", "SPI:"), - ("position_source", "Position Source:"), - ("origin_country", "Origin Country (Live):"), - ("timestamp", "Last Position Time:"), - ("last_contact_timestamp", "Last Contact Time:"), + ("callsign", "Callsign (Live):"), ("origin_country", "Origin Country (Live):"), + ("baro_altitude_m", "Altitude (Baro):"), ("geo_altitude_m", "Altitude (Geo):"), + ("velocity_mps", "Speed (GS):"), ("vertical_rate_mps", "Vertical Rate:"), + ("true_track_deg", "Track (°):"), ("on_ground", "On Ground:"), + ("squawk", "Squawk:"), ("spi", "SPI:"), + ("position_source", "Position Source:"), + ("timestamp", "Last Position Time:"), ("last_contact_timestamp", "Last Contact Time:") ] - for row_idx, (key, text_label) in enumerate(live_fields): + row_idx = 0 + col_idx = 0 + for key, text_label in live_fields: lbl = ttk.Label(parent_frame, text=text_label) - lbl.grid(row=row_idx, column=0, sticky=tk.W, pady=1, padx=(0, 3)) - val_lbl = ttk.Label(parent_frame, text="N/A", wraplength=180) - val_lbl.grid(row=row_idx, column=1, sticky=tk.W, pady=1, padx=(0, 10)) + lbl.grid(row=row_idx, column=col_idx * 2, sticky=tk.W, pady=1, padx=(0 if col_idx == 0 else 10, 3)) + val_lbl = ttk.Label(parent_frame, text="N/A", wraplength=150) + val_lbl.grid(row=row_idx, column=col_idx * 2 + 1, sticky=tk.W, pady=1, padx=(0, 5)) self.detail_labels[key] = val_lbl + + if col_idx == 0: col_idx = 1 + else: col_idx = 0; row_idx += 1 + if col_idx == 1: # If odd number of fields + ttk.Frame(parent_frame).grid(row=row_idx, column=2, columnspan=2) def update_details( self, @@ -317,26 +281,20 @@ class FullFlightDetailsWindow(tk.Toplevel): live_data: Optional[Dict[str, Any]], full_track_data: Optional[List[Dict[str, Any]]], ): - if not self.winfo_exists(): - return + if not self.winfo_exists(): return 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}" - ) + 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 static_data: all_data.update(static_data) if live_data: - if "icao24" in live_data and isinstance(live_data["icao24"], str): + 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() - ) + live_data_processed['icao24'] = live_data_processed['icao24'].lower().strip() all_data.update(live_data_processed) else: all_data.update(live_data) @@ -345,104 +303,53 @@ class FullFlightDetailsWindow(tk.Toplevel): if label_widget.winfo_exists(): value = all_data.get(key) if key == "icao24": - display_text = str(value).upper() if value else "N/A" + display_text = self.icao24.upper() if self.icao24 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 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 + 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) + 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" - ) - if not self.jetphotos_link_label.winfo_ismapped(): - self.jetphotos_link_label.grid() + 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") + if not self.jetphotos_link_label.winfo_ismapped(): self.jetphotos_link_label.pack(fill=tk.X) else: self.current_jetphotos_url = None - if self.jetphotos_link_label.winfo_ismapped(): - self.jetphotos_link_label.grid_remove() + if self.jetphotos_link_label.winfo_ismapped(): self.jetphotos_link_label.pack_forget() self._update_track_table(full_track_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." - ) + 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]]]): - if not self.track_table.winfo_exists(): - return - for item in self.track_table.get_children(): - self.track_table.delete(item) + 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( - f"FullDetailsWindow ({self.icao24.upper()}): 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 ({self.icao24.upper()}): Populating track table with {len(track_data_list)} points." - ) + 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(): @@ -450,52 +357,28 @@ class FullFlightDetailsWindow(tk.Toplevel): 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("%H:%M:%S") - except: - display_value = str(raw_value) + 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}" - elif col_key in ["baro_altitude_m", "geo_altitude_m"]: - display_value = f"{raw_value:.0f}" - elif col_key in [ - "velocity_mps", - "true_track_deg", - "vertical_rate_mps", - ]: - display_value = f"{raw_value:.1f}" - else: - display_value = str(raw_value) - elif isinstance(raw_value, bool): - display_value = str(raw_value) - else: - display_value = str(raw_value) + if col_key in ["latitude", "longitude"]: display_value = f"{raw_value:.5f}" + elif col_key in ["baro_altitude_m", "geo_altitude_m"]: display_value = f"{raw_value:.0f}" + elif col_key in ["velocity_mps", "true_track_deg", "vertical_rate_mps"]: display_value = f"{raw_value:.1f}" + else: display_value = str(raw_value) + elif isinstance(raw_value, bool): display_value = str(raw_value) + else: display_value = str(raw_value) values.append(display_value) self.track_table.insert("", 0, values=values) - 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." - ) + 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() + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Updating detail map with track and live data.") + if self.detail_map_manager: # Ensure it exists before calling methods + self.detail_map_manager.clear_map_display() valid_track_points_for_bbox: List[Tuple[float, float]] = [] canonical_track_states_for_drawing: List[CanonicalFlightState] = [] @@ -511,121 +394,84 @@ class FullFlightDetailsWindow(tk.Toplevel): icao24=current_icao_normalized, 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)), - baro_altitude_m=point_dict.get("baro_altitude_m"), + latitude=lat, longitude=lon, on_ground=bool(point_dict.get("on_ground", False)), + 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"), + 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"), + 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}" - ) + 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") + 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)) + valid_track_points_for_bbox.append((live_lat, live_lon)) try: 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 + if isinstance(live_icao, str): live_icao = live_icao.lower().strip() + else: live_icao = current_icao_normalized live_state = CanonicalFlightState( - icao24=live_icao, + icao24=live_icao, callsign=current_live_data.get("callsign"), - origin_country=current_live_data.get("origin_country"), + 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, + 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"), + 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"), + 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"), + spi=bool(current_live_data.get("spi")), + position_source=current_live_data.get("position_source") ) 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}" - ) + 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." - ) + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): No valid track points or live position to display on map.") + if self.detail_map_manager: self.detail_map_manager._display_placeholder_text("No flight track data available to display.") + return + + if not self.detail_map_manager: # Safety check + logger.error(f"FullDetailsWindow ({self.icao24.upper()}): self.detail_map_manager is None before populating its data. Aborting map update.") 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_for_drawing) + 5) - if canonical_track_states_for_drawing - else 5 - ) - ) + track_deque = deque(maxlen=(len(canonical_track_states_for_drawing) + 5) if canonical_track_states_for_drawing else 5) for state in canonical_track_states_for_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 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[current_icao_normalized] = ( - track_deque # Usa ICAO normalizzato - ) - - self.detail_map_manager._current_flights_to_display_gui = ( - current_flight_for_map_drawing - ) + self.detail_map_manager.flight_tracks_gui[current_icao_normalized] = 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 - ) + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Single point track/live. Centering map with patch.") + if self.detail_map_manager: 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 - 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 - else: - padding_lon_eff = max(padding_deg, (max_lon - min_lon) * 0.15) + padding_deg = 0.05 + if abs(max_lat - min_lat) < 1e-6 : padding_lat_eff = padding_deg + 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 + 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), @@ -635,27 +481,17 @@ class FullFlightDetailsWindow(tk.Toplevel): } if track_bbox["lat_min"] >= track_bbox["lat_max"]: - track_bbox["lat_max"] = track_bbox["lat_min"] + 0.01 + 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 + track_bbox["lon_max"] = track_bbox["lon_min"] + 0.01 if _is_valid_bbox_dict(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 - ) + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Requesting detail map render for calculated BBox of track: {track_bbox}") + if self.detail_map_manager: self.detail_map_manager._request_map_render_for_bbox(track_bbox, preserve_current_zoom_if_possible=False) else: - logger.warning( - f"FullDetailsWindow ({self.icao24.upper()}): Calculated track BBox is invalid: {track_bbox}. Attempting fallback." - ) + 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, - ) + if self.detail_map_manager: 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): if self.current_jetphotos_url: @@ -663,9 +499,7 @@ class FullFlightDetailsWindow(tk.Toplevel): 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}" - ) + logger.error(f"Failed to open JetPhotos link {self.current_jetphotos_url}: {e}") else: logger.warning("No JetPhotos URL to open for current aircraft.") @@ -673,175 +507,82 @@ class FullFlightDetailsWindow(tk.Toplevel): self.update_idletasks() 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 + if width <= 1: width = self.winfo_reqwidth() if self.winfo_reqwidth() > 1 else 1100 + if height <= 1: height = self.winfo_reqheight() if self.winfo_reqheight() > 1 else 700 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 x < 0: - x = 0 - if y < 0: - y = 0 + if x + width > x_screen: x = x_screen - width + if y + height > y_screen: y = y_screen - height + if x < 0: x = 0 + if y < 0: y = 0 - if width > 0 and height > 0: - self.geometry(f"{width}x{height}+{x}+{y}") + 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.upper()} is closing." - ) # Use uppercase for display log + logger.info(f"FullFlightDetailsWindow for {self.icao24.upper()} is closing.") if self.detail_map_manager: - logger.info( - f"FullDetailsWindow ({self.icao24.upper()}): Requesting shutdown for detail_map_manager worker." - ) + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Requesting shutdown for detail_map_manager worker.") try: self.detail_map_manager.shutdown_worker() except Exception as e: - logger.error( - f"FullDetailsWindow ({self.icao24.upper()}): Error during detail_map_manager worker shutdown: {e}", - exc_info=True, - ) - - # MODIFIED: Notify controller on close + 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 is already lowercase - logger.info( - f"FullDetailsWindow ({self.icao24.upper()}): Notified controller of closure." - ) + self.controller.details_window_closed(self.icao24) + logger.info(f"FullDetailsWindow ({self.icao24.upper()}): Notified controller of closure.") 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, - ) + logger.error(f"FullDetailsWindow ({self.icao24.upper()}): Error notifying controller of details window closure: {e_notify_close}", exc_info=True) self.destroy() - if __name__ == "__main__": root_test = tk.Tk() root_test.title("Main Window (Test)") sample_static = { - "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", + "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", - "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, - "last_contact_timestamp": time.time() - 58, - "latitude": 45.15, - "longitude": 9.15, + "icao24": "tst001", + "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, "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, - "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, - }, + {"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}, ] sample_track_single_point = [ - { - "latitude": 46.0, - "longitude": 10.0, - "baro_altitude_m": 8000, - "timestamp": time.time() - 120, - "on_ground": False, - } + {"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, + "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() - ): + if hasattr(root_test, "details_win_instance_test") and root_test.details_win_instance_test.winfo_exists(): root_test.details_win_instance_test.destroy() - # Pass ICAO in its original case, FullFlightDetailsWindow will normalize it - details_window = FullFlightDetailsWindow(root_test, icao) + 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 - 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) + ttk.Button(root_test, text="Open Full Details (Multi-Point Track)", command=lambda: open_details_test(sample_static, sample_live, sample_track, icao="tst001")).pack(padx=20, pady=5) + ttk.Button(root_test, text="Open Full Details (Single-Point Track)", command=lambda: open_details_test(sample_static, sample_live, sample_track_single_point, icao="tst001")).pack(padx=20, pady=5) + ttk.Button(root_test, text="Open Full Details (Live Only, No Track)", command=lambda: open_details_test(None, sample_live_only, None, icao="tst002")).pack(padx=20, pady=5) + ttk.Button(root_test, text="Open Full Details (No Track, No Live)", command=lambda: open_details_test(sample_static, None, None, icao="tst001")).pack(padx=20, pady=5) - root_test.mainloop() + root_test.mainloop() \ No newline at end of file