893 lines
52 KiB
Python
893 lines
52 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
|
|
import copy
|
|
from datetime import datetime, timezone
|
|
|
|
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,
|
|
)
|
|
# MODIFIED: Import _is_valid_bbox_dict from map_utils for bounding_box validation
|
|
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__)
|
|
|
|
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
|
|
|
|
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.")
|
|
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):
|
|
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()):
|
|
self._gui_after_id = None
|
|
return
|
|
|
|
flight_payloads_this_cycle: List[CanonicalFlightState] = []
|
|
|
|
try:
|
|
while not self.flight_data_queue.empty():
|
|
message = None
|
|
try:
|
|
message = self.flight_data_queue.get(block=False, timeout=0.01)
|
|
except QueueEmpty:
|
|
break
|
|
except Exception as 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")
|
|
if flight_states_payload_chunk is not None:
|
|
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)}")
|
|
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_db_add:
|
|
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.")
|
|
|
|
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
|
|
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
|
|
|
|
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
|
|
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 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
|
|
|
|
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)
|
|
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}")
|
|
|
|
# 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()):
|
|
|
|
flight_of_interest_updated_this_cycle = False
|
|
latest_live_data_for_detail_icao: Optional[Dict[str, Any]] = None
|
|
|
|
for state_obj in flight_payloads_this_cycle:
|
|
if state_obj.icao24 == self.active_detail_window_icao:
|
|
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
|
|
|
|
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.")
|
|
|
|
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)
|
|
|
|
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
|
|
)
|
|
if 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}")
|
|
|
|
try:
|
|
self.active_detail_window_ref.update_details(
|
|
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)
|
|
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)
|
|
|
|
except Exception as e_outer:
|
|
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()):
|
|
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 Exception as 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
|
|
|
|
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 is None:
|
|
self.flight_data_queue = Queue(maxsize=200)
|
|
else:
|
|
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 self.live_adapter_thread and self.live_adapter_thread.is_alive():
|
|
module_logger.info("Old adapter thread found alive. Stopping it first.")
|
|
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("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)
|
|
finally:
|
|
self.live_adapter_thread = None
|
|
|
|
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()
|
|
|
|
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)
|
|
else:
|
|
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.")
|
|
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 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.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 (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._process_flight_data_queue()
|
|
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)
|
|
|
|
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: # 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):
|
|
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)
|
|
|
|
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 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}")
|
|
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
|
|
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")):
|
|
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:
|
|
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)
|
|
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}")
|
|
if not self.aircraft_db_manager:
|
|
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.")
|
|
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():
|
|
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()
|
|
|
|
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]:
|
|
try:
|
|
current_limit = csv.field_size_limit()
|
|
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
|
|
|
|
try:
|
|
with open(csv_filepath, "r", encoding="utf-8-sig") as f:
|
|
reader = csv.reader(f)
|
|
try:
|
|
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
|
|
|
|
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."))
|
|
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()):
|
|
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}")
|
|
|
|
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.")
|
|
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.")
|
|
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, 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():
|
|
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}")
|
|
|
|
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
|
|
|
|
# MODIFIED: Close existing detail window before opening a new one
|
|
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
|
|
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
|
|
# 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)
|
|
|
|
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 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():
|
|
details_win = self.active_detail_window_ref
|
|
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
|
|
|
|
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 # Clear refs if creation/update failed
|
|
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()
|
|
# 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.")
|
|
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
|
|
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.")
|
|
|
|
|
|
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_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):
|
|
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]):
|
|
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.") |