1254 lines
56 KiB
Python
1254 lines
56 KiB
Python
# FlightMonitor/controller/app_controller.py
|
|
from queue import Queue, Empty as QueueEmpty
|
|
import threading
|
|
import tkinter as tk # Usato solo per tk.TclError e TYPE_CHECKING
|
|
import time
|
|
|
|
from ..data.opensky_live_adapter import (
|
|
OpenSkyLiveAdapter, # Ora usa la libreria ufficiale
|
|
AdapterMessage,
|
|
MSG_TYPE_FLIGHT_DATA,
|
|
MSG_TYPE_ADAPTER_STATUS,
|
|
STATUS_STARTING,
|
|
STATUS_FETCHING,
|
|
STATUS_RECOVERED,
|
|
STATUS_RATE_LIMITED, # Potrebbe essere meno usato se la libreria gestisce internamente
|
|
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 typing import List, Optional, Dict, Any, TYPE_CHECKING
|
|
|
|
from ..utils.gui_utils import (
|
|
GUI_STATUS_OK,
|
|
GUI_STATUS_WARNING,
|
|
GUI_STATUS_ERROR,
|
|
GUI_STATUS_FETCHING,
|
|
GUI_STATUS_UNKNOWN,
|
|
# GUI_STATUS_PERMANENT_FAILURE #STATUS_PERMANENT_FAILURE è usato dall'adapter
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from ..gui.main_window import MainWindow
|
|
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
|
|
|
|
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
|
|
|
|
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.main_window
|
|
and hasattr(self.main_window, "root")
|
|
and self.main_window.root.winfo_exists()
|
|
and hasattr(self.main_window, "update_semaphore_and_status")
|
|
):
|
|
if not self.data_storage:
|
|
err_msg = (
|
|
"Data storage init failed. Data will not be saved. Check logs."
|
|
)
|
|
self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, err_msg)
|
|
else:
|
|
self.main_window.update_semaphore_and_status(
|
|
GUI_STATUS_OK, "System Initialized. Ready."
|
|
)
|
|
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:
|
|
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:
|
|
module_logger.debug(
|
|
f"Received flight data with {len(flight_states_payload)} states. Processing..."
|
|
)
|
|
|
|
if self.data_storage:
|
|
saved_count = 0
|
|
for state in flight_states_payload:
|
|
if not isinstance(state, CanonicalFlightState):
|
|
module_logger.warning(
|
|
f"Received non-CanonicalFlightState object: {type(state)}. Skipping."
|
|
)
|
|
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 hasattr(
|
|
self.main_window, "display_flights_on_canvas"
|
|
) # Questo è un po' ridondante se chiamiamo map_manager
|
|
and self.is_live_monitoring_active
|
|
and self._active_bounding_box
|
|
):
|
|
module_logger.debug(
|
|
f"Controller: Calling display_flights_on_canvas with {len(flight_states_payload)} states."
|
|
)
|
|
try:
|
|
# Ora display_flights_on_canvas chiamerà map_manager.update_flights_on_map
|
|
self.main_window.display_flights_on_canvas(
|
|
flight_states_payload, self._active_bounding_box
|
|
)
|
|
except Exception as e_display:
|
|
module_logger.error(
|
|
f"Error calling display_flights_on_canvas: {e_display}",
|
|
exc_info=False,
|
|
)
|
|
|
|
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_status:
|
|
module_logger.warning(
|
|
f"TclError updating status bar (OK): {e_tcl_status}. GUI likely closing."
|
|
)
|
|
self._gui_after_id = None
|
|
return
|
|
except Exception as e_status:
|
|
module_logger.error(
|
|
f"Error updating status bar (OK): {e_status}",
|
|
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_status:
|
|
module_logger.warning(
|
|
f"TclError updating status bar (WARN): {e_tcl_status}. GUI likely closing."
|
|
)
|
|
self._gui_after_id = None
|
|
return
|
|
except Exception as e_status:
|
|
module_logger.error(
|
|
f"Error updating status bar (WARN): {e_status}",
|
|
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", {}
|
|
) # Dettagli aggiuntivi dall'adapter
|
|
|
|
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
|
|
): # Potrebbe essere gestito dalla libreria OpenSky
|
|
gui_status_level_to_set = GUI_STATUS_WARNING
|
|
delay = details_from_adapter.get(
|
|
"delay", "N/A"
|
|
) # 'delay' è chiave nel dizionario details
|
|
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"
|
|
) # 'status_code' è chiave nei details
|
|
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_status:
|
|
module_logger.warning(
|
|
f"TclError updating status bar ({gui_status_level_to_set}): {e_tcl_status}. GUI likely closing."
|
|
)
|
|
self._gui_after_id = None
|
|
return
|
|
except Exception as e_status:
|
|
module_logger.error(
|
|
f"Error updating status bar ({gui_status_level_to_set}): {e_status}",
|
|
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 # Esci dal while loop della coda
|
|
|
|
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_status:
|
|
module_logger.warning(
|
|
f"TclError updating status bar (UNKNOWN MSG): {e_tcl_status}. GUI likely closing."
|
|
)
|
|
self._gui_after_id = None
|
|
return
|
|
except Exception as e_status:
|
|
module_logger.error(
|
|
f"Error updating status bar (UNKNOWN MSG): {e_status}",
|
|
exc_info=False,
|
|
)
|
|
except Exception as e_message_processing:
|
|
module_logger.error(
|
|
f"Error processing adapter message (Type: {message.get('type')}): {e_message_processing}",
|
|
exc_info=True,
|
|
)
|
|
finally:
|
|
try:
|
|
self.flight_data_queue.task_done()
|
|
except (ValueError, RuntimeError) as e_task_done:
|
|
module_logger.error(
|
|
f"Error calling task_done on queue: {e_task_done}",
|
|
exc_info=False,
|
|
)
|
|
|
|
except tk.TclError as e_tcl_outer:
|
|
module_logger.warning(
|
|
f"TclError during adapter queue processing: {e_tcl_outer}. Aborting queue processing.",
|
|
exc_info=False,
|
|
)
|
|
self._gui_after_id = None
|
|
return
|
|
except Exception as e_outer:
|
|
module_logger.error(
|
|
f"Unexpected critical error processing adapter message 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 Exception:
|
|
pass # Ignora se la GUI sta morendo
|
|
|
|
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, window might be gone."
|
|
)
|
|
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 Exception:
|
|
pass
|
|
self._gui_after_id = None
|
|
module_logger.debug(
|
|
"_process_flight_data_queue: Not rescheduling (monitoring inactive or GUI gone)."
|
|
)
|
|
|
|
def start_live_monitoring(self, bounding_box: Dict[str, float]):
|
|
if not self.main_window:
|
|
module_logger.error(
|
|
"Controller: Main window not set. Cannot start live monitoring."
|
|
)
|
|
return
|
|
|
|
if (
|
|
not bounding_box
|
|
): # MainWindow.get_bounding_box_from_gui dovrebbe aver validato
|
|
err_msg = "Controller: Bounding box is required but was not provided."
|
|
module_logger.error(err_msg)
|
|
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
|
|
self.main_window._reset_gui_to_stopped_state(
|
|
"Start failed: Bounding box missing."
|
|
)
|
|
return
|
|
|
|
if not self.data_storage:
|
|
err_msg = "DataStorage not initialized. Live monitoring cannot start."
|
|
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 + " Check logs."
|
|
)
|
|
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
|
|
self.main_window._reset_gui_to_stopped_state(err_msg)
|
|
return
|
|
|
|
if self.is_live_monitoring_active:
|
|
module_logger.warning(
|
|
"Controller: Live monitoring already active. Start request ignored."
|
|
)
|
|
# Potrebbe essere utile resettare i bottoni GUI se questo stato è raggiunto inaspettatamente
|
|
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:
|
|
module_logger.debug(
|
|
f"Controller instructing map manager to set view for BBox: {bounding_box}"
|
|
)
|
|
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"
|
|
): # Se map_manager non è pronto, pulisci
|
|
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) # Aumentato maxsize per buffer
|
|
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, # Passa il dizionario, l'adapter lo convertirà se necessario
|
|
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):
|
|
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() # Chiamata diretta, _process_flight_data_queue non riprogrammerà
|
|
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):
|
|
module_logger.info("Controller: Application exit requested. Cleaning up resources.")
|
|
|
|
# MODIFIED: Added shutdown for MapCanvasManager's worker thread.
|
|
# WHY: To ensure the map rendering thread is properly terminated before the application exits.
|
|
# WHERE: At the beginning of the application exit process.
|
|
# HOW: Checking for the existence of main_window, map_manager_instance,
|
|
# and the shutdown_worker method, then calling it within a try-except block.
|
|
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
|
|
)
|
|
else:
|
|
module_logger.debug("Controller: MapCanvasManager instance found, but no 'shutdown_worker' method.")
|
|
else:
|
|
module_logger.debug("Controller: No MapCanvasManager instance found or main window not set; skipping map worker shutdown.")
|
|
|
|
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:
|
|
module_logger.debug("Controller: Live monitoring/adapter active during app exit, stopping it.")
|
|
self.stop_live_monitoring(from_error=False) # This will join the adapter thread
|
|
else:
|
|
module_logger.debug("Controller: Live monitoring/adapter not active or already stopped during app exit.")
|
|
|
|
if self.data_storage:
|
|
module_logger.debug("Controller: Closing DataStorage connection during app exit.")
|
|
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 # Ensure it's cleared
|
|
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.")
|
|
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):
|
|
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
|
|
):
|
|
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 not available for left click."
|
|
)
|
|
except Exception as e_dms:
|
|
module_logger.warning(
|
|
f"Error calculating 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 not available to update clicked map info."
|
|
)
|
|
|
|
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 not available for right click."
|
|
)
|
|
except Exception as e_dms:
|
|
module_logger.warning(
|
|
f"Error calculating 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 panel (right click): {e_tcl}. GUI closing."
|
|
)
|
|
except Exception as e_update:
|
|
module_logger.error(
|
|
f"Error updating map clicked info panel (right click): {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 not available 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 not available 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 calling map_manager.recenter_map_at_coords: {e_recenter}",
|
|
exc_info=False,
|
|
)
|
|
if hasattr(self.main_window, "show_error_message"):
|
|
self.main_window.show_error_message(
|
|
"Map Error", "Failed to recenter map."
|
|
)
|
|
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 calling map_manager.set_bbox_around_coords: {e_set_bbox}",
|
|
exc_info=False,
|
|
)
|
|
if hasattr(self.main_window, "show_error_message"):
|
|
self.main_window.show_error_message(
|
|
"Map Error", "Failed to set monitoring area."
|
|
)
|
|
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 not available 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
|
|
):
|
|
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()
|
|
module_logger.debug(f"Fetched map info from manager: {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):
|
|
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 calling 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 not available 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 calling 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 not available 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 calling 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 not available 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 calling 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 not available for center and fit patch.")
|
|
|
|
# MODIFICATO: Aggiunto nuovo metodo per impostare la lunghezza della traccia
|
|
# PERCHÉ: Necessario per permettere alla GUI di comunicare questa impostazione al MapCanvasManager.
|
|
# DOVE: Aggiunto come nuovo metodo pubblico nella classe AppController.
|
|
# COME: Chiama un metodo corrispondente in MapCanvasManager se disponibile.
|
|
def set_map_track_length(self, length: int):
|
|
"""
|
|
Sets the desired track length (number of points) for display on the map.
|
|
Passes this setting to the MapCanvasManager.
|
|
"""
|
|
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, # Logga lo stacktrace per errori inattesi qui
|
|
)
|
|
# Notifica l'utente tramite la GUI se possibile
|
|
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 its 'set_max_track_points' method not available to set track length."
|
|
)
|