349 lines
15 KiB
Python
349 lines
15 KiB
Python
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 |