SXXXXXXX_FlightMonitor/flightmonitor/controller/app_controller.py
VALLONGOL 9ae7c54631 Chore: Stop tracking files based on .gitignore update.
Untracked files matching the following rules:
- Rule "!.vscode/launch.json": 1 file
2025-05-30 11:22:51 +02:00

763 lines
51 KiB
Python

# FlightMonitor/controller/app_controller.py
from queue import Queue, Empty as QueueEmpty
import threading
import tkinter as tk
import time
import os
import csv # Assicurati che questo import sia presente
from ..data.opensky_live_adapter import (
OpenSkyLiveAdapter,
AdapterMessage,
MSG_TYPE_FLIGHT_DATA,
MSG_TYPE_ADAPTER_STATUS,
STATUS_STARTING,
STATUS_FETCHING,
STATUS_RECOVERED,
STATUS_RATE_LIMITED,
STATUS_API_ERROR_TEMPORARY,
STATUS_PERMANENT_FAILURE,
STATUS_STOPPED,
)
from ..data import config as app_config
from ..utils.logger import get_logger
from ..data.storage import DataStorage
from ..data.common_models import CanonicalFlightState
from ..data.aircraft_database_manager import AircraftDatabaseManager
from typing import List, Optional, Dict, Any, TYPE_CHECKING, Callable
from ..utils.gui_utils import (
GUI_STATUS_OK,
GUI_STATUS_WARNING,
GUI_STATUS_ERROR,
GUI_STATUS_FETCHING,
GUI_STATUS_UNKNOWN,
)
if TYPE_CHECKING:
from ..gui.main_window import MainWindow, ImportProgressDialog # Importa ImportProgressDialog per type hinting
from ..map.map_canvas_manager import MapCanvasManager
module_logger = get_logger(__name__)
GUI_QUEUE_CHECK_INTERVAL_MS = 150
ADAPTER_JOIN_TIMEOUT_SECONDS = 5.0
DEFAULT_CLICK_AREA_SIZE_KM = 50.0
class AppController:
def __init__(self):
self.main_window: Optional["MainWindow"] = None
self.live_adapter_thread: Optional[OpenSkyLiveAdapter] = None
self.is_live_monitoring_active: bool = False
self.flight_data_queue: Optional[Queue[AdapterMessage]] = None
self._gui_after_id: Optional[str] = None
self._active_bounding_box: Optional[Dict[str, float]] = None
self.data_storage: Optional[DataStorage] = None
self.aircraft_db_manager: Optional[AircraftDatabaseManager] = None
try:
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 in AppController: {e}",
exc_info=True,
)
self.data_storage = None
try:
self.aircraft_db_manager = AircraftDatabaseManager()
module_logger.info("AircraftDatabaseManager initialized successfully by AppController.")
except Exception as e:
module_logger.critical(
f"CRITICAL: Failed to initialize AircraftDatabaseManager in AppController: {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."
)
initial_status_msg = "System Initialized. Ready."
initial_status_level = GUI_STATUS_OK
if not self.data_storage:
err_msg_ds = "Data storage init failed. History will not be saved."
module_logger.error(err_msg_ds)
initial_status_msg = err_msg_ds
initial_status_level = GUI_STATUS_ERROR
if not self.aircraft_db_manager:
err_msg_adb = "Aircraft DB init failed. Static details may be unavailable."
module_logger.error(err_msg_adb)
if initial_status_level == GUI_STATUS_OK:
initial_status_msg = err_msg_adb
initial_status_level = GUI_STATUS_WARNING
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)
else:
module_logger.error(
"Main window not set or lacks update_semaphore_and_status during set_main_window."
)
def _process_flight_data_queue(self):
# ... (Questo metodo rimane invariato rispetto alla versione precedente completa)
if not self.flight_data_queue:
module_logger.warning("_process_flight_data_queue: flight_data_queue is None.")
return
if not (self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists()):
module_logger.info("_process_flight_data_queue: Main window or root does not exist. Stopping queue processing.")
self._gui_after_id = None
return
try:
while not self.flight_data_queue.empty():
message: Optional[AdapterMessage] = None
try:
message = self.flight_data_queue.get(block=False, timeout=0.01)
except QueueEmpty:
break
except Exception as e_get:
module_logger.error(f"Error getting message from queue: {e_get}. Continuing processing...", exc_info=False)
continue
if message is None: continue
try:
message_type = message.get("type")
if message_type == MSG_TYPE_FLIGHT_DATA:
flight_states_payload: Optional[List[CanonicalFlightState]] = message.get("payload")
if flight_states_payload is not None:
if self.data_storage:
saved_count = 0
for state in flight_states_payload:
if not isinstance(state, CanonicalFlightState): 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,
)
if flight_id:
pos_id = self.data_storage.add_position_daily(flight_id, state)
if pos_id: saved_count += 1
except Exception as e_storage:
module_logger.error(f"Error saving flight state {state.icao24} to storage: {e_storage}", exc_info=False)
if saved_count > 0: module_logger.info(f"Saved {saved_count} position updates to DB.")
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):
try:
self.main_window.map_manager_instance.update_flights_on_map(flight_states_payload)
except Exception as e_display:
module_logger.error(f"Error calling map_manager.update_flights_on_map: {e_display}", exc_info=True)
gui_message = f"Live data: {len(flight_states_payload)} aircraft tracked." if flight_states_payload else "Live data: No aircraft in area."
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 as e_tcl: module_logger.warning(f"TclError updating status (OK): {e_tcl}."); self._gui_after_id = None; return
except Exception as e_stat: module_logger.error(f"Error updating status (OK): {e_stat}", exc_info=False)
else:
module_logger.warning("Received flight_data message with None payload.")
if hasattr(self.main_window, "update_semaphore_and_status"):
try: self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Received empty data payload.")
except tk.TclError as e_tcl: module_logger.warning(f"TclError updating status (WARN): {e_tcl}."); self._gui_after_id = None; return
except Exception as e_stat: module_logger.error(f"Error updating status (WARN): {e_stat}", exc_info=False)
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}")
module_logger.info(f"Processing Adapter Status: Code='{status_code}', Message='{gui_message_from_adapter}'")
gui_status_level_to_set = GUI_STATUS_UNKNOWN
action_required_by_controller = None
details_from_adapter = message.get("details", {})
if status_code == STATUS_STARTING: gui_status_level_to_set = GUI_STATUS_FETCHING
elif status_code == STATUS_FETCHING: gui_status_level_to_set = GUI_STATUS_FETCHING
elif status_code == STATUS_RECOVERED: gui_status_level_to_set = GUI_STATUS_OK
elif status_code == STATUS_RATE_LIMITED:
gui_status_level_to_set = GUI_STATUS_WARNING
delay = details_from_adapter.get("delay", "N/A")
gui_message_from_adapter = f"API Rate Limit. Retry in {float(delay):.0f}s." if isinstance(delay, (int, float)) else f"API Rate Limit. Retry: {delay}."
elif status_code == STATUS_API_ERROR_TEMPORARY:
gui_status_level_to_set = GUI_STATUS_WARNING
err_code_detail = details_from_adapter.get("status_code", "N/A")
delay = details_from_adapter.get("delay", "N/A")
gui_message_from_adapter = f"Temp API Error ({err_code_detail}). Retry in {float(delay):.0f}s." if isinstance(delay, (int, float)) else f"Temp API Error ({err_code_detail}). Retry: {delay}."
elif status_code == STATUS_PERMANENT_FAILURE:
gui_status_level_to_set = GUI_STATUS_ERROR
action_required_by_controller = "STOP_MONITORING"
elif status_code == 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 as e_tcl: module_logger.warning(f"TclError status ({gui_status_level_to_set}): {e_tcl}."); self._gui_after_id = None; return
except Exception as e_stat: module_logger.error(f"Error status ({gui_status_level_to_set}): {e_stat}", exc_info=False)
if action_required_by_controller == "STOP_MONITORING":
module_logger.critical("Permanent failure from adapter. Triggering controller stop.")
self.stop_live_monitoring(from_error=True); break
else:
module_logger.warning(f"Unknown message type from adapter: '{message_type}'. Msg: {message}")
if hasattr(self.main_window, "update_semaphore_and_status"):
try: self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, f"Unknown adapter message: {message_type}")
except tk.TclError as e_tcl: module_logger.warning(f"TclError status (UNKNOWN MSG): {e_tcl}."); self._gui_after_id = None; return
except Exception as e_stat: module_logger.error(f"Error status (UNKNOWN MSG): {e_stat}", exc_info=False)
except Exception as e_msg_proc:
module_logger.error(f"Error processing adapter message (Type: {message.get('type', 'N/A')}): {e_msg_proc}", exc_info=True)
finally:
try: self.flight_data_queue.task_done()
except (ValueError, RuntimeError) as e_td: module_logger.error(f"Error calling task_done: {e_td}", exc_info=False)
except tk.TclError as e_tcl_outer:
module_logger.warning(f"TclError during queue processing: {e_tcl_outer}. Aborting.", exc_info=False); self._gui_after_id = None; return
except Exception as e_outer:
module_logger.error(f"Unexpected critical error processing queue: {e_outer}", exc_info=True)
if hasattr(self.main_window, "update_semaphore_and_status"):
try: self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, "Critical error processing data. See logs.")
except: pass
finally:
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: module_logger.warning("TclError scheduling next queue check."); self._gui_after_id = None
except Exception as e_after: module_logger.error(f"Error scheduling next queue check: {e_after}", exc_info=True); self._gui_after_id = None
else:
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: pass
self._gui_after_id = None
module_logger.debug("_process_flight_data_queue: Not rescheduling.")
def start_live_monitoring(self, bounding_box: Dict[str, float]):
# ... (come nella versione precedente completa) ...
if not self.main_window:
module_logger.error("Controller: Main window not set. Cannot start live monitoring."); return
if not bounding_box:
err_msg = "Controller: Bounding box is required."; module_logger.error(err_msg)
if hasattr(self.main_window, "_reset_gui_to_stopped_state"): self.main_window._reset_gui_to_stopped_state(f"Start failed: {err_msg}"); return
if not self.data_storage:
err_msg_ds = "DataStorage not initialized. History will not be saved."
if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, err_msg_ds + " Check logs.")
if self.is_live_monitoring_active:
module_logger.warning("Controller: Live monitoring already active. Start request ignored.")
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.")
return
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 is not None 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_set_bbox:
module_logger.error(f"Error instructing map manager to set BBox {bounding_box}: {e_map_set_bbox}", exc_info=True)
if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Map update error on start. See logs.")
else:
if hasattr(self.main_window, "clear_all_views_data"):
try: self.main_window.clear_all_views_data()
except Exception as e_clear: module_logger.error(f"Error calling clear_all_views_data on start: {e_clear}", exc_info=False)
if self.flight_data_queue is None:
self.flight_data_queue = Queue(maxsize=200); module_logger.debug("Created new flight data queue.")
else:
while not self.flight_data_queue.empty():
try:
old_message = self.flight_data_queue.get_nowait(); self.flight_data_queue.task_done()
module_logger.debug(f"Discarded old message from queue: {old_message.get('type', 'Unknown Type')}")
except QueueEmpty: break
except Exception as e_q_clear: module_logger.warning(f"Error clearing old message from queue: {e_q_clear}")
adapter_thread_to_stop = self.live_adapter_thread
if adapter_thread_to_stop and adapter_thread_to_stop.is_alive():
module_logger.warning("Controller: Old LiveAdapter thread alive. Attempting stop and join.")
try:
adapter_thread_to_stop.stop()
if (self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists()):
try: self.main_window.root.update_idletasks()
except Exception: pass
adapter_thread_to_stop.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS)
if adapter_thread_to_stop.is_alive(): module_logger.error(f"Controller: Old LiveAdapter thread DID NOT join in time!")
else: module_logger.info("Controller: Old LiveAdapter thread joined successfully.")
except Exception as e_stop_join: module_logger.error(f"Error during old adapter stop/join: {e_stop_join}", exc_info=True)
finally: self.live_adapter_thread = None
else: module_logger.debug("Controller: No active LiveAdapter thread to stop or already stopped.")
self.live_adapter_thread = OpenSkyLiveAdapter(output_queue=self.flight_data_queue, bounding_box=self._active_bounding_box, polling_interval=app_config.LIVE_POLLING_INTERVAL_SECONDS)
self.is_live_monitoring_active = True
self.live_adapter_thread.start()
module_logger.info(f"Controller: New live adapter thread '{self.live_adapter_thread.name}' started.")
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 Exception: pass
finally: self._gui_after_id = None
if (self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists()):
self._gui_after_id = self.main_window.root.after(100, self._process_flight_data_queue)
module_logger.info("Controller: GUI queue polling scheduled.")
else:
module_logger.error("Controller: Cannot schedule GUI queue polling: MainWindow or root does not exist. Aborting live monitoring.")
self.is_live_monitoring_active = False
if self.live_adapter_thread and self.live_adapter_thread.is_alive():
try: self.live_adapter_thread.stop()
except Exception as e_stop_fail: module_logger.error(f"Error trying to stop adapter: {e_stop_fail}")
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):
# ... (come nella versione precedente completa) ...
if not self.is_live_monitoring_active and not (from_error and self.live_adapter_thread and self.live_adapter_thread.is_alive()):
module_logger.debug(f"Controller: Stop requested but live monitoring/adapter not active (from_error={from_error}). Ignoring.")
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 or not started.")
return
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 Exception: pass
finally: self._gui_after_id = None
adapter_thread_to_stop = self.live_adapter_thread
if adapter_thread_to_stop and adapter_thread_to_stop.is_alive():
module_logger.debug(f"Controller: Signaling LiveAdapter thread ({adapter_thread_to_stop.name}) to stop.")
try:
adapter_thread_to_stop.stop()
if (self.main_window and hasattr(self.main_window, "root") and self.main_window.root.winfo_exists()):
try: self.main_window.root.update_idletasks()
except Exception: pass
module_logger.debug(f"Controller: Waiting for LiveAdapter thread ({adapter_thread_to_stop.name}) to join...")
adapter_thread_to_stop.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS)
if adapter_thread_to_stop.is_alive(): module_logger.error(f"Controller: LiveAdapter thread ({adapter_thread_to_stop.name}) did NOT join in time!")
else: module_logger.info(f"Controller: LiveAdapter thread ({adapter_thread_to_stop.name}) joined successfully.")
except Exception as e_stop_join: module_logger.error(f"Error during adapter stop/join sequence: {e_stop_join}", exc_info=True)
finally: self.live_adapter_thread = None
else: module_logger.debug("Controller: No active LiveAdapter thread to stop or already stopped."); 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()):
module_logger.debug("Controller: Processing final messages from adapter queue post-join...")
try: self._process_flight_data_queue(); module_logger.debug("Controller: Requested final processing of adapter queue.")
except Exception as e_final_loop: module_logger.error(f"Controller: Unexpected error in final queue processing request: {e_final_loop}", exc_info=True)
else: module_logger.debug("Controller: No flight data queue or GUI to process after stop.")
if hasattr(self.main_window, "clear_all_views_data"):
try: self.main_window.clear_all_views_data()
except Exception as e_clear_views: module_logger.error(f"Error calling clear_all_views_data after stop: {e_clear_views}", exc_info=False)
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
stop_status_msg = "Monitoring stopped."
if from_error: stop_status_msg = "Monitoring stopped due to an error."
try: self.main_window._reset_gui_to_stopped_state(stop_status_msg)
except tk.TclError: module_logger.warning("TclError resetting GUI state after stop. GUI likely gone.")
except Exception as e_reset_gui: module_logger.error(f"Error resetting GUI state after stop: {e_reset_gui}", exc_info=True)
self._active_bounding_box = None
module_logger.info("Controller: Live monitoring shutdown sequence fully completed.")
def on_application_exit(self):
# ... (come nella versione precedente completa) ...
module_logger.info("Controller: Application exit requested. Cleaning up resources.")
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):
module_logger.debug("Controller: Requesting MapCanvasManager to shutdown its worker.")
try: map_manager.shutdown_worker(); module_logger.info("Controller: MapCanvasManager worker shutdown requested.")
except Exception as e_map_shutdown: module_logger.error(f"Controller: Error during 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)
if self.data_storage:
try: self.data_storage.close_connection()
except Exception as e_db_close: 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()
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.info("Controller: Cleanup on application exit finished.")
def start_history_monitoring(self):
# ... (come prima)
if not self.main_window:
module_logger.error("Main window not set for history."); return
if not self.data_storage:
err_msg = "DataStorage not initialized. Cannot use history features."
module_logger.error(f"Controller: {err_msg}")
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}")
return
module_logger.info("Controller: History monitoring started (placeholder).")
if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "History mode active (placeholder).")
def stop_history_monitoring(self):
# ... (come prima)
if not self.main_window: return
module_logger.info("Controller: History monitoring stopped (placeholder).")
if hasattr(self.main_window, "update_semaphore_and_status"): self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "History monitoring stopped.")
def on_map_left_click(self, latitude: float, longitude: float, screen_x: int, screen_y: int):
# ... (come prima)
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)
else: module_logger.warning("Main window N/A to update clicked map info.")
def request_detailed_flight_info(self, icao24: str):
# ... (come prima)
module_logger.info(f"Controller: Detailed info request for ICAO24: {icao24}")
if not self.main_window:
module_logger.error("Controller: MainWindow not set, cannot show flight details.")
return
if not icao24:
module_logger.debug("Controller: Empty ICAO24 received, clearing details panel.")
if hasattr(self.main_window, "update_selected_flight_details"):
self.main_window.update_selected_flight_details(None)
return
live_flight_state_data: Optional[Dict[str, Any]] = None
static_aircraft_details: Optional[Dict[str, Any]] = None
combined_details: Dict[str, Any] = {"icao24": icao24.lower()}
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_manager = self.main_window.map_manager_instance
with map_manager._map_data_lock:
live_flights_list = map_manager._current_flights_to_display_gui
for state_obj in live_flights_list:
if state_obj.icao24.lower() == icao24.lower():
live_flight_state_data = state_obj.to_dict()
break
if live_flight_state_data:
module_logger.debug(f"Found live data for {icao24}: {list(live_flight_state_data.keys())}")
combined_details.update(live_flight_state_data)
else:
module_logger.info(f"No live data found for {icao24} currently displayed.")
if self.aircraft_db_manager:
static_aircraft_details = self.aircraft_db_manager.get_aircraft_details(icao24)
if static_aircraft_details:
module_logger.debug(f"Found static data for {icao24} from DB: {list(static_aircraft_details.keys())}")
for key, value in static_aircraft_details.items():
if key not in combined_details:
combined_details[key] = value
else:
module_logger.info(f"No static data found for {icao24} in aircraft DB.")
else:
module_logger.warning("AircraftDatabaseManager not available, static details not retrieved.")
if hasattr(self.main_window, "update_selected_flight_details"):
self.main_window.update_selected_flight_details(combined_details)
else:
module_logger.error("MainWindow does not have 'update_selected_flight_details' method.")
def import_aircraft_database_from_file_with_progress(self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"): # type: ignore
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.")
if progress_dialog_ref and progress_dialog_ref.winfo_exists(): # type: ignore
progress_dialog_ref.import_finished(False, "Error: Database manager not active.") # type: ignore
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.")
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(): # type: ignore
progress_dialog_ref.import_finished(False, "Error: Main application window not available.") # type: ignore
return
if progress_dialog_ref and progress_dialog_ref.winfo_exists(): # type: ignore
progress_dialog_ref.import_started() # type: ignore
import_thread = threading.Thread(
target=self._perform_db_import_with_progress_threaded,
args=(csv_filepath, progress_dialog_ref),
daemon=True
)
import_thread.start()
module_logger.info(f"Aircraft DB import thread started for: {csv_filepath}")
def _count_csv_rows(self, csv_filepath: str) -> Optional[int]:
"""Counts data rows in a CSV file (excluding header), sets field size limit."""
try:
current_limit = csv.field_size_limit()
# Ensure this limit is consistent with AircraftDatabaseManager.import_from_csv
new_limit_target = 10 * 1024 * 1024 # 10 MB
if new_limit_target > current_limit:
csv.field_size_limit(new_limit_target)
module_logger.info(f"Count Rows: Increased csv.field_size_limit from {current_limit} to {new_limit_target}")
except Exception as e_limit:
module_logger.error(f"Count Rows: Error attempting to increase csv.field_size_limit: {e_limit}")
# Continue, but be aware counting might fail if fields are indeed too large
try:
with open(csv_filepath, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
try:
header = next(reader)
if not header :
module_logger.info(f"Count rows: CSV file '{csv_filepath}' has no header or is empty after header.")
return 0
except StopIteration:
module_logger.info(f"Count rows: CSV file '{csv_filepath}' is completely empty.")
return 0
row_count = 0
for _row_idx, _row_content in enumerate(reader): # Iterate to count
row_count += 1
return row_count
except FileNotFoundError:
module_logger.error(f"Count rows: CSV file not found: {csv_filepath}")
return None
except csv.Error as e_csv:
module_logger.error(f"Count rows: CSV format error in '{csv_filepath}' (e.g., field too large): {e_csv}", exc_info=True)
return None
except Exception as e:
module_logger.error(f"Count rows: General error reading CSV {csv_filepath}: {e}", exc_info=True)
return None
def _perform_db_import_with_progress_threaded(self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"): # type: ignore
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(): # type: ignore
self.main_window.root.after(0, lambda: progress_dialog_ref.import_finished(False, "Internal Error: DB Manager missing.")) # type: ignore
return
module_logger.info(f"Import thread: Starting row count for: {csv_filepath}")
def schedule_gui_update(callable_func: Callable, *args: Any): # type: ignore
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}")
else: module_logger.warning("GUI N/A for update from import thread.")
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(): # type: ignore
schedule_gui_update(progress_dialog_ref.import_finished, False, f"Error: Could not read/count rows in '{os.path.basename(csv_filepath)}'. Check logs.") # type: ignore
return
if total_data_rows == 0:
if progress_dialog_ref and progress_dialog_ref.winfo_exists(): # type: ignore
schedule_gui_update(progress_dialog_ref.update_progress, 0, 0, 0, f"File '{os.path.basename(csv_filepath)}' is empty or header-only.") # type: ignore
schedule_gui_update(progress_dialog_ref.import_finished, True, "Import complete: No data rows to import.") # type: ignore
return
if progress_dialog_ref and progress_dialog_ref.winfo_exists(): # type: ignore
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)}'...") # type: ignore
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(): # type: ignore
schedule_gui_update(progress_dialog_ref.update_progress, # type: ignore
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,
progress_callback=import_progress_update_for_dialog_from_controller,
total_rows_for_callback=total_data_rows
)
final_message = f"DB Import complete. Processed: {processed_final}, Imported/Updated: {imported_final}."
success = True
if imported_final == 0 and processed_final > 0 and total_data_rows > 0 :
final_message = f"DB Import: {processed_final} CSV data rows processed, 0 imported (check CSV format/logs)."
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(): # type: ignore
schedule_gui_update(progress_dialog_ref.update_progress, processed_final, imported_final, total_data_rows, "Finalizing import...") # type: ignore
schedule_gui_update(progress_dialog_ref.import_finished, success, final_message) # type: ignore
# ... (Metodi di interazione mappa come on_map_right_click, on_map_context_menu_request, ecc. come nella versione precedente)
def on_map_right_click(self, latitude: float, longitude: float, screen_x: int, screen_y: int):
# ... (come prima)
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)"
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)
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)
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):
# ... (come prima)
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)
else: module_logger.warning("Main window N/A to show context menu.")
def recenter_map_at_coords(self, lat: float, lon: float):
# ... (come prima)
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.")
def set_bbox_around_coords(self, center_lat: float, center_lon: float, area_size_km: float = DEFAULT_CLICK_AREA_SIZE_KM):
# ... (come prima)
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.")
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]):
# ... (come prima)
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.")
def update_general_map_info(self):
# ... (come prima)
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):
module_logger.debug("Skipping general map info update: MainWindow or map manager not ready.")
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
return
map_manager: "MapCanvasManager" = self.main_window.map_manager_instance
if hasattr(map_manager, "get_current_map_info"):
map_info = {}
try:
map_info = map_manager.get_current_map_info()
zoom, map_geo_bounds, target_bbox_input, flight_count = (
map_info.get("zoom"), map_info.get("map_geo_bounds"),
map_info.get("target_bbox_input"), map_info.get("flight_count")
)
map_size_km_w, map_size_km_h = map_info.get("map_size_km_w"), map_info.get("map_size_km_h")
map_size_str = "N/A"
if map_size_km_w is not None and map_size_km_h is not None:
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)"
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, 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)
else: module_logger.warning("MainWindow missing 'update_general_map_info_display' method.")
except Exception as e_get_info:
module_logger.error(f"Error getting map info from map manager: {e_get_info}", exc_info=False)
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
else:
module_logger.warning("MapCanvasManager missing 'get_current_map_info' method.")
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
def map_zoom_in(self):
# ... (come prima)
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):
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.")
def map_zoom_out(self):
# ... (come prima)
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):
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.")
def map_pan_direction(self, direction: str):
# ... (come prima)
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):
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}.")
def map_center_on_coords_and_fit_patch(self, lat: float, lon: float, patch_size_km: float):
# ... (come prima)
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.")
def set_map_track_length(self, length: int):
# ... (come prima)
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)
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}")
else:
module_logger.warning("MapCanvasManager or 'set_max_track_points' N/A to set track length.")