SXXXXXXX_FlightMonitor/flightmonitor/controller/app_controller.py
2025-06-04 12:38:41 +02:00

1014 lines
43 KiB
Python

# FlightMonitor/controller/app_controller.py
from queue import Queue, Empty as QueueEmpty
import time
import os
import csv
import copy
from datetime import datetime, timezone
from ..data.opensky_live_adapter import (
OpenSkyLiveAdapter,
AdapterMessage,
)
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,
)
from .aircraft_db_importer import AircraftDBImporter
from .live_data_processor import LiveDataProcessor
from ..map.map_utils import _is_valid_bbox_dict
if TYPE_CHECKING:
from ..gui.main_window import MainWindow
from ..gui.dialogs.import_progress_dialog import ImportProgressDialog
from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow
from ..map.map_canvas_manager import MapCanvasManager
module_logger = get_logger(__name__)
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._active_bounding_box: Optional[Dict[str, float]] = None
self.data_storage: Optional[DataStorage] = None
self.aircraft_db_manager: Optional[AircraftDatabaseManager] = None
self.aircraft_db_importer: Optional[AircraftDBImporter] = None
self.live_data_processor: Optional[LiveDataProcessor] = None
self.active_detail_window_icao: Optional[str] = None
self.active_detail_window_ref: Optional["FullFlightDetailsWindow"] = 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: {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: {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."
)
if self.aircraft_db_manager and self.main_window:
self.aircraft_db_importer = AircraftDBImporter(
self.aircraft_db_manager, self.main_window
)
module_logger.info("AircraftDBImporter initialized successfully by AppController.")
else:
module_logger.warning("AircraftDBImporter could not be initialized due to missing dependencies.")
self.flight_data_queue = Queue(maxsize=200)
self.live_data_processor = LiveDataProcessor(self, self.flight_data_queue)
module_logger.info("LiveDataProcessor initialized successfully by 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 start_live_monitoring(self, bounding_box: Dict[str, float]):
if not self.main_window:
module_logger.error("Controller: Main window not set for live monitoring.")
return
if not bounding_box or not _is_valid_bbox_dict(bounding_box):
err_msg = "Controller: Bounding box is required and must be valid."
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 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."
)
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
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()
if self.flight_data_queue:
while not self.flight_data_queue.empty():
try:
self.flight_data_queue.get_nowait()
self.flight_data_queue.task_done()
except QueueEmpty:
break
except Exception:
break
if not self.live_data_processor:
module_logger.critical("Controller: LiveDataProcessor not initialized. Cannot start monitoring.")
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
self.main_window._reset_gui_to_stopped_state("Start failed: Internal error (Processor N/A).")
return
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()
self.live_data_processor.start_processing_queue()
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."
)
return
module_logger.info(
f"Controller: Stopping live monitoring (from_error={from_error})."
)
self.is_live_monitoring_active = False
if self.live_data_processor:
self.live_data_processor.stop_processing_queue()
else:
module_logger.warning("Controller: LiveDataProcessor not initialized during stop_live_monitoring.")
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.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."
)
except Exception as e_join:
module_logger.error(f"Error stopping adapter: {e_join}", exc_info=True)
finally:
self.live_adapter_thread = None
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)
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()
):
try:
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:
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
):
map_manager = self.main_window.map_manager_instance
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."
)
except Exception as e_map_shutdown:
module_logger.error(
f"Controller: Error during Main MapCanvasManager worker shutdown: {e_map_shutdown}",
exc_info=True,
)
if self.live_data_processor and self.is_live_monitoring_active:
module_logger.info("Controller: Stopping LiveDataProcessor explicitly on app exit.")
self.live_data_processor.stop_processing_queue()
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()
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
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.info("Controller: Cleanup on application exit finished.")
def start_history_monitoring(self):
if not self.main_window:
module_logger.error("Main window not set for history monitoring.")
return
if not self.data_storage:
err_msg = "DataStorage not initialized. Cannot use history features."
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
if hasattr(self.main_window, "update_semaphore_and_status"):
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 hasattr(self.main_window, "update_semaphore_and_status"):
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})"
)
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
)
def import_aircraft_database_from_file_with_progress(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
if not self.aircraft_db_importer:
module_logger.error(
"AppController: AircraftDBImporter not initialized. Cannot perform import."
)
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.root.after(0, lambda: progress_dialog_ref.import_finished(
False, "Error: Import function not initialized correctly (controller issue)."
))
elif self.main_window and hasattr(self.main_window, "show_error_message"):
self.main_window.show_error_message(
"Import Error", "Aircraft database importer not ready."
)
return
self.aircraft_db_importer.import_aircraft_database_with_progress(
csv_filepath, progress_dialog_ref
)
# MODIFIED: Re-added request_detailed_flight_info (this was the missing function)
# WHY: This function is crucial for displaying selected flight details in the main window panel.
# It was inadvertently removed during the refactoring process.
# HOW: Re-inserted the function with its corrected logic for combining live and static data.
def request_detailed_flight_info(self, icao24: str):
normalized_icao24 = icao24.lower().strip()
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."
)
return
if not normalized_icao24:
if hasattr(self.main_window, "update_selected_flight_details"):
self.main_window.update_selected_flight_details(None)
return
live_data_for_panel: Optional[Dict[str, Any]] = None
static_data_for_panel: Optional[Dict[str, Any]] = None
# Initialize combined_details_for_panel with icao24.
combined_details_for_panel: Dict[str, Any] = {"icao24": normalized_icao24}
# Process live data first
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_for_panel = state.to_dict()
break
if live_data_for_panel:
# IMPORTANT: Use .update() to ensure live data fields overwrite existing ones
combined_details_for_panel.update(live_data_for_panel)
module_logger.debug(f"AppController: Added live data to details for {normalized_icao24}.")
# Process static data second, only if key not already set by live data
if self.aircraft_db_manager:
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():
# Only add static data if the key is not already populated (e.g., by live data)
if k not in combined_details_for_panel or combined_details_for_panel[k] is None:
combined_details_for_panel[k] = v
module_logger.debug(f"AppController: Added static data to details for {normalized_icao24}.")
# Final call to update the GUI panel
if hasattr(self.main_window, "update_selected_flight_details"):
self.main_window.update_selected_flight_details(combined_details_for_panel)
module_logger.debug(f"AppController: Called update_selected_flight_details with combined data for {normalized_icao24}.")
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}"
)
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."
)
return
if (
self.active_detail_window_ref
and self.active_detail_window_ref.winfo_exists()
):
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()
else:
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
static_data: Optional[Dict[str, Any]] = None
if self.aircraft_db_manager:
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")
):
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
)
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,
)
try:
from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow
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:
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
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."
)
if hasattr(self.main_window, "show_error_message"):
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,
)
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
self.active_detail_window_icao = 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()
if self.active_detail_window_icao == normalized_closed_icao24:
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
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()
):
self.main_window.full_flight_details_window = None
else:
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})"
)
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
):
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):
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:
module_logger.error(
f"Error map_manager.recenter_map_at_coords: {e}", 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
):
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:
module_logger.error(
f"Error map_manager.set_bbox_around_coords: {e}",
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}"
)
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):
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"
):
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_info.get("zoom")
map_geo_bounds = map_info.get("map_geo_bounds")
target_bbox_input_from_map_info = map_info.get("target_bbox_input")
flight_count = map_info.get("flight_count")
map_size_km_w = map_info.get("map_size_km_w")
map_size_km_h = 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_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 Exception as e_get_info:
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
else:
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
):
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):
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):
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
):
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):
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."
)