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