From 8b284ea109f71e28322138828d6dce4ae366de08 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Thu, 15 May 2025 09:52:24 +0200 Subject: [PATCH] Initial commit for profile FlightMonitor --- FlightMonitor.ico | Bin 0 -> 43801 bytes flightmonitor/__main__.py | 41 +- .../{core/core.py => controller/__init__.py} | 0 flightmonitor/controller/app_controller.py | 110 ++++++ .../{gui/gui.py => data/__init__.py} | 0 flightmonitor/data/live_fetcher.py | 140 +++++++ flightmonitor/gui/main_window.py | 349 ++++++++++++++++++ 7 files changed, 627 insertions(+), 13 deletions(-) rename flightmonitor/{core/core.py => controller/__init__.py} (100%) create mode 100644 flightmonitor/controller/app_controller.py rename flightmonitor/{gui/gui.py => data/__init__.py} (100%) create mode 100644 flightmonitor/data/live_fetcher.py create mode 100644 flightmonitor/gui/main_window.py diff --git a/FlightMonitor.ico b/FlightMonitor.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..230319cc4f88676f8b4fdc77fe55ea91b923c269 100644 GIT binary patch literal 43801 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 Z{~s8s3SAzh%!&X2002ovPDHLkV1kZwjy3=Q literal 0 HcmV?d00001 diff --git a/flightmonitor/__main__.py b/flightmonitor/__main__.py index 2acfea8..a0236fb 100644 --- a/flightmonitor/__main__.py +++ b/flightmonitor/__main__.py @@ -1,17 +1,32 @@ -# flightmonitor/__main__.py - -# Example import assuming your main logic is in a 'main' function -# within a 'app' module in your 'flightmonitor.core' package. -# from flightmonitor.core.app import main as start_application -# -# Or, if you have a function in flightmonitor.core.core: -# from flightmonitor.core.core import main_function +import tkinter as tk +from .gui.main_window import MainWindow +from .controller.app_controller import AppController def main(): - print(f"Running FlightMonitor...") - # Placeholder: Replace with your application's entry point - # Example: start_application() - print("To customize, edit 'flightmonitor/__main__.py' and your core modules.") + """ + Main function to launch the Flight Monitor application. + Initializes the Tkinter root, the application controller, and the main window. + """ + # Initialize the Tkinter root window + root = tk.Tk() + + # Create the application controller + # The controller might need a reference to the window, + # and the window will need a reference to the controller. + # We can pass the controller to the window, and then set the window in the controller. + app_controller = AppController() + + # Create the main application window + # Pass the root and the controller to the MainWindow + main_app_window = MainWindow(root, app_controller) + + # Set the main window instance in the controller if it needs to call methods on the window + app_controller.set_main_window(main_app_window) + + # Start the Tkinter event loop + root.mainloop() if __name__ == "__main__": - main() + # This ensures that main() is called only when the script is executed directly + # (e.g., python -m FlightMonitor) and not when imported as a module. + main() \ No newline at end of file diff --git a/flightmonitor/core/core.py b/flightmonitor/controller/__init__.py similarity index 100% rename from flightmonitor/core/core.py rename to flightmonitor/controller/__init__.py diff --git a/flightmonitor/controller/app_controller.py b/flightmonitor/controller/app_controller.py new file mode 100644 index 0000000..e07a36e --- /dev/null +++ b/flightmonitor/controller/app_controller.py @@ -0,0 +1,110 @@ +# FlightMonitor/controller/app_controller.py + +# Import LiveFetcher from the data module +from .data.live_fetcher import LiveFetcher +# We will need to instantiate it, so no need to import the entire module if only class is used + +class AppController: + """ + Coordinates operations between the GUI and the data/logic modules. + Manages the application's state and flow. + """ + def __init__(self, main_window=None): # main_window can be set later + """ + Initializes the AppController. + + Args: + main_window: The main GUI window instance. + """ + self.main_window = main_window + self.live_fetcher = None # Will be an instance of LiveFetcher + self.is_live_monitoring_active = False + + def set_main_window(self, main_window): + """ + Sets the main window instance after initialization, if needed. + + Args: + main_window: The main GUI window instance. + """ + self.main_window = main_window + + def start_live_monitoring(self, bounding_box: dict): + """ + Starts the live flight data monitoring process. + + Args: + bounding_box (dict): The geographical bounding box for filtering flights. + """ + if not self.main_window: + print("Error: Main window not set in controller.") + return + + if self.is_live_monitoring_active: + self.main_window.update_status("Live monitoring is already active.") + return + + self.main_window.update_status(f"Attempting to fetch live flights for bbox: {bounding_box}...") + self.is_live_monitoring_active = True # Set flag before potential blocking call + + if self.live_fetcher is None: + self.live_fetcher = LiveFetcher(timeout_seconds=15) # Initialize with a timeout + + # --- This is a SYNCHRONOUS call for now --- + # In a later phase, this will be moved to a separate thread. + flights_data = self.live_fetcher.fetch_flights(bounding_box) + + if flights_data is not None: # fetch_flights returns None on error, [] if no flights + self.main_window.update_status(f"Fetched {len(flights_data)} flights. Displaying...") + self.main_window.display_flights_on_canvas(flights_data, bounding_box) + if not flights_data: # If list is empty + self.main_window.update_status("No flights found in the selected area.") + else: + error_msg = "Failed to fetch live flight data. Check console for details." + self.main_window.update_status(error_msg) + self.main_window.show_error_message("API Error", error_msg) + # Since fetching failed, we should probably allow user to try again + # or revert the UI state to "stopped" + self.stop_live_monitoring(from_error=True) # Call stop to reset UI + + # For now, live monitoring is a one-shot fetch. + # We'll implement periodic polling later. + # So, let's simulate it stopping after one fetch for now, or the UI will stay "stuck" + # self.stop_live_monitoring() # Commented out: let user stop it manually or we'll add timer + + def stop_live_monitoring(self, from_error=False): + """Stops the live flight data monitoring process.""" + if not self.is_live_monitoring_active and not from_error: + # if called not due to an error, and not active, do nothing. + # if from_error is true, we might want to proceed to update UI regardless of this flag + if not from_error: + self.main_window.update_status("Live monitoring is not active.") + return + + self.is_live_monitoring_active = False + # In the future, if there's a running thread/task, it would be stopped here. + if self.main_window and not from_error: # only update status if not already handled by an error message + self.main_window.update_status("Live monitoring stopped.") + # If called from an error in start_live_monitoring, main_window.stop_monitoring() + # will be called by main_window itself to reset buttons. + # If we want controller to explicitly tell window to reset UI: + if self.main_window and from_error: + self.main_window._stop_monitoring() # Call the UI method to reset buttons etc. + + + def start_history_monitoring(self): + """Starts the historical flight data analysis process.""" + if not self.main_window: + print("Error: Main window not set in controller.") + return + self.main_window.update_status("History monitoring started (placeholder).") + # Logic for loading and displaying historical data will go here. + # For now, main_window._start_monitoring handles the placeholder display. + + def stop_history_monitoring(self): + """Stops the historical flight data analysis process.""" + if not self.main_window: + # print("Error: Main window not set in controller.") # Can be noisy if not active + return + self.main_window.update_status("History monitoring stopped.") + # Clean up historical data display, etc. \ No newline at end of file diff --git a/flightmonitor/gui/gui.py b/flightmonitor/data/__init__.py similarity index 100% rename from flightmonitor/gui/gui.py rename to flightmonitor/data/__init__.py diff --git a/flightmonitor/data/live_fetcher.py b/flightmonitor/data/live_fetcher.py new file mode 100644 index 0000000..7634564 --- /dev/null +++ b/flightmonitor/data/live_fetcher.py @@ -0,0 +1,140 @@ +# FlightMonitor/data/live_fetcher.py +import requests # External library for making HTTP requests +import json # For parsing JSON responses + +# It's good practice to define the API endpoint URL as a constant +OPENSKY_API_URL = "https://opensky-network.org/api/states/all" + +class LiveFetcher: + """ + Fetches live flight data from an external API (e.g., OpenSky Network). + """ + + def __init__(self, timeout_seconds=10): + """ + Initializes the LiveFetcher. + + Args: + timeout_seconds (int): Timeout for the API request in seconds. + """ + self.timeout = timeout_seconds + + def fetch_flights(self, bounding_box: dict): + """ + Fetches current flight states within a given geographical bounding box. + + Args: + bounding_box (dict): A dictionary containing the keys + 'lat_min', 'lon_min', 'lat_max', 'lon_max'. + + Returns: + list: A list of dictionaries, where each dictionary represents a flight + and contains keys like 'icao24', 'callsign', 'longitude', 'latitude', + 'altitude', 'velocity', 'heading'. + Returns None if an error occurs (e.g., network issue, API error). + """ + if not bounding_box: + print("Error: Bounding box not provided to fetch_flights.") + return None + + params = { + "lamin": bounding_box["lat_min"], + "lomin": bounding_box["lon_min"], + "lamax": bounding_box["lat_max"], + "lomax": bounding_box["lon_max"], + } + + try: + response = requests.get(OPENSKY_API_URL, params=params, timeout=self.timeout) + response.raise_for_status() # Raises an HTTPError for bad responses (4XX or 5XX) + + data = response.json() + # print(f"Raw data from OpenSky: {json.dumps(data, indent=2)}") # For debugging + + if data and "states" in data and data["states"] is not None: + flights_list = [] + for state_vector in data["states"]: + # According to OpenSky API documentation for /states/all: + # 0: icao24 + # 1: callsign + # 2: origin_country + # 5: longitude + # 6: latitude + # 7: baro_altitude (meters) + # 8: on_ground (boolean) + # 9: velocity (m/s over ground) + # 10: true_track (degrees, 0-360, clockwise from North) + # 13: geo_altitude (meters) + # Ensure vector has enough elements and longitude/latitude are not None + if len(state_vector) > 13 and state_vector[5] is not None and state_vector[6] is not None: + flight_info = { + "icao24": state_vector[0].strip() if state_vector[0] else "N/A", + "callsign": state_vector[1].strip() if state_vector[1] else "N/A", + "origin_country": state_vector[2], + "longitude": float(state_vector[5]), + "latitude": float(state_vector[6]), + "baro_altitude": float(state_vector[7]) if state_vector[7] is not None else None, + "on_ground": bool(state_vector[8]), + "velocity": float(state_vector[9]) if state_vector[9] is not None else None, + "true_track": float(state_vector[10]) if state_vector[10] is not None else None, + "geo_altitude": float(state_vector[13]) if state_vector[13] is not None else None, + } + # Use callsign if available and not empty, otherwise icao24 for display label + flight_info["display_label"] = flight_info["callsign"] if flight_info["callsign"] != "N/A" and flight_info["callsign"] else flight_info["icao24"] + + flights_list.append(flight_info) + return flights_list + else: + # No states found or states is null (can happen if area is empty or over water with no traffic) + print("No flight states returned from API or 'states' field is null.") + return [] # Return empty list if no flights, not None + except requests.exceptions.Timeout: + print(f"Error: API request timed out after {self.timeout} seconds.") + return None + except requests.exceptions.HTTPError as http_err: + print(f"Error: HTTP error occurred: {http_err} - Status: {response.status_code}") + # You could inspect response.text or response.json() here for more details if needed + # print(f"Response content: {response.text}") + return None + except requests.exceptions.RequestException as req_err: + print(f"Error: An error occurred during API request: {req_err}") + return None + except json.JSONDecodeError: + print("Error: Could not decode JSON response from API.") + return None + except ValueError as val_err: # Handles potential float conversion errors + print(f"Error: Could not parse flight data value: {val_err}") + return None + +if __name__ == '__main__': + # Example usage for testing the fetcher directly + print("Testing LiveFetcher...") + # Example bounding box (Switzerland) + # Note: OpenSky may return empty for very small or specific unloaded areas + # Larger areas like Central Europe are more likely to have data. + example_bbox = { + "lat_min": 45.8389, "lon_min": 5.9962, + "lat_max": 47.8229, "lon_max": 10.5226 + } + # example_bbox = { # Broader Central Europe + # "lat_min": 45.0, "lon_min": 5.0, + # "lat_max": 55.0, "lon_max": 15.0 + # } + + fetcher = LiveFetcher(timeout_seconds=15) + flights = fetcher.fetch_flights(example_bbox) + + if flights is not None: + if flights: # Check if list is not empty + print(f"Successfully fetched {len(flights)} flights.") + for i, flight in enumerate(flights): + if i < 5: # Print details of first 5 flights + print( + f" Flight {i+1}: {flight.get('display_label', 'N/A')} " + f"Lat={flight.get('latitude')}, Lon={flight.get('longitude')}, " + f"Alt={flight.get('baro_altitude')}m, Vel={flight.get('velocity')}m/s" + ) + else: + print("No flights found in the specified area.") + else: + print("Failed to fetch flights.") \ No newline at end of file diff --git a/flightmonitor/gui/main_window.py b/flightmonitor/gui/main_window.py new file mode 100644 index 0000000..8ce272a --- /dev/null +++ b/flightmonitor/gui/main_window.py @@ -0,0 +1,349 @@ +import tkinter as tk +from tkinter import ttk +from tkinter import messagebox # Importato per eventuali messaggi di errore + +# Valori di default per il bounding box (Europa centrale circa) +DEFAULT_LAT_MIN = 45.0 +DEFAULT_LON_MIN = 5.0 +DEFAULT_LAT_MAX = 55.0 +DEFAULT_LON_MAX = 15.0 +DEFAULT_CANVAS_WIDTH = 800 +DEFAULT_CANVAS_HEIGHT = 600 + +class MainWindow: + """ + Main window of the Flight Monitor application. + It handles the layout and basic user interactions. + """ + def __init__(self, root: tk.Tk, controller): + """ + Initializes the main window. + + Args: + root (tk.Tk): The root Tkinter object. + controller: The application controller instance. + """ + self.root = root + self.controller = controller + self.root.title("Flight Monitor") + self.root.geometry("1000x750") # Aumentata un po' la dimensione per i nuovi widget + + # Main frame + self.main_frame = ttk.Frame(self.root, padding="10") + self.main_frame.pack(fill=tk.BOTH, expand=True) + + # --- Control Frame --- + self.control_frame = ttk.LabelFrame(self.main_frame, text="Controls", padding="10") + self.control_frame.pack(side=tk.TOP, fill=tk.X, pady=5) + + # Mode selection + self.mode_var = tk.StringVar(value="Live") # Default to Live + self.live_radio = ttk.Radiobutton( + self.control_frame, + text="Live", + variable=self.mode_var, + value="Live", + command=self._on_mode_change + ) + self.live_radio.pack(side=tk.LEFT, padx=5) + + self.history_radio = ttk.Radiobutton( + self.control_frame, + text="History", + variable=self.mode_var, + value="History", + command=self._on_mode_change + ) + self.history_radio.pack(side=tk.LEFT, padx=5) + + # Start/Stop buttons + self.start_button = ttk.Button( + self.control_frame, + text="Start Monitoring", + command=self._start_monitoring + ) + self.start_button.pack(side=tk.LEFT, padx=5) + + self.stop_button = ttk.Button( + self.control_frame, + text="Stop Monitoring", + command=self._stop_monitoring, + state=tk.DISABLED + ) + self.stop_button.pack(side=tk.LEFT, padx=5) + + # --- Bounding Box Input Frame --- + self.bbox_frame = ttk.LabelFrame(self.main_frame, text="Geographic Area (Bounding Box)", padding="10") + self.bbox_frame.pack(side=tk.TOP, fill=tk.X, pady=5) + + # Lat Min + ttk.Label(self.bbox_frame, text="Lat Min:").grid(row=0, column=0, padx=5, pady=2, sticky=tk.W) + self.lat_min_var = tk.StringVar(value=str(DEFAULT_LAT_MIN)) + self.lat_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_min_var, width=10) + self.lat_min_entry.grid(row=0, column=1, padx=5, pady=2) + + # Lon Min + ttk.Label(self.bbox_frame, text="Lon Min:").grid(row=0, column=2, padx=5, pady=2, sticky=tk.W) + self.lon_min_var = tk.StringVar(value=str(DEFAULT_LON_MIN)) + self.lon_min_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_min_var, width=10) + self.lon_min_entry.grid(row=0, column=3, padx=5, pady=2) + + # Lat Max + ttk.Label(self.bbox_frame, text="Lat Max:").grid(row=1, column=0, padx=5, pady=2, sticky=tk.W) + self.lat_max_var = tk.StringVar(value=str(DEFAULT_LAT_MAX)) + self.lat_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lat_max_var, width=10) + self.lat_max_entry.grid(row=1, column=1, padx=5, pady=2) + + # Lon Max + ttk.Label(self.bbox_frame, text="Lon Max:").grid(row=1, column=2, padx=5, pady=2, sticky=tk.W) + self.lon_max_var = tk.StringVar(value=str(DEFAULT_LON_MAX)) + self.lon_max_entry = ttk.Entry(self.bbox_frame, textvariable=self.lon_max_var, width=10) + self.lon_max_entry.grid(row=1, column=3, padx=5, pady=2) + + + # --- Output Area (Canvas) --- + self.output_frame = ttk.LabelFrame(self.main_frame, text="Flight Map", padding="10") + self.output_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, pady=5) + + self.flight_canvas = tk.Canvas( + self.output_frame, + bg="lightgray", # Background color for visibility + width=DEFAULT_CANVAS_WIDTH, + height=DEFAULT_CANVAS_HEIGHT + ) + self.flight_canvas.pack(fill=tk.BOTH, expand=True) + # Placeholder text on canvas (can be removed or updated dynamically) + self.flight_canvas.create_text( + DEFAULT_CANVAS_WIDTH / 2, + DEFAULT_CANVAS_HEIGHT / 2, + text="Map Area - Waiting for data...", + tags="placeholder_text" + ) + + + # --- Status Bar --- + self.status_bar_frame = ttk.Frame(self.root, padding=(5, 2)) + self.status_bar_frame.pack(side=tk.BOTTOM, fill=tk.X) + self.status_label = ttk.Label(self.status_bar_frame, text="Status: Idle") + self.status_label.pack(side=tk.LEFT) + + self._on_mode_change() # Initialize view based on default mode + + def _on_mode_change(self): + """Handles UI changes when the monitoring mode (Live/History) changes.""" + selected_mode = self.mode_var.get() + self.update_status(f"Mode changed to {selected_mode}") + if selected_mode == "Live": + self.bbox_frame.pack(side=tk.TOP, fill=tk.X, pady=5) # Show bbox for live + self.flight_canvas.itemconfig("placeholder_text", text="Map Area - Waiting for live data...") + # Enable/disable fields as needed + self._set_bbox_entries_state(tk.NORMAL) + else: # History mode + # self.bbox_frame.pack_forget() # Hide bbox for history (or adapt for history filters) + # For now, let's keep it visible but disabled for History + self._set_bbox_entries_state(tk.DISABLED) + self.flight_canvas.itemconfig("placeholder_text", text="Map Area - Waiting for historical data...") + self.clear_canvas() # Clear canvas on mode change + + def _set_bbox_entries_state(self, state): + """Enable or disable bounding box entry fields.""" + self.lat_min_entry.config(state=state) + self.lon_min_entry.config(state=state) + self.lat_max_entry.config(state=state) + self.lon_max_entry.config(state=state) + + def _start_monitoring(self): + """Starts the monitoring process based on the selected mode.""" + selected_mode = self.mode_var.get() + self.update_status(f"Starting {selected_mode} monitoring...") + self.start_button.config(state=tk.DISABLED) + self.stop_button.config(state=tk.NORMAL) + self.live_radio.config(state=tk.DISABLED) + self.history_radio.config(state=tk.DISABLED) + self._set_bbox_entries_state(tk.DISABLED) # Disable bbox while running + + if selected_mode == "Live": + try: + bounding_box = self.get_bounding_box() + if bounding_box: + self.controller.start_live_monitoring(bounding_box) + else: + # Error already shown by get_bounding_box + self._stop_monitoring() # Revert state + except ValueError as e: + messagebox.showerror("Input Error", str(e)) + self._stop_monitoring() # Revert state if there was an error before calling controller + elif selected_mode == "History": + self.controller.start_history_monitoring() + # Placeholder for history: + self.update_status("History mode selected. Displaying sample historical data.") + self.flight_canvas.delete("placeholder_text") # Remove placeholder + # Example: Draw a dummy item for history + self.flight_canvas.create_oval(90, 90, 110, 110, fill="blue", outline="black", tags="flight_dot") + self.flight_canvas.create_text(100, 125, text="HIST_FLIGHT", tags="flight_label") + + + def _stop_monitoring(self): + """Stops the monitoring process.""" + self.update_status("Monitoring stopped. Status: Idle") + self.start_button.config(state=tk.NORMAL) + self.stop_button.config(state=tk.DISABLED) + self.live_radio.config(state=tk.NORMAL) + self.history_radio.config(state=tk.NORMAL) + self._set_bbox_entries_state(tk.NORMAL if self.mode_var.get() == "Live" else tk.DISABLED) + + selected_mode = self.mode_var.get() + if selected_mode == "Live": + self.controller.stop_live_monitoring() + elif selected_mode == "History": + self.controller.stop_history_monitoring() + # self.clear_canvas() # Optionally clear canvas on stop + # self.flight_canvas.create_text( + # DEFAULT_CANVAS_WIDTH / 2, + # DEFAULT_CANVAS_HEIGHT / 2, + # text="Map Area - Monitoring stopped.", + # tags="placeholder_text" + # ) + + + def get_bounding_box(self): + """ + Retrieves and validates the bounding box coordinates from the input fields. + + Returns: + dict: A dictionary with 'lat_min', 'lon_min', 'lat_max', 'lon_max' if valid. + None: If validation fails. + """ + try: + lat_min = float(self.lat_min_var.get()) + lon_min = float(self.lon_min_var.get()) + lat_max = float(self.lat_max_var.get()) + lon_max = float(self.lon_max_var.get()) + + if not (-90 <= lat_min <= 90 and -90 <= lat_max <= 90 and + -180 <= lon_min <= 180 and -180 <= lon_max <= 180): + messagebox.showerror("Input Error", "Invalid latitude or longitude range.") + return None + if lat_min >= lat_max: + messagebox.showerror("Input Error", "Lat Min must be less than Lat Max.") + return None + if lon_min >= lon_max: + messagebox.showerror("Input Error", "Lon Min must be less than Lon Max.") + return None + return { + "lat_min": lat_min, "lon_min": lon_min, + "lat_max": lat_max, "lon_max": lon_max + } + except ValueError: + messagebox.showerror("Input Error", "Bounding box coordinates must be valid numbers.") + return None + + def display_flights_on_canvas(self, flights_data, bounding_box): + """ + Displays flight data on the canvas. + + Args: + flights_data (list): A list of flight data dictionaries. + Each dictionary should have 'latitude', 'longitude', + and 'callsign' (or 'icao24'). + bounding_box (dict): The bounding box used for fetching, + needed for coordinate scaling. + """ + self.clear_canvas() + if not flights_data: + self.flight_canvas.create_text( + self.flight_canvas.winfo_width() / 2, + self.flight_canvas.winfo_height() / 2, + text="No flights found in the selected area.", + tags="placeholder_text" + ) + return + + canvas_width = self.flight_canvas.winfo_width() + canvas_height = self.flight_canvas.winfo_height() + + # If canvas hasn't been drawn yet, its dimensions might be 1. Use defaults. + if canvas_width <= 1 or canvas_height <= 1: + canvas_width = DEFAULT_CANVAS_WIDTH + canvas_height = DEFAULT_CANVAS_HEIGHT + # Schedule a redraw if we used defaults, as winfo_width/height might be updated later + self.root.after(100, lambda: self.display_flights_on_canvas(flights_data, bounding_box)) + # return # Optional: wait for actual dimensions + + # Unpack bounding box for easier access + lat_min = bounding_box["lat_min"] + lon_min = bounding_box["lon_min"] + lat_max = bounding_box["lat_max"] + lon_max = bounding_box["lon_max"] + + # Ensure deltas are not zero to avoid division by zero + delta_lat = lat_max - lat_min + delta_lon = lon_max - lon_min + if delta_lat == 0: delta_lat = 1 # Avoid division by zero + if delta_lon == 0: delta_lon = 1 # Avoid division by zero + + + for flight in flights_data: + lat = flight.get("latitude") + lon = flight.get("longitude") + callsign = flight.get("callsign", flight.get("icao24", "N/A")) # Use callsign or icao24 + + if lat is None or lon is None: + # print(f"Skipping flight {callsign} due to missing coordinates.") + continue # Skip if no coordinates + + # Basic scaling: map geographic coordinates to canvas coordinates + # X corresponds to longitude, Y corresponds to latitude + # Canvas Y is inverted (0 at top, height at bottom) + # Ensure longitude is within the bounding box to avoid extreme scaling issues if data is outside + # (though API should filter, this is a safeguard for display) + if not (lon_min <= lon <= lon_max and lat_min <= lat <= lat_max): + # print(f"Flight {callsign} ({lat}, {lon}) is outside bbox {bounding_box} - skipping display.") + continue + + x_pixel = ((lon - lon_min) / delta_lon) * canvas_width + y_pixel = canvas_height - (((lat - lat_min) / delta_lat) * canvas_height) + + # Draw a small circle for the aircraft + radius = 3 + self.flight_canvas.create_oval( + x_pixel - radius, y_pixel - radius, + x_pixel + radius, y_pixel + radius, + fill="red", outline="black", tags="flight_dot" + ) + # Optionally, display callsign + self.flight_canvas.create_text( + x_pixel, y_pixel - (radius + 5), # Position text above dot + text=callsign, + font=("Arial", 7), + tags="flight_label" + ) + self.update_status(f"Displayed {len(flights_data)} flights on map.") + + + def clear_canvas(self): + """Clears all items (flights, placeholder) from the canvas.""" + self.flight_canvas.delete("flight_dot") + self.flight_canvas.delete("flight_label") + self.flight_canvas.delete("placeholder_text") + + def update_status(self, message: str): + """ + Updates the status bar with a message. + + Args: + message (str): The message to display. + """ + self.status_label.config(text=f"Status: {message}") + # print(f"Status Update: {message}") # For console logging during development + + def show_error_message(self, title: str, message: str): + """ + Displays an error message box. + + Args: + title (str): The title of the message box. + message (str): The error message. + """ + messagebox.showerror(title, message) + self.update_status(f"Error: {message[:50]}...") # Update status bar as well \ No newline at end of file