# Rewinger ATC (Smooth) - Abflug.Cloud - Emanuele Bettoni
# Smooth variant: uses in-place marker updates (set_position, set_text, change_icon)
# instead of delete+recreate to eliminate the "blink" when traffic positions update.
# Copyright (C) 2025  Abflug.Cloud - Emanuele Bettoni Development Team
# This program is under MIT License
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Rewinger is a modified version of:
# - Aircraft Tracker by Jugac64 / jlgabriel  from https://github.com/jlgabriel/Aerofly-FS4-Maps
# - Original Rewinger released by SageMage / Emanuele Bettoni from https://github.com/AeroSageMage/rewinger
# both released under MIT License by their respective authors
#
# Includes control tower detection and display functionality.
# Control towers are identified by the pattern "ATC_" in callsign with
# airplane_model (ICAO) and airplane_livery (function) fields. Towers are displayed
# with a special icon and reconstructed identifier (ATC_ICAO_FUNCTION).
import socket
import threading
import re
import tkinter as tk
from tkinter import ttk
from tkintermapview import TkinterMapView
from tkinter import font as tkfont
from tkinter import filedialog
from PIL import Image, ImageTk
from typing import Optional, Dict, Any, Tuple, List
from dataclasses import dataclass
import time
import csv
import xml.etree.ElementTree as ET
import os
import sys
import requests
# Import shared simulator data source
from simulator_data_source import SimulatorDataSource, DataSourceType, GPSData as SimulatorGPSData
from config_utils import get_butter_api_key, get_server_url, compute_own_icao
from dpi_utils import configure_tk_dpi, get_dpi_scale_factor, get_safe_window_geometry
from terminal_debug import load_config_terminal_debug_spec, terminal_debug_print

load_config_terminal_debug_spec()

# Constants
UDP_PORT = 49002  # Port for direct simulator data (Aerofly, etc.)
TRAFFIC_PORT = 49004  # Port for multiplayer server traffic (legacy UDP, now using HTTP API)
WINDOW_SIZE = "1200x1000"
MAP_SIZE = (800, 600)
CONTROL_FRAME_WIDTH = 400
INFO_DISPLAY_SIZE = (24, 9)
UPDATE_INTERVAL = 1000  # milliseconds
RECEIVE_TIMEOUT = 5.0  # seconds
HTTP_POLL_INTERVAL = 2.0  # seconds between HTTP API polls for traffic (default, can be changed in GUI)
MIN_POLL_INTERVAL = 1.0  # minimum polling interval in seconds (to prevent server overload)
DEFAULT_TRAFFIC_RADIUS = 50.0  # miles - default radius for traffic query
SERVER_URL = get_server_url()  # Get from config file
APP_VERSION = "3.1.1"
APP_VERSION_DATE = "20260510"
APP_NAME = "Rewinger"

# Target deviation thresholds (structured for future config.json migration)
TARGET_DEVIATION_TIGHT = 0.01    # 1% - In target (Ⓣ)
TARGET_DEVIATION_MEDIUM = 0.05  # 5% - Outside target (Ⓣ*)
TARGET_DEVIATION_WIDE = 0.10    # 10% - Widely outside (Ⓣ**)

# Heading deviation (percentage of 360°)
HEADING_DEVIATION_TIGHT = 0.02   # 2% of 360° = ±7.2°
HEADING_DEVIATION_MEDIUM = 0.05  # 5% of 360° = ±18°
HEADING_DEVIATION_WIDE = 0.10    # 10% of 360° = ±36°

# Speed deviation (percentage of target speed)
SPEED_DEVIATION_TIGHT = 0.02     # 2%
SPEED_DEVIATION_MEDIUM = 0.05    # 5%
SPEED_DEVIATION_WIDE = 0.10      # 10%

# Vertical speed deviation (percentage of target VS)
VS_DEVIATION_TIGHT = 0.05        # 5%
VS_DEVIATION_MEDIUM = 0.10       # 10%
VS_DEVIATION_WIDE = 0.20         # 20%

# Altitude deviation (percentage of target altitude)
ALT_DEVIATION_TIGHT = 0.01       # 1%
ALT_DEVIATION_MEDIUM = 0.05      # 5%
ALT_DEVIATION_WIDE = 0.10        # 10%

# UI Dimensions
DEFAULT_RIGHT_PANEL_WIDTH = 400  # pixels (resizable)
MIN_RIGHT_PANEL_WIDTH = 200      # pixels
MAX_RIGHT_PANEL_WIDTH = 600      # pixels
BOTTOM_STRIPE_HEIGHT = 150       # pixels (standalone)
BOTTOM_STRIPE_HEIGHT_EMBEDDED = 30  # pixels (unified app - single row)
TRAFFIC_LIST_WINDOW_SIZE = "600x400"  # width x height

# Feature flags (for enabling/disabling UI elements while keeping functions)
ENABLE_FLIGHT_PLAN_UI = False  # Set to True to show flight plan controls in bottom stripe
ENABLE_RECORDING_UI = False     # Set to True to show recording controls in bottom stripe

# Target input constraints
SPEED_MIN = 0
SPEED_MAX = 1000
HEADING_MIN = 0
HEADING_MAX = 360
VS_MIN = -6000
VS_MAX = 6000
ALT_MIN = 0
ALT_MAX = 60000

# Keyboard shortcuts
KEYBOARD_CYCLE_MODIFIER = "Shift"
KEYBOARD_CYCLE_FORWARD = "Right"
KEYBOARD_CYCLE_BACKWARD = "Left"
@dataclass
class GPSData:
    """Dataclass to store GPS data received from the flight simulator."""
    longitude: float
    latitude: float
    altitude: float
    track: float
    ground_speed: float

@dataclass
class AttitudeData:
    """Dataclass to store attitude data received from the flight simulator."""
    true_heading: float
    pitch: float
    roll: float
@dataclass    
class AircraftData:
    """Dataclass to store airplane data received from the network."""

    id: str # Unique identifier for this particular aircraft instance
    type_id: str # ID of the plane model from a predefined list/table
    registration: str # Official registration number assigned to the airplane by its national aviation authority
    callsign: str # Assigned radio call sign used by air traffic control and pilots for identification purposes
    icao24: str # International Civil Aviation Organization's unique four-character identifier for this aircraft
    FlightNumber: str

@dataclass
class AirTrafficData:
    """Dataclass to store traffic data received from the network (API v2.2)."""
    icao_address: str
    latitude: float
    longitude: float
    altitude_ft: float
    vertical_speed_ft_min: float
    airborne_flag: int
    heading_true: float
    velocity_knots: float
    callsign: str
    squawk_code: str  # API v2.2 - 4-digit squawk code (default: "0000")
    departure_airport: str  # Flight plan field (API v2.0) - "N/A" if not set
    arrival_airport: str  # Flight plan field (API v2.0) - "N/A" if not set
    airplane_model: str  # Flight plan field (API v2.0) - "N/A" if not set
    airplane_livery: str  # Flight plan field (API v2.0) - "N/A" if not set
    flightplan_route: str = "NO DATA"  # SimBrief route text (API v2.2) - default if not set
    

# Target deviation calculation functions
def calculate_heading_deviation(actual_heading: int, target_heading: int) -> float:
    """
    Calculate heading deviation as percentage of 360°.
    
    Args:
        actual_heading: Current heading (0-360, integer)
        target_heading: Target heading (0-360, integer)
    
    Returns:
        Deviation percentage (0.0 to 1.0)
    """
    # Handle wrap-around (e.g., 350° to 10° = 20° difference, not 340°)
    diff = abs(actual_heading - target_heading)
    if diff > 180:
        diff = 360 - diff
    
    # Convert to percentage of 360°
    return diff / 360.0


def calculate_speed_deviation(actual_speed: float, target_speed: int) -> float:
    """
    Calculate speed deviation as percentage.
    
    Args:
        actual_speed: Current speed (knots, float)
        target_speed: Target speed (knots, integer)
    
    Returns:
        Deviation percentage (0.0 to 1.0+)
    """
    if target_speed == 0:
        return 1.0  # 100% deviation if target is 0
    
    return abs(actual_speed - target_speed) / target_speed


def calculate_vs_deviation(actual_vs: float, target_vs: int) -> float:
    """
    Calculate vertical speed deviation as percentage.
    
    Args:
        actual_vs: Current vertical speed (ft/min, float, can be negative)
        target_vs: Target vertical speed (ft/min, integer, can be negative)
    
    Returns:
        Deviation percentage (0.0 to 1.0+)
    """
    if target_vs == 0:
        # If target is 0, any non-zero VS is a deviation
        return 1.0 if actual_vs != 0 else 0.0
    
    return abs(actual_vs - target_vs) / abs(target_vs)


def calculate_altitude_deviation(actual_alt: float, target_alt: int) -> float:
    """
    Calculate altitude deviation as percentage.
    
    Args:
        actual_alt: Current altitude (feet, float, always positive)
        target_alt: Target altitude (feet, integer, always positive)
    
    Returns:
        Deviation percentage (0.0 to 1.0+)
    """
    if target_alt == 0:
        return 1.0  # 100% deviation if target is 0
    
    return abs(actual_alt - target_alt) / target_alt


def is_control_tower(traffic_data: AirTrafficData) -> bool:
    """
    Check if a traffic data entry represents a control tower.
    
    Control towers are identified by:
    1. Callsign contains "ATC_" or "_ATC_" (case-insensitive), OR
       airplane_model/airplane_livery contains "ATC_" or "_ATC_"
    2. Departure airport is a valid ICAO code (3-4 alphanumeric) - represents the airport
    3. Arrival airport is the ATC function (e.g., GROUND, TOWER, APPROACH) - represents the function
    
    This approach works around server-side callsign truncation (8 chars max).
    Example: Callsign="_ATC_" or "ATC_EDDF", Departure="EDDF", Arrival="GROUND" → ATC_EDDF_GROUND
    
    Fields with "N/A", "N/C", or "NO DATA" are skipped.
    
    Args:
        traffic_data: AirTrafficData object to check
    
    Returns:
        True if the traffic data matches control tower pattern, False otherwise
    """
    import re
    
    skip_values = ('N/A', 'N/C', 'NO DATA', '')
    icao_pattern = re.compile(r'^[A-Z0-9]{3,4}$')
    
    # Check callsign, airplane_model, and airplane_livery
    callsign = (traffic_data.callsign or "").strip().upper()
    model = (traffic_data.airplane_model or "").strip().upper()
    livery = (traffic_data.airplane_livery or "").strip().upper()
    
    # DEBUG: Log what we're checking
    terminal_debug_print("rewinger", f"[DEBUG REWINGER] Checking for control tower: callsign='{callsign}', model='{model}', livery='{livery}'")
    
    # Check if callsign contains ATC indicator (skip N/A values)
    has_atc_indicator = False
    
    if callsign and callsign not in skip_values:
        has_atc_indicator = (
            "ATC_" in callsign or "_ATC_" in callsign or callsign.startswith("ATC_")
        )
        if has_atc_indicator:
            terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Found ATC indicator in callsign: '{callsign}'")
    
    if not has_atc_indicator:
        terminal_debug_print("rewinger", f"[DEBUG REWINGER] ✗ Not a control tower: No ATC indicator in callsign")
        return False
    
    # Check model (should be ICAO code) and livery (should be function)
    icao_code = None
    function = None
    
    if model and model not in skip_values:
        if icao_pattern.match(model):
            icao_code = model
            terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Model is ICAO code: '{model}'")
        else:
            terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Model is not ICAO format: '{model}'")
    
    if livery and livery not in skip_values:
        if not icao_pattern.match(livery):  # Function should NOT match ICAO pattern
            function = livery
            terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Livery is function: '{livery}'")
        else:
            terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Livery looks like ICAO (unexpected): '{livery}'")
    
    # Consider it a tower if we have at least ICAO or function
    has_icao_or_function = (icao_code is not None) or (function is not None)
    
    result = has_atc_indicator and has_icao_or_function
    if result:
        terminal_debug_print("rewinger", f"[DEBUG REWINGER] ✓ Control tower DETECTED: callsign has ATC, ICAO='{icao_code}', function='{function}'")
    else:
        terminal_debug_print("rewinger", f"[DEBUG REWINGER] ✗ Not a control tower: ATC_indicator={has_atc_indicator}, ICAO='{icao_code}', function='{function}'")
    return result


def get_target_status_symbol(icao: str, traffic_data: Dict[str, Tuple[AirTrafficData, float]], 
                             targets: Dict[str, Dict[str, Any]]) -> str:
    """
    Get the target status symbol (Ⓣ, Ⓣ*, Ⓣ**, Ⓣ***) for an aircraft.
    
    Uses OR logic - if ANY target deviates, show asterisks.
    
    Args:
        icao: ICAO address of the aircraft
        traffic_data: Current traffic data dict
        targets: Target settings dict
    
    Returns:
        String with symbol (Ⓣ, Ⓣ*, Ⓣ**, Ⓣ***) or empty string if no targets
    """
    if icao not in targets:
        return ""  # No targets set
    
    target = targets[icao]
    traffic_entry = traffic_data.get(icao)
    
    if not traffic_entry:
        return "Ⓣ"  # Target set but no current data
    
    actual = traffic_entry[0]  # AirTrafficData object
    
    # Calculate deviations for all targets
    deviations = []
    
    if "ground_speed_kts" in target:
        deviations.append(calculate_speed_deviation(
            actual.velocity_knots, 
            target["ground_speed_kts"]
        ))
    
    if "heading_deg" in target:
        deviations.append(calculate_heading_deviation(
            int(actual.heading_true),
            target["heading_deg"]
        ))
    
    if "vertical_speed_ft_min" in target:
        deviations.append(calculate_vs_deviation(
            actual.vertical_speed_ft_min,
            target["vertical_speed_ft_min"]
        ))
    
    if "altitude_ft" in target:
        deviations.append(calculate_altitude_deviation(
            actual.altitude_ft,
            target["altitude_ft"]
        ))
    
    if not deviations:
        return "Ⓣ"
    
    # Find maximum deviation (OR logic)
    max_deviation = max(deviations)
    
    if max_deviation <= TARGET_DEVIATION_TIGHT:
        return "Ⓣ"
    elif max_deviation <= TARGET_DEVIATION_MEDIUM:
        return "Ⓣ*"
    elif max_deviation <= TARGET_DEVIATION_WIDE:
        return "Ⓣ**"
    else:
        return "Ⓣ***"


class UDPReceiver:
    """
    Class responsible for receiving and parsing UDP data from the flight simulator.
    Now uses SimulatorDataSource for GPS data (supports both UDP and FSWidget TCP).
    """
    def __init__(self, port: int = UDP_PORT, traffic_port: int = TRAFFIC_PORT,
                 use_fswidget: bool = False, fswidget_ip: str = "localhost", auto_detect: bool = False,
                 simulator_data_source: Optional[SimulatorDataSource] = None):
        self.port = port
        self.traffic_port = traffic_port
        self.socket: Optional[socket.socket] = None
        self.traffic_socket: Optional[socket.socket] = None
        
        # Use shared SimulatorDataSource for GPS data (TCP/FSWidget is main target)
        if simulator_data_source is not None:
            self.simulator_data_source = simulator_data_source
            self._owns_simulator_data_source = False
        else:
            source_type = DataSourceType.FSWIDGET if use_fswidget else DataSourceType.UDP
            self.simulator_data_source = SimulatorDataSource(
                source_type=source_type,
                udp_port=port,
                fswidget_ip=fswidget_ip,
                auto_detect=auto_detect
            )
            self._owns_simulator_data_source = True
        # Set callback to update latest_gps_data when new GPS arrives
        self.simulator_data_source.on_gps_update = self._on_gps_update
        
        self.latest_gps_data: Optional[GPSData] = None
        self.latest_attitude_data: Optional[AttitudeData] = None
        self.latest_aircraft_data: Optional[AircraftData] = None
        self.traffic_data: Dict[str, Tuple[AirTrafficData, float]] = {}
        self.running: bool = False
        self.receive_thread: Optional[threading.Thread] = None
        self.traffic_thread: Optional[threading.Thread] = None
        self.traffic_thread_stop_event: Optional[threading.Event] = None
        self.last_receive_time: float = 0
        self.log_to_csv: bool = False
        self.armed_for_recording: bool = False
        self.csv_files = {}
        self.simulator_name: str = "Unknown"
        self.simulator_name_set: bool = False
        self.use_http_api: bool = False
        self.api_server_url: Optional[str] = None
        self.api_key: Optional[str] = None
        self.callsign: Optional[str] = None
        self.traffic_radius: float = DEFAULT_TRAFFIC_RADIUS
        self.poll_interval: float = HTTP_POLL_INTERVAL
        self.http_session: Optional[requests.Session] = None
        self.manual_obs_lat: Optional[float] = None
        self.manual_obs_lon: Optional[float] = None
        self.own_icao: Optional[str] = None
    
    def replace_simulator_data_source(self, new_ds: SimulatorDataSource) -> None:
        """Replace with external data source (unified app handoff). Stops owned one if any."""
        if self._owns_simulator_data_source and self.simulator_data_source:
            self.simulator_data_source.stop()
        self.simulator_data_source = new_ds
        self.simulator_data_source.on_gps_update = self._on_gps_update
        self._owns_simulator_data_source = False
    
    def _on_gps_update(self, gps: SimulatorGPSData) -> None:
        """Callback when new GPS data arrives from SimulatorDataSource."""
        # Convert SimulatorGPSData to local GPSData format
        self.latest_gps_data = GPSData(
            longitude=gps.longitude,
            latitude=gps.latitude,
            altitude=gps.altitude,
            track=gps.track,
            ground_speed=gps.ground_speed
        )
        self.last_receive_time = time.time()


    def start_receiving(self) -> None:
        """Initialize and start the UDP receiving threads."""
        # Start simulator data source only if we own it (created it)
        if self._owns_simulator_data_source:
            self.simulator_data_source.start()
        
        # Socket for attitude and aircraft data (only if using UDP mode)
        # If using FSWidget, we only get GPS data, so this socket is not needed for GPS
        # But we still need it for attitude/aircraft data if they come via UDP
        if self.simulator_data_source.source_type == DataSourceType.UDP:
            # Socket for direct simulator data (Aerofly, etc.) on port 49002
            # Note: SimulatorDataSource already binds to this port for GPS, but we need it
            # for attitude and aircraft data too, so we'll share the socket or use SO_REUSEPORT
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            # Enable SO_REUSEPORT to allow multiple programs to bind to the same port
            if hasattr(socket, 'SO_REUSEPORT'):
                self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            self.socket.settimeout(0.5)
            self.socket.bind(('', self.port))
            
            self.running = True
            self.receive_thread = threading.Thread(target=self._receive_data, daemon=True)
            self.receive_thread.start()
        else:
            # FSWidget mode: GPS comes from SimulatorDataSource, no UDP thread needed
            self.running = True
        
        # Legacy UDP traffic socket (kept for backward compatibility, but not used if HTTP API is enabled)
        self.traffic_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.traffic_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        if hasattr(socket, 'SO_REUSEPORT'):
            self.traffic_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
        self.traffic_socket.settimeout(0.5)
        self.traffic_socket.bind(('', self.traffic_port))
        
        # Start traffic thread based on mode
        # Stop any existing traffic thread first
        if self.traffic_thread and self.traffic_thread.is_alive():
            if self.traffic_thread_stop_event:
                self.traffic_thread_stop_event.set()
            self.traffic_thread.join(timeout=2.0)
        
        # Create new stop event for the new thread
        self.traffic_thread_stop_event = threading.Event()
        
        if self.use_http_api:
            self.traffic_thread = threading.Thread(target=self._poll_traffic_http, daemon=True)
        else:
            self.traffic_thread = threading.Thread(target=self._receive_traffic, daemon=True)
        
        self.traffic_thread.start()
    
    def set_http_api(self, server_url: str, api_key: str, radius: float = DEFAULT_TRAFFIC_RADIUS, 
                     callsign: Optional[str] = None, poll_interval: float = HTTP_POLL_INTERVAL,
                     manual_lat: Optional[float] = None, manual_lon: Optional[float] = None) -> None:
        """
        Configure HTTP API for traffic polling.
        
        NOTE: Rewinger is a pure traffic viewer - it does NOT send position updates to the server.
        It only queries the traffic API using the simulator's position (UDP/FSWidget) to see nearby aircraft.
        The server should return flight plan data (callsign, departure, arrival, etc.) in the traffic
        response based on ICAO address, even if Rewinger never registered a flight plan. This allows
        Rewinger to display the correct callsign and flight plan info that was registered by Abflug Client.
        """
        self.use_http_api = True
        self.api_server_url = server_url.rstrip('/')
        self.api_key = api_key
        # Callsign will be set from server response (not from parameter)
        self.callsign = None  # Will be updated from server when we receive our own traffic
        self.traffic_radius = radius
        # Enforce minimum polling interval
        self.poll_interval = max(poll_interval, MIN_POLL_INTERVAL)
        # Store manual observation position (for observers)
        self.manual_obs_lat = manual_lat
        self.manual_obs_lon = manual_lon
        
        # Calculate own ICAO address (same method as server uses)
        self.own_icao = compute_own_icao(api_key)
        
        # Setup HTTP session
        if self.http_session:
            self.http_session.close()
        self.http_session = requests.Session()
        # Explicitly enable SSL certificate verification to prevent MITM attacks
        self.http_session.verify = True
        self.http_session.headers.update({
            'X-API-Key': self.api_key,
            'Content-Type': 'application/json'
        })
        
        # Stop old traffic thread if running (to prevent duplicate polling threads)
        if self.traffic_thread and self.traffic_thread.is_alive():
            # Signal the old thread to stop
            if self.traffic_thread_stop_event:
                self.traffic_thread_stop_event.set()
            # Wait for the thread to finish (with timeout to avoid blocking forever)
            self.traffic_thread.join(timeout=2.0)
        
        # Create new stop event for the new thread
        self.traffic_thread_stop_event = threading.Event()
        
        # Start new HTTP polling thread
        if self.running:
            self.traffic_thread = threading.Thread(target=self._poll_traffic_http, daemon=True)
            self.traffic_thread.start()
            if manual_lat is not None and manual_lon is not None:
                # Unified ATC / observer: no sim packets yet; avoid "Disconnected" until first poll
                self.last_receive_time = time.time()
            
            # NOTE: Rewinger does NOT send position updates - it's a pure traffic viewer
            # Position updates would register Rewinger as a client and may interfere with
            # Abflug Client's flight plan. Rewinger only queries traffic using the position
            # from the simulator (UDP/FSWidget) to see nearby aircraft.
            
            #print(f"HTTP API configured: {self.api_server_url} (radius: {radius} miles)")
            #print(f"Started HTTP API traffic polling thread (viewer mode - no position updates)")
    
    def disable_http_api(self) -> None:
        """Disable HTTP API and fall back to UDP."""
        # NOTE: Rewinger doesn't send position updates, so no disconnect needed
        # (it's a pure viewer, not a registered client)
        
        self.use_http_api = False
        if self.http_session:
            self.http_session.close()
            self.http_session = None
        
        # Clear manual observation position
        self.manual_obs_lat = None
        self.manual_obs_lon = None
        
        # Restart UDP traffic thread
        if self.running:
            # Stop any existing traffic thread first
            if self.traffic_thread and self.traffic_thread.is_alive():
                if self.traffic_thread_stop_event:
                    self.traffic_thread_stop_event.set()
                self.traffic_thread.join(timeout=2.0)
            
            # Create new stop event for the new thread
            self.traffic_thread_stop_event = threading.Event()
            
            self.traffic_thread = threading.Thread(target=self._receive_traffic, daemon=True)
            self.traffic_thread.start()
            #print("HTTP API disabled, using UDP for traffic")

    def _receive_data(self) -> None:
        """Continuously receive and parse UDP data while the thread is running."""
        while self.running:
            try:
                data, addr = self.socket.recvfrom(1024)
                self.last_receive_time = time.time()
                message = data.decode('utf-8')
                
                # Extract simulator name only from standard ForeFlight UDP messages
                if not self.simulator_name_set and ',' in message:
                    # Check for standard ForeFlight message types
                    if message.startswith(('XGPS', 'XATT', 'XTRAFFIC')):
                        parts = message.split(',', 1)
                        if len(parts) > 1:
                            msg_type = parts[0]
                            # Extract simulator name after the standard prefix
                            if msg_type.startswith('XGPS'):
                                self.simulator_name = msg_type[4:]
                            elif msg_type.startswith('XATT'):
                                self.simulator_name = msg_type[4:]
                            elif msg_type.startswith('XTRAFFIC'):
                                self.simulator_name = msg_type[8:]
                            
                            if self.simulator_name:  # Only set if we found a name
                                self.simulator_name_set = True
                                #print(f"Detected simulator: {self.simulator_name}")
                
                # GPS data is now handled by SimulatorDataSource, skip XGPS messages
                # (they're already being processed by the shared module)
                # if message.startswith('XGPS'):
                #     self.latest_gps_data = self._parse_gps_data(message)
                if message.startswith('XATT'):
                    self.latest_attitude_data = self._parse_attitude_data(message)
                if message.startswith('XAIRCRAFT'):
                    self.latest_aircraft_data = self._parse_aircraft_data(message)
                # Note: XTRAFFIC is now handled by _receive_traffic on port 49004
                        
                # Check if we need to start logging after arming
                if self.armed_for_recording and (self.latest_gps_data or len(self.traffic_data) > 0):
                    self.armed_for_recording = False
                    self.log_to_csv = True
                    #print("Recording automatically started after arming")
                    # Initialize CSV files
                    # Ensure output_recorder directory exists
                    os.makedirs("output_recorder", exist_ok=True)
                    timestamp = time.strftime("%Y%m%d-%H%M%S")
                    self.csv_files = {
                        'gps': open(f"output_recorder/output_GPS_DATA_{timestamp}.csv", "w", newline=''),
                        'attitude': open(f"output_recorder/output_ATTITUDE_DATA_{timestamp}.csv", "w", newline=''),
                        'traffic': open(f"output_recorder/output_TRAFFIC_DATA_{timestamp}.csv", "w", newline='')
                    }
                    # Write headers
                    csv.writer(self.csv_files['gps']).writerow(['Timestamp', 'Latitude', 'Longitude', 'Altitude', 'Track', 'Ground_Speed'])
                    csv.writer(self.csv_files['attitude']).writerow(['Timestamp', 'True_Heading', 'Pitch', 'Roll'])
                    csv.writer(self.csv_files['traffic']).writerow(['Timestamp', 'ICAO', 'Latitude', 'Longitude', 'Altitude_ft', 'VS_ft_min', 'Airborne', 'Heading', 'Velocity_kts', 'Callsign', 'Departure_Airport', 'Arrival_Airport', 'Airplane_Model', 'Airplane_Livery'])
            except socket.timeout:
                # This is expected, just continue the loop
                pass
            except Exception as e:
                print(f"Error receiving data: {e}")
    
    def _receive_traffic(self) -> None:
        """Continuously receive multiplayer traffic data from the server via UDP (legacy)."""
        #print("Started multiplayer traffic listener (UDP)")
        while self.running:
            try:
                data, addr = self.traffic_socket.recvfrom(1024)
                self.last_receive_time = time.time()
                message = data.decode('utf-8')
                
                # DEBUG: Show traffic messages
                #print(f"DEBUG TRAFFIC: Received from {addr}: {repr(message[:100])}")
                
                if message.startswith('XTRAFFIC'):
                    traffic_data = self._parse_traffic_data(message)
                    if traffic_data:
                        # Store with current timestamp
                        self.traffic_data[traffic_data.icao_address] = (traffic_data, time.time())
                        #print(f"DEBUG: Parsed traffic: {traffic_data.callsign} at {traffic_data.latitude},{traffic_data.longitude}")
                    else:
                        print(f"DEBUG: Failed to parse XTRAFFIC message!")
                        
            except socket.timeout:
                # This is expected, just continue the loop
                pass
            except Exception as e:
                if self.running:
                    print(f"Error receiving traffic data: {e}")
    
    def _poll_traffic_http(self) -> None:
        """Poll HTTP API for nearby traffic."""
        #print("Started HTTP API traffic polling")
        poll_count = 0
        while self.running:
            # Check if this thread should stop (another thread may have been started)
            if self.traffic_thread_stop_event and self.traffic_thread_stop_event.is_set():
                #print("Traffic thread stop event set, exiting")
                break
            
            try:
                # Check if HTTP API is still enabled (might have been disabled)
                if not self.use_http_api:
                    print("HTTP API disabled, exiting polling thread")
                    break
                
                # Determine position to use for query: GPS data takes priority, fallback to manual observation position, then default
                query_lat = None
                query_lon = None
                using_manual_pos = False
                using_default_pos = False
                
                # Get GPS from SimulatorDataSource (works for both UDP and FSWidget)
                simulator_gps = self.simulator_data_source.get_latest_gps()
                #print(f"DEBUG HTTP: Simulator GPS: {simulator_gps}")
                if simulator_gps:
                    # Use GPS data if available (normal operation)
                    query_lat = simulator_gps.latitude
                    query_lon = simulator_gps.longitude
                elif self.manual_obs_lat is not None and self.manual_obs_lon is not None:
                    # Use manual observation position if GPS is not available (observer mode)
                    query_lat = self.manual_obs_lat
                    query_lon = self.manual_obs_lon
                    using_manual_pos = True
                else:
                    # No position available - use default position (0,0) to allow traffic polling
                    # This allows traffic to be displayed even when GPS data is not yet available
                    # (e.g., when using FSWidget on mobile, or before simulator connects)
                    query_lat = 0.0
                    query_lon = 0.0
                    using_default_pos = True
                    #if poll_count == 0:
                    #    print("DEBUG HTTP: No GPS data or manual position, using default position (0,0) for traffic query")
                
                # Check if HTTP session is set up
                if not self.http_session:
                    #print("ERROR: HTTP session not initialized!")
                    time.sleep(self.poll_interval)
                    continue
                
                # Build API URL
                api_url = f"{self.api_server_url}/api/traffic"
                params = {
                    'lat': query_lat,
                    'lon': query_lon,
                    'radius_miles': self.traffic_radius
                }
                
                poll_count += 1
                
                # DEBUG: Log traffic request
                pos_source = "GPS" if simulator_gps else ("Manual" if using_manual_pos else "Default")
                terminal_debug_print("rewinger", f"[DEBUG REWINGER] Traffic request #{poll_count}: GET {api_url}")
                terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Position: lat={query_lat:.6f}, lon={query_lon:.6f}, radius={self.traffic_radius} miles (source: {pos_source})")
                if using_manual_pos:
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Observer Mode: Using manual position for traffic query")
                if using_default_pos:
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER]   WARNING: Using default position (0,0) - traffic may not be accurate!", always=True)
                
                # Make HTTP GET request
                response = self.http_session.get(api_url, params=params, timeout=5.0)
                response.raise_for_status()
                
                data = response.json()
                traffic_list = data.get('traffic', [])
                
                # DEBUG: Log traffic response
                terminal_debug_print("rewinger", f"[DEBUG REWINGER] Traffic response: status={response.status_code}, received {len(traffic_list)} aircraft")
                if len(traffic_list) == 0:
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER]   WARNING: No traffic aircraft received!", always=True)
                if 'count' in data:
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Server reported count: {data['count']}")
                if 'query' in data:
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Query info: {data['query']}")
                
                # DEBUG: Show raw API response
                #print(f"\n{'='*80}")
                #print(f"DEBUG TRAFFIC API: Received {len(traffic_list)} traffic aircraft from API")
                #print(f"DEBUG TRAFFIC API: Query params: lat={query_lat:.4f}, lon={query_lon:.4f}, radius={self.traffic_radius} miles")
                #print(f"DEBUG TRAFFIC API: Own ICAO (calculated): {self.own_icao}")
                #print(f"DEBUG TRAFFIC API: Raw JSON response keys: {list(data.keys())}")
                #if 'query' in data:
                #    print(f"DEBUG TRAFFIC API: Query info: {data['query']}")
                #if 'count' in data:
                #    print(f"DEBUG TRAFFIC API: Count: {data['count']}")
                
                current_time = time.time()
                new_traffic = {}
                for idx, traffic_dict in enumerate(traffic_list):
                    # Convert dict to AirTrafficData (API v2.2 includes squawk_code and flightplan_route)
                    traffic = AirTrafficData(
                        icao_address=traffic_dict['icao_address'],
                        latitude=traffic_dict['latitude'],
                        longitude=traffic_dict['longitude'],
                        altitude_ft=traffic_dict['altitude_ft'],
                        vertical_speed_ft_min=traffic_dict['vertical_speed_ft_min'],
                        airborne_flag=traffic_dict['airborne_flag'],
                        heading_true=traffic_dict['heading_true'],
                        velocity_knots=traffic_dict['velocity_knots'],
                        callsign=traffic_dict['callsign'],
                        squawk_code=traffic_dict.get('squawk_code', '0000'),  # API v2.2 - default to "0000"
                        departure_airport=traffic_dict.get('departure_airport', 'N/C'),
                        arrival_airport=traffic_dict.get('arrival_airport', 'N/C'),
                        airplane_model=traffic_dict.get('airplane_model', 'N/C'),
                        airplane_livery=traffic_dict.get('airplane_livery', 'N/C'),
                        flightplan_route=traffic_dict.get('flightplan_route', 'NO DATA')  # API v2.2 - SimBrief route
                    )
                    
                    # DEBUG: Log each traffic aircraft
                    is_tower = is_control_tower(traffic)
                    tower_marker = " [TOWER]" if is_tower else ""
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Aircraft [{idx+1}]: ICAO={traffic.icao_address}, Callsign={traffic.callsign}{tower_marker}")
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER]     Position: lat={traffic.latitude:.6f}, lon={traffic.longitude:.6f}, alt={traffic.altitude_ft:.0f}ft")
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER]     Model={traffic.airplane_model}, Livery={traffic.airplane_livery}")
                    
                    #print(f"DEBUG TRAFFIC [{idx+1}]: Parsed AirTrafficData:")
                    #print(f"  ICAO: {traffic.icao_address}")
                    #print(f"  Callsign: '{traffic.callsign}'")
                    #print(f"  Position: ({traffic.latitude:.6f}, {traffic.longitude:.6f})")
                    #print(f"  Altitude: {traffic.altitude_ft:.0f} ft")
                    #print(f"  Departure: {traffic.departure_airport}")
                    #print(f"  Arrival: {traffic.arrival_airport}")
                    #print(f"  Model: {traffic.airplane_model}")
                    #print(f"  Livery: {traffic.airplane_livery}")
                    
                    new_traffic[traffic.icao_address] = (traffic, current_time)
                    
                    # If this is our own traffic (same ICAO), extract callsign from server
                    is_own = bool(self.own_icao and traffic.icao_address == self.own_icao)
                    #print(f"DEBUG TRAFFIC [{idx+1}]: Is own aircraft? {is_own} (traffic ICAO: '{traffic.icao_address}', own ICAO: '{self.own_icao}')")
                    if is_own:
                        old_callsign = self.callsign
                        self.callsign = traffic.callsign
                        #print(f"DEBUG TRAFFIC [{idx+1}]: *** IDENTIFIED OWN TRAFFIC ***")
                        #print(f"DEBUG TRAFFIC [{idx+1}]: Callsign updated: '{old_callsign}' -> '{self.callsign}'")
                        #print(f"DEBUG TRAFFIC [{idx+1}]: Full flight plan data:")
                        #print(f"    Departure: {traffic.departure_airport}")
                        #print(f"    Arrival: {traffic.arrival_airport}")
                        #print(f"    Model: {traffic.airplane_model}")
                        #print(f"    Livery: {traffic.airplane_livery}")
                 
                #print(f"{'='*80}\n")
                
                # Update traffic_data (replace old data)
                self.traffic_data = new_traffic
                self.last_receive_time = current_time
                
                # DEBUG: Summary of traffic update
                terminal_debug_print("rewinger", f"[DEBUG REWINGER] Traffic data updated: {len(new_traffic)} aircraft in memory")
                if len(new_traffic) > 0:
                    tower_count = sum(1 for t in new_traffic.values() if is_control_tower(t[0]))
                    if tower_count > 0:
                        terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Found {tower_count} control tower(s) in traffic")
                
            except requests.exceptions.RequestException as e:
                if self.running:
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER] ERROR polling HTTP API for traffic: {e}", always=True)
                    if hasattr(e, 'response') and e.response is not None:
                        terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Response status: {e.response.status_code}", always=True)
                        terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Response text: {e.response.text[:200]}", always=True)
                    else:
                        terminal_debug_print("rewinger", f"[DEBUG REWINGER]   No response object available", always=True)
            except Exception as e:
                if self.running:
                    terminal_debug_print("rewinger", f"[DEBUG REWINGER] ERROR processing HTTP traffic data: {e}", always=True)
                    import traceback
                    traceback.print_exc()
            
            # Wait before next poll (use configured interval)
            # Use wait() instead of sleep() so we can be interrupted by stop event
            if self.traffic_thread_stop_event:
                # Wait for poll_interval, but interrupt if stop event is set
                if self.traffic_thread_stop_event.wait(timeout=self.poll_interval):
                    # Stop event was set, exit the loop
                    break
            else:
                time.sleep(self.poll_interval)
        
        #print("HTTP API traffic polling thread exited")
    
    @staticmethod
    def _parse_gps_data(message: str) -> Optional[GPSData]:
        """Parse GPS data from the received message."""
        # Match XGPS followed by optional simulator name and data
        pattern = r'XGPS(?:[^,]+)?,([-\d.]+),([-\d.]+),([-\d.]+),([-\d.]+),([-\d.]+)'
        match = re.match(pattern, message)
        if match:
            latitude, longitude, altitude, track, ground_speed = map(float, match.groups())
            
            # Check for the specific "menu state" condition
            if (latitude == 0.0 and longitude == 0.0 and 
                altitude == 0.0 and track == 90.0 and ground_speed == 0.0):
                # This is the menu state - return None instead
                return None
                
            # Otherwise return the valid GPS data
            return GPSData(*map(float, match.groups()))
        
        return None
    @staticmethod
    def _parse_attitude_data(message: str) -> Optional[AttitudeData]:
        """Parse attitude data from the received message."""
        pattern = r'XATT(?:[^,]+)?,([-\d.]+),([-\d.]+),([-\d.]+)'
        match = re.match(pattern, message)
        if match:
            return AttitudeData(*map(float, match.groups()))
        return None
    @staticmethod
    def _parse_aircraft_data(message: str) -> Optional[AircraftData]:
        """Parse Aircraft data from the received message."""
        pattern = r'^XAIRCRAFT(?:[^,]+)?,([A-Za-z0-9\-_]+),([A-Za-z0-9\-_]+),([A-Za-z0-9\-_]+),([A-Za-z0-9\-_]+),([A-Za-z0-9\-_]+),([A-Za-z0-9\-_]+)'
        match = re.match(pattern, message)
        if match:
            return AircraftData(*map(str, match.groups()))
        return None
    @staticmethod
    def _parse_traffic_data(message: str) -> Optional[AirTrafficData]:
        """Parse traffic data from the received message (legacy UDP format - no flight plan fields or squawk_code)."""
        pattern = r'^XTRAFFIC(?:[^,]+)?,([A-Za-z0-9\-_]+),([-\d.]+),([-\d.]+),([-\d.]+),([-\d.]+),([01]),'\
                r'([-\d.]+),([-\d.]+),([A-Za-z0-9\-_\s]+)'
        match = re.match(pattern, message.strip())  # Strip whitespace before matching
        if match:
            groups = match.groups()
            return AirTrafficData(
                icao_address=str(groups[0]),
                latitude=float(groups[1]),
                longitude=float(groups[2]),
                altitude_ft=float(groups[3]),
                vertical_speed_ft_min=float(groups[4]),
                airborne_flag=int(groups[5]),
                heading_true=float(groups[6]),
                velocity_knots=float(groups[7]),
                callsign=str(groups[8]),
                squawk_code='0000',  # UDP format doesn't include squawk_code, default to "0000" (API v2.2)
                departure_airport='N/B',  # UDP format doesn't include flight plan fields
                arrival_airport='N/B',
                airplane_model='N/B',
                airplane_livery='N/B',
                flightplan_route='NO DATA'  # UDP format doesn't include SimBrief route
            )
        return None

    def set_csv_logging(self, enabled: bool) -> None:
        """Enable or disable CSV logging."""
        # If we're turning off logging, close any open files
        if self.log_to_csv and not enabled:
            for file in self.csv_files.values():
                file.close()
            self.csv_files = {}
            
        self.log_to_csv = enabled
        self.armed_for_recording = False
        
        # If we're turning on logging, initialize new CSV files
        if enabled:
            # Ensure output_recorder directory exists
            os.makedirs("output_recorder", exist_ok=True)
            timestamp = time.strftime("%Y%m%d-%H%M%S")
            self.csv_files = {
                'gps': open(f"output_recorder/output_GPS_DATA_{timestamp}.csv", "w", newline=''),
                'attitude': open(f"output_recorder/output_ATTITUDE_DATA_{timestamp}.csv", "w", newline=''),
                'traffic': open(f"output_recorder/output_TRAFFIC_DATA_{timestamp}.csv", "w", newline='')
            }
            # Write headers
            csv.writer(self.csv_files['gps']).writerow(['Timestamp', 'Latitude', 'Longitude', 'Altitude', 'Track', 'Ground_Speed'])
            csv.writer(self.csv_files['attitude']).writerow(['Timestamp', 'True_Heading', 'Pitch', 'Roll'])
            csv.writer(self.csv_files['traffic']).writerow(['Timestamp', 'ICAO', 'Latitude', 'Longitude', 'Altitude_ft', 'VS_ft_min', 'Airborne', 'Heading', 'Velocity_kts', 'Callsign', 'Departure_Airport', 'Arrival_Airport', 'Airplane_Model', 'Airplane_Livery'])
        
        status = "enabled" if enabled else "disabled"
        #print(f"CSV logging {status}")
        
    def arm_recording(self) -> None:
        """Arm the recording system to start when data is received."""
        self.armed_for_recording = True
        self.log_to_csv = False
        #print("Recording armed and waiting for data")

    def get_latest_data(self) -> Dict[str, Any]:
        """Return the latest received GPS and attitude data."""
        # Clean outdated traffic data (older than 30 seconds)
        current_time = time.time()
        traffic_timeout = 30.0  # seconds
        self.traffic_data = {
            icao: (data, timestamp) 
            for icao, (data, timestamp) in self.traffic_data.items() 
            if current_time - timestamp < traffic_timeout
        }
        
        # Write to CSV if logging is enabled
        if self.log_to_csv and self.csv_files:
            current_time = time.time()
            
            # Write GPS data
            if self.latest_gps_data and 'gps' in self.csv_files:
                writer = csv.writer(self.csv_files['gps'])
                writer.writerow([
                    current_time,
                    self.latest_gps_data.latitude,
                    self.latest_gps_data.longitude,
                    self.latest_gps_data.altitude,
                    self.latest_gps_data.track,
                    self.latest_gps_data.ground_speed
                ])
                self.csv_files['gps'].flush()  # Ensure data is written immediately
            
            # Write attitude data
            if self.latest_attitude_data and 'attitude' in self.csv_files:
                writer = csv.writer(self.csv_files['attitude'])
                writer.writerow([
                    current_time,
                    self.latest_attitude_data.true_heading,
                    self.latest_attitude_data.pitch,
                    self.latest_attitude_data.roll
                ])
                self.csv_files['attitude'].flush()
            
            # Write traffic data
            if 'traffic' in self.csv_files:
                writer = csv.writer(self.csv_files['traffic'])
                for icao, (traffic_data, traffic_timestamp) in self.traffic_data.items():
                    writer.writerow([
                        traffic_timestamp,
                        traffic_data.icao_address,
                        traffic_data.latitude,
                        traffic_data.longitude,
                        traffic_data.altitude_ft,
                        traffic_data.vertical_speed_ft_min,
                        traffic_data.airborne_flag,
                        traffic_data.heading_true,
                        traffic_data.velocity_knots,
                        traffic_data.callsign,
                        traffic_data.departure_airport,
                        traffic_data.arrival_airport,
                        traffic_data.airplane_model,
                        traffic_data.airplane_livery
                    ])
                self.csv_files['traffic'].flush()
        
        return {
            'gps': self.latest_gps_data,
            'attitude': self.latest_attitude_data,
            'aircraft': self.latest_aircraft_data,
            'traffic': {icao: data for icao, (data, _) in self.traffic_data.items()},
            'connected': (time.time() - self.last_receive_time) < RECEIVE_TIMEOUT
        }

    def stop(self) -> None:
        """Stop receiving data."""
        self.running = False
        
        # Signal threads to stop
        if self.traffic_thread_stop_event:
            self.traffic_thread_stop_event.set()
        # NOTE: No position thread to stop - Rewinger doesn't send position updates
        
        # Stop simulator data source only if we own it
        if self._owns_simulator_data_source:
            self.simulator_data_source.stop()
        
        # Close sockets
        if self.socket:
            try:
                self.socket.close()
            except:
                pass
            self.socket = None
        
        if self.traffic_socket:
            try:
                self.traffic_socket.close()
            except:
                pass
            self.traffic_socket = None
        
        # Wait for threads to finish
        if self.receive_thread:
            self.receive_thread.join(timeout=2)
        if self.traffic_thread:
            self.traffic_thread.join(timeout=2)
        # NOTE: No position thread - Rewinger doesn't send position updates
        
        # Stop simulator data source only if we own it (duplicate check, but safe)
        if self._owns_simulator_data_source and self.simulator_data_source:
            self.simulator_data_source.stop()
        
        if self.socket:
            self.socket.close()
        if self.traffic_socket:
            self.traffic_socket.close()
        
        if self.http_session:
            self.http_session.close()
            self.http_session = None
        
        # Close any open CSV files
        if self.csv_files:
            for file in self.csv_files.values():
                file.close()

class AircraftTrackerApp:
    """
    Main application class for the Aircraft Tracker.
    Handles the GUI and updates the aircraft position on the map.
    Can run standalone or embedded in a parent (e.g. tab).
    """
    def __init__(self, container, shared_data_source=None, embedded_in_unified=False):
        """
        Args:
            container: tk.Tk (standalone) or ttk.Frame (embedded). UI is built inside this.
            shared_data_source: Optional SimulatorDataSource to share (unified app). Caller owns lifecycle.
            embedded_in_unified: If True, Map tab is in unified app - sim/server from Connect tab, hide Multiplayer UI.
        """
        self.container = container
        self.master = container.winfo_toplevel()
        self.shared_data_source = shared_data_source
        self.embedded_in_unified = embedded_in_unified
        
        # Configure DPI scaling for Windows
        self.scale_factor = configure_tk_dpi(self.master)
        
        # When standalone (container is root), set geometry and title
        if isinstance(container, tk.Tk):
            self.master.title(f"{APP_NAME} / {APP_VERSION}")
            base_width, base_height = map(int, WINDOW_SIZE.split('x'))
            safe_geometry = get_safe_window_geometry(self.master, base_width, base_height, self.scale_factor, center=True)
            self.master.geometry(safe_geometry)
        
        # Initialize ttk.Style for themed widgets with custom colors
        # ttk.Style allows us to create custom styles that work with system themes
        self.style = ttk.Style()
        self.setup_custom_styles()
        
        # Initialize flight plan related attributes
        self.flight_plan_waypoints = []
        self.flight_plan_path = None
        self.current_kml_file = None
        
        # Initialize target management
        self.aircraft_targets = {}  # Dict[icao: str, Dict[str, Any]]
        self.selected_aircraft_icao = None  # Current selected aircraft ICAO
        
        # Traffic list window (will be created on demand)
        self.traffic_list_window = None
        
        self.setup_ui()
        
        # Bind keyboard shortcuts
        self.setup_keyboard_shortcuts()
        
        # Initialize with default UDP (will be reconfigured if FSWidget is selected)
        self.data_source_type = DataSourceType.UDP
        self.fswidget_ip = "localhost"
        self.auto_detect = False
        if shared_data_source is not None:
            self.udp_receiver = UDPReceiver(simulator_data_source=shared_data_source)
        else:
            self.udp_receiver = UDPReceiver(use_fswidget=False, fswidget_ip="localhost", auto_detect=False)
        self.udp_receiver.start_receiving()
        self.setup_aircraft_marker()
        # Dictionary to keep track of traffic markers
        self.traffic_markers = {}
        # Setup a different icon for traffic
        self.traffic_image = Image.open("traffic_icon.png").resize((24, 24))
        # Setup control tower icon (with fallback to traffic icon if not found)
        try:
            tower_icon_path = os.path.join(os.path.dirname(__file__), "control_tower.png")
            if os.path.exists(tower_icon_path):
                self.control_tower_image = Image.open(tower_icon_path).resize((24, 24))
            else:
                # Fallback: try current directory
                self.control_tower_image = Image.open("control_tower.png").resize((24, 24))
        except (FileNotFoundError, IOError):
            # Fallback to traffic icon if control tower icon not found
            self.control_tower_image = self.traffic_image.copy()
        self.update_aircraft_position()
        # Variables to track map center mode
        self.follow_aircraft = True
        self.map_center = None
        if self.embedded_in_unified:
            self._apply_embedded_traffic_radius()
    
    def setup_custom_styles(self):
        """
        Set up custom ttk.Style configurations for colored status labels.
        
        ttk.Style works differently from tk widgets:
        - tk.Label supports fg='red' directly
        - ttk.Label requires named styles
        
        We use independent style names (not sub-styles) to avoid base style issues.
        Styles can be changed dynamically using widget.configure(style='StyleName').
        """
        # Create independent style names (each inherits from TLabel)
        # Using dot notation for readability, but each is an independent style
        self.style.configure("StatusLabel.Green", foreground="green")
        self.style.configure("StatusLabel.Red", foreground="red")
        self.style.configure("StatusLabel.Orange", foreground="orange")
        self.style.configure("StatusLabel.Gray", foreground="gray")
        self.style.configure("StatusLabel.Black", foreground="black")

    def setup_ui(self):
        """Set up the main user interface components."""
        # Create bottom stripe first (so it's at the bottom)
        self.setup_bottom_stripe()
        
        # Create main content area (map + right panel)
        self.content_frame = ttk.Frame(self.container)
        self.content_frame.pack(fill="both", expand=True)

        # Create and configure the map widget (left side, will expand)
        self.map_widget = TkinterMapView(self.content_frame, width=MAP_SIZE[0], height=MAP_SIZE[1], corner_radius=0)
        self.map_widget.pack(side="left", fill="both", expand=True)

        # Make right panel resizable (separator between map and panel)
        # Must create separator BEFORE the panel so it appears between map and panel
        self.setup_resizable_panel()
        
        # Create the right panel for aircraft information (will be populated later)
        self.aircraft_info_frame = ttk.Frame(self.content_frame, width=DEFAULT_RIGHT_PANEL_WIDTH, relief=tk.FLAT)
        self.aircraft_info_frame.pack(side="right", fill="y")
        self.aircraft_info_frame.pack_propagate(False)  # Maintain width

        # Set up the window close protocol (only when standalone)
        if isinstance(self.container, tk.Tk):
            self.master.protocol("WM_DELETE_WINDOW", self.close_application)
        
        # Initialize right panel with "no selection" message
        self.setup_aircraft_info_panel()

    def setup_bottom_stripe(self):
        """Set up the bottom stripe with connection and settings controls."""
        stripe_height = BOTTOM_STRIPE_HEIGHT_EMBEDDED if self.embedded_in_unified else BOTTOM_STRIPE_HEIGHT
        self.bottom_stripe = ttk.Frame(self.container, height=stripe_height, relief=tk.FLAT)
        self.bottom_stripe.pack(side="bottom", fill="x")
        self.bottom_stripe.pack_propagate(False)  # Maintain height
        
        # Left section: Connection status and quick controls
        left_section = ttk.Frame(self.bottom_stripe)
        left_section.pack(side="left", padx=10, pady=2, fill="y")
        
        if self.embedded_in_unified:
            # Single horizontal row: status | map | follow | filter | callsign | api_status
            row = ttk.Frame(left_section)
            row.pack(fill="x")
            self.connection_status = tk.Label(row, text="Disconnected", fg="red")
            self.connection_status.pack(side=tk.LEFT, padx=(0, 8))
            ttk.Label(row, text="Radius (mi):", font=("Arial", 7)).pack(side=tk.LEFT, padx=(0, 2))
            self.traffic_radius_entry = ttk.Entry(row, width=5)
            self.traffic_radius_entry.insert(0, str(int(DEFAULT_TRAFFIC_RADIUS)))
            self.traffic_radius_entry.pack(side=tk.LEFT, padx=(0, 8))
            self.traffic_radius_entry.bind("<FocusOut>", self._apply_embedded_traffic_radius)
            self.traffic_radius_entry.bind("<Return>", self._on_embedded_radius_return)
            # Map dropdown
            map_options = self.get_map_options()
            map_names = [name for name, _ in map_options]
            self.map_selection_var = tk.StringVar(value=map_names[0])
            self.map_selection_dropdown = ttk.OptionMenu(row, self.map_selection_var, *map_names,
                                                        command=self.on_map_selection_change)
            self.map_selection_dropdown.config(width=12)
            self.map_selection_dropdown.pack(side=tk.LEFT, padx=(0, 8))
            # Follow and Filter toggles
            self.follow_var = tk.BooleanVar(value=True)
            self.follow_checkbox = tk.Checkbutton(row, text="Follow", variable=self.follow_var,
                                                  command=self.toggle_follow_mode, font=("Arial", 7))
            self.follow_checkbox.pack(side=tk.LEFT, padx=2)
            self.proximity_filter_var = tk.BooleanVar(value=True)
            self.proximity_filter_checkbox = tk.Checkbutton(row, text="Filter", variable=self.proximity_filter_var,
                                                            font=("Arial", 7))
            self.proximity_filter_checkbox.pack(side=tk.LEFT, padx=2)
            # Callsign and status
            ttk.Label(row, text="Callsign:", font=("Arial", 7)).pack(side=tk.LEFT, padx=(8, 2))
            self.callsign_entry = ttk.Entry(row, width=8, state="readonly")
            self.callsign_entry.pack(side=tk.LEFT, padx=2)
            self.api_status_label = tk.Label(row, text="Use Connect tab", fg="gray", font=("Arial", 7))
            self.api_status_label.pack(side=tk.LEFT, padx=(4, 0))
            # Hidden entry for set_http_api_from_client / connect_http_api (poll interval)
            self.poll_interval_entry = ttk.Entry(left_section, width=1)
            self.poll_interval_entry.insert(0, str(HTTP_POLL_INTERVAL))
            self.poll_interval_entry.pack_forget()
            right_section = ttk.Frame(self.bottom_stripe)
            right_section.pack(side="right", padx=10, pady=2, fill="y")
            self.traffic_list_button = ttk.Button(right_section, text="Traffic List",
                                                  command=self.open_traffic_list_window, width=12)
            self.traffic_list_button.pack(pady=0)
        else:
            # Stacked layout for standalone
            self.connection_status = tk.Label(left_section, text="Disconnected", fg="red", font=("Arial", 10, "bold"))
            self.connection_status.pack(pady=2)
            self.setup_map_selection_compact(left_section)
            self.setup_map_control_compact(left_section)
            # Full standalone UI
            middle_section = ttk.Frame(self.bottom_stripe)
            middle_section.pack(side="left", padx=10, pady=5, fill="both", expand=True)
            self.setup_http_api_controls_compact(middle_section)
            right_section = ttk.Frame(self.bottom_stripe)
            right_section.pack(side="right", padx=10, pady=5, fill="y")
            if ENABLE_FLIGHT_PLAN_UI:
                self.setup_flightplan_controls_compact(right_section)
            if ENABLE_RECORDING_UI:
                self.setup_recording_controls_compact(right_section)
            self.traffic_list_button = ttk.Button(right_section, text="Traffic List",
                                                  command=self.open_traffic_list_window, width=15)
            self.traffic_list_button.pack(pady=2)
            self.close_button = ttk.Button(right_section, text="Close", command=self.close_application, width=15)
            self.close_button.pack(pady=2)

    def _apply_embedded_traffic_radius(self, event=None) -> None:
        """Copy unified Map stripe radius entry into UDPReceiver (used on each HTTP poll)."""
        if not self.embedded_in_unified:
            return
        if not hasattr(self, "traffic_radius_entry") or not hasattr(self, "udp_receiver"):
            return
        if self.udp_receiver is None:
            return
        try:
            raw = self.traffic_radius_entry.get().strip()
            r = float(raw) if raw else DEFAULT_TRAFFIC_RADIUS
            if r <= 0:
                r = DEFAULT_TRAFFIC_RADIUS
            self.udp_receiver.traffic_radius = r
        except (ValueError, tk.TclError):
            cur = getattr(self.udp_receiver, "traffic_radius", DEFAULT_TRAFFIC_RADIUS)
            disp = str(int(cur)) if float(cur) == int(float(cur)) else str(cur)
            self.traffic_radius_entry.delete(0, tk.END)
            self.traffic_radius_entry.insert(0, disp)

    def _on_embedded_radius_return(self, event):
        self._apply_embedded_traffic_radius()
        return "break"

    def setup_resizable_panel(self):
        """Make the right panel resizable."""
        # Create a separator that can be dragged (between map and aircraft panel)
        # The separator should appear between the map (left) and the aircraft panel (right)
        # Pack order: map (left), separator (right), panel (right)
        # This makes separator appear between them
        self.panel_separator = ttk.Frame(self.content_frame, width=4, cursor="sb_h_double_arrow")
        # Pack separator on the right side, it will appear to the left of the aircraft panel
        # (since panel was packed last, it's rightmost; separator is to its left)
        self.panel_separator.pack(side="right", fill="y")
        
        # Bind mouse events for resizing
        self.panel_separator.bind("<B1-Motion>", self.resize_panel)
        self.panel_separator.bind("<Button-1>", lambda e: setattr(self, '_resizing', True))
        self.panel_separator.bind("<ButtonRelease-1>", lambda e: setattr(self, '_resizing', False))
        self._resizing = False

    def resize_panel(self, event):
        """Handle panel resizing."""
        if not self._resizing:
            return
        
        # Calculate new width based on mouse position
        mouse_x = event.x_root
        window_x = self.master.winfo_x()
        content_x = self.content_frame.winfo_x()
        relative_x = mouse_x - window_x - content_x
        
        # Get current content frame width
        content_width = self.content_frame.winfo_width()
        
        # Calculate new panel width (content_width - relative_x - separator width)
        new_width = content_width - relative_x - 4
        
        # Constrain to min/max
        new_width = max(MIN_RIGHT_PANEL_WIDTH, min(MAX_RIGHT_PANEL_WIDTH, new_width))
        
        # Update panel width
        self.aircraft_info_frame.config(width=new_width)
    
    def setup_keyboard_shortcuts(self):
        """Set up keyboard shortcuts for the application."""
        # Bind Escape key to deselect aircraft
        self.master.bind("<Escape>", lambda e: self.clear_aircraft_selection())
        
        # Bind Shift+Arrow keys for cycling through aircraft
        self.master.bind(f"<{KEYBOARD_CYCLE_MODIFIER}-{KEYBOARD_CYCLE_FORWARD}>", self.cycle_aircraft_forward)
        self.master.bind(f"<{KEYBOARD_CYCLE_MODIFIER}-{KEYBOARD_CYCLE_BACKWARD}>", self.cycle_aircraft_backward)
        # Also bind to Shift+Left/Right for arrow keys
        self.master.bind("<Shift-Left>", self.cycle_aircraft_backward)
        self.master.bind("<Shift-Right>", self.cycle_aircraft_forward)

    def setup_aircraft_info_panel(self):
        """Set up the right panel for displaying selected aircraft information."""
        # Clear any existing widgets
        for widget in self.aircraft_info_frame.winfo_children():
            widget.destroy()
        
        # Initial "no selection" message
        no_selection_label = tk.Label(
            self.aircraft_info_frame,
            text="NO DATA\n\nSelect an aircraft\nby clicking on\na marker",
            font=("Arial", 14),
            fg="gray",
            justify=tk.CENTER
        )
        no_selection_label.pack(expand=True, fill="both", pady=50)
        
        # Store reference for later updates
        self.aircraft_info_content = None

    def update_aircraft_info_panel(self, traffic_data: AirTrafficData):
        """Update the right panel with selected aircraft information."""
        # Save which target field had focus (if any) before clearing
        focused_target_field = None
        if hasattr(self, 'target_entries') and self.selected_aircraft_icao in self.target_entries:
            target_entries = self.target_entries.get(self.selected_aircraft_icao, {})
            focused_widget = self.master.focus_get()
            for field_name, entry_widget in target_entries.items():
                try:
                    if entry_widget == focused_widget:
                        focused_target_field = field_name
                        break
                except:
                    pass
        
        # Clear existing content
        for widget in self.aircraft_info_frame.winfo_children():
            widget.destroy()
        
        # Create scrollable frame
        canvas = tk.Canvas(self.aircraft_info_frame)
        scrollbar = tk.Scrollbar(self.aircraft_info_frame, orient="vertical", command=canvas.yview)
        scrollable_frame = ttk.Frame(canvas)
        
        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )
        
        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        
        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        # Store content frame reference
        self.aircraft_info_content = scrollable_frame
        
        # Store widget references for updating without recreating
        if not hasattr(self, 'aircraft_info_widgets'):
            self.aircraft_info_widgets = {}
        
        # 1. Aircraft Identification Section
        ident_frame = ttk.LabelFrame(scrollable_frame, text="Aircraft Identification")
        ident_frame.pack(fill="x", padx=5, pady=5)
        
        callsign_label = ttk.Label(ident_frame, text=f"Callsign: {traffic_data.callsign or 'N/A'}")
        callsign_label.pack(anchor=tk.W, padx=5, pady=2)
        self.aircraft_info_widgets['callsign_label'] = callsign_label
        
        icao_label = ttk.Label(ident_frame, text=f"ICAO: {traffic_data.icao_address}")
        icao_label.pack(anchor=tk.W, padx=5, pady=2)
        self.aircraft_info_widgets['icao_label'] = icao_label
        
        squawk_label = ttk.Label(ident_frame, text=f"Squawk: {traffic_data.squawk_code or '0000'}")
        squawk_label.pack(anchor=tk.W, padx=5, pady=2)
        self.aircraft_info_widgets['squawk_label'] = squawk_label
        
        # 2. Current Position Section
        pos_frame = ttk.LabelFrame(scrollable_frame, text="Current Position")
        pos_frame.pack(fill="x", padx=5, pady=5)
        
        lat_label = ttk.Label(pos_frame, text=f"Latitude: {traffic_data.latitude:.6f}°")
        lat_label.pack(anchor=tk.W, padx=5, pady=1)
        self.aircraft_info_widgets['lat_label'] = lat_label
        
        lon_label = ttk.Label(pos_frame, text=f"Longitude: {traffic_data.longitude:.6f}°")
        lon_label.pack(anchor=tk.W, padx=5, pady=1)
        self.aircraft_info_widgets['lon_label'] = lon_label
        
        alt_label = ttk.Label(pos_frame, text=f"Altitude: {int(traffic_data.altitude_ft)} ft")
        alt_label.pack(anchor=tk.W, padx=5, pady=1)
        self.aircraft_info_widgets['alt_label'] = alt_label
        
        # 3. Current State Section
        state_frame = ttk.LabelFrame(scrollable_frame, text="Current State")
        state_frame.pack(fill="x", padx=5, pady=5)
        
        heading_label = ttk.Label(state_frame, text=f"Heading: {int(traffic_data.heading_true)}°")
        heading_label.pack(anchor=tk.W, padx=5, pady=1)
        self.aircraft_info_widgets['heading_label'] = heading_label
        
        speed_label = ttk.Label(state_frame, text=f"Ground Speed: {int(traffic_data.velocity_knots)} kts")
        speed_label.pack(anchor=tk.W, padx=5, pady=1)
        self.aircraft_info_widgets['speed_label'] = speed_label
        
        vs_label = ttk.Label(state_frame, text=f"Vertical Speed: {int(traffic_data.vertical_speed_ft_min)} ft/min")
        vs_label.pack(anchor=tk.W, padx=5, pady=1)
        self.aircraft_info_widgets['vs_label'] = vs_label
        
        # 4. Flight Plan Data Section
        fp_frame = ttk.LabelFrame(scrollable_frame, text="Flight Plan")
        fp_frame.pack(fill="x", padx=5, pady=5)
        
        ttk.Label(fp_frame, text=f"Departure: {traffic_data.departure_airport}").pack(anchor=tk.W, padx=5, pady=1)
        ttk.Label(fp_frame, text=f"Arrival: {traffic_data.arrival_airport}").pack(anchor=tk.W, padx=5, pady=1)
        ttk.Label(fp_frame, text=f"Model: {traffic_data.airplane_model}").pack(anchor=tk.W, padx=5, pady=1)
        ttk.Label(fp_frame, text=f"Livery: {traffic_data.airplane_livery}").pack(anchor=tk.W, padx=5, pady=1)
        
        # 5. SimBrief Route Section
        route_frame = ttk.LabelFrame(scrollable_frame, text="SimBrief Route")
        route_frame.pack(fill="both", expand=True, padx=5, pady=5)
        
        route_text = tk.Text(route_frame, height=6, width=30, font=("Courier", 8), wrap=tk.WORD)
        route_text.insert("1.0", traffic_data.flightplan_route if traffic_data.flightplan_route != "NO DATA" else "No route data available")
        route_text.config(state=tk.DISABLED)  # Read-only
        route_text.pack(fill="both", expand=True, padx=5, pady=5)
        
        # 6. Full Traffic Payload Section (formatted table)
        payload_frame = ttk.LabelFrame(scrollable_frame, text="Traffic Payload")
        payload_frame.pack(fill="x", padx=5, pady=5)
        
        # Create a text widget with formatted data (store reference for updates)
        payload_text = tk.Text(payload_frame, height=8, font=("Courier", 7), wrap=tk.NONE)
        payload_text.insert("1.0", self.format_traffic_payload(traffic_data))
        payload_text.config(state=tk.DISABLED)
        payload_text.pack(fill="both", expand=True, padx=5, pady=5)
        self.aircraft_info_widgets['payload_text'] = payload_text
        
        # 7. Target Settings Section
        self.setup_target_settings_section(scrollable_frame, traffic_data.icao_address)
        
        # Update canvas scroll region
        canvas.update_idletasks()
        canvas.configure(scrollregion=canvas.bbox("all"))
        
        # Restore focus to the target field that was previously focused
        if focused_target_field and hasattr(self, 'target_entries') and traffic_data.icao_address in self.target_entries:
            target_entries = self.target_entries[traffic_data.icao_address]
            if focused_target_field in target_entries:
                # Use after_idle to restore focus after all widgets are created
                def restore_focus():
                    try:
                        target_entries[focused_target_field].focus_set()
                        # Also select any text in the field
                        target_entries[focused_target_field].select_range(0, tk.END)
                    except:
                        pass
                self.master.after_idle(restore_focus)

    def update_aircraft_info_values(self, traffic_data: AirTrafficData):
        """Update only the display values in the aircraft info panel without recreating widgets."""
        # Update stored widget references if they exist
        if not hasattr(self, 'aircraft_info_widgets'):
            return
        
        widgets = self.aircraft_info_widgets
        
        # Update identification labels
        if 'callsign_label' in widgets:
            widgets['callsign_label'].config(text=f"Callsign: {traffic_data.callsign or 'N/A'}")
        if 'icao_label' in widgets:
            widgets['icao_label'].config(text=f"ICAO: {traffic_data.icao_address}")
        if 'squawk_label' in widgets:
            widgets['squawk_label'].config(text=f"Squawk: {traffic_data.squawk_code or '0000'}")
        
        # Update position labels
        if 'lat_label' in widgets:
            widgets['lat_label'].config(text=f"Latitude: {traffic_data.latitude:.6f}°")
        if 'lon_label' in widgets:
            widgets['lon_label'].config(text=f"Longitude: {traffic_data.longitude:.6f}°")
        if 'alt_label' in widgets:
            widgets['alt_label'].config(text=f"Altitude: {int(traffic_data.altitude_ft)} ft")
        
        # Update state labels
        if 'heading_label' in widgets:
            widgets['heading_label'].config(text=f"Heading: {int(traffic_data.heading_true)}°")
        if 'speed_label' in widgets:
            widgets['speed_label'].config(text=f"Ground Speed: {int(traffic_data.velocity_knots)} kts")
        if 'vs_label' in widgets:
            widgets['vs_label'].config(text=f"Vertical Speed: {int(traffic_data.vertical_speed_ft_min)} ft/min")
        
        # Update payload text
        if 'payload_text' in widgets:
            payload_text = widgets['payload_text']
            payload_text.config(state=tk.NORMAL)
            payload_text.delete("1.0", tk.END)
            payload_text.insert("1.0", self.format_traffic_payload(traffic_data))
            payload_text.config(state=tk.DISABLED)

    def format_traffic_payload(self, traffic_data: AirTrafficData) -> str:
        """Format traffic data as a readable table."""
        lines = [
            f"ICAO Address:     {traffic_data.icao_address}",
            f"Latitude:        {traffic_data.latitude:.6f}",
            f"Longitude:       {traffic_data.longitude:.6f}",
            f"Altitude (ft):   {traffic_data.altitude_ft:.1f}",
            f"VS (ft/min):     {traffic_data.vertical_speed_ft_min:.1f}",
            f"Airborne:        {traffic_data.airborne_flag}",
            f"Heading:         {traffic_data.heading_true:.1f}°",
            f"Velocity (kts):  {traffic_data.velocity_knots:.1f}",
            f"Callsign:        {traffic_data.callsign or 'N/A'}",
            f"Squawk:          {traffic_data.squawk_code or '0000'}",
            f"Departure:       {traffic_data.departure_airport}",
            f"Arrival:         {traffic_data.arrival_airport}",
            f"Model:           {traffic_data.airplane_model}",
            f"Livery:          {traffic_data.airplane_livery}",
            f"Route:           {traffic_data.flightplan_route[:50] if len(traffic_data.flightplan_route) > 50 else traffic_data.flightplan_route}..."
        ]
        return "\n".join(lines)

    def setup_target_settings_section(self, parent, icao: str):
        """Set up target settings section in aircraft info panel."""
        target_frame = ttk.LabelFrame(parent, text="Target Settings")
        target_frame.pack(fill="x", padx=5, pady=5)
        
        # Get current targets for this aircraft
        targets = self.aircraft_targets.get(icao, {})
        
        # Ground Speed Target
        speed_frame = ttk.Frame(target_frame)
        speed_frame.pack(fill="x", padx=5, pady=2)
        ttk.Label(speed_frame, text="Speed (kts):", width=12, anchor=tk.W).pack(side=tk.LEFT)
        speed_entry = ttk.Entry(speed_frame, width=8)
        speed_entry.pack(side=tk.LEFT, padx=2)
        if "ground_speed_kts" in targets:
            speed_entry.insert(0, str(targets["ground_speed_kts"]))
        speed_entry.bind("<Return>", lambda e: self.set_target(icao, "ground_speed_kts", speed_entry.get()))
        
        # Heading Target
        heading_frame = ttk.Frame(target_frame)
        heading_frame.pack(fill="x", padx=5, pady=2)
        ttk.Label(heading_frame, text="Heading (°):", width=12, anchor=tk.W).pack(side=tk.LEFT)
        heading_entry = ttk.Entry(heading_frame, width=8)
        heading_entry.pack(side=tk.LEFT, padx=2)
        if "heading_deg" in targets:
            heading_entry.insert(0, str(targets["heading_deg"]))
        heading_entry.bind("<Return>", lambda e: self.set_target(icao, "heading_deg", heading_entry.get()))
        
        # Vertical Speed Target
        vs_frame = ttk.Frame(target_frame)
        vs_frame.pack(fill="x", padx=5, pady=2)
        ttk.Label(vs_frame, text="VS (ft/min):", width=12, anchor=tk.W).pack(side=tk.LEFT)
        vs_entry = ttk.Entry(vs_frame, width=8)
        vs_entry.pack(side=tk.LEFT, padx=2)
        if "vertical_speed_ft_min" in targets:
            vs_entry.insert(0, str(targets["vertical_speed_ft_min"]))
        vs_entry.bind("<Return>", lambda e: self.set_target(icao, "vertical_speed_ft_min", vs_entry.get()))
        
        # Altitude Target
        alt_frame = ttk.Frame(target_frame)
        alt_frame.pack(fill="x", padx=5, pady=2)
        ttk.Label(alt_frame, text="Altitude (ft):", width=12, anchor=tk.W).pack(side=tk.LEFT)
        alt_entry = ttk.Entry(alt_frame, width=8)
        alt_entry.pack(side=tk.LEFT, padx=2)
        if "altitude_ft" in targets:
            alt_entry.insert(0, str(targets["altitude_ft"]))
        alt_entry.bind("<Return>", lambda e: self.set_target(icao, "altitude_ft", alt_entry.get()))
        
        # Buttons frame
        buttons_frame = ttk.Frame(target_frame)
        buttons_frame.pack(pady=5, padx=5, fill="x")
        
        # Assign targets button (reads all fields and sets them)
        assign_button = ttk.Button(buttons_frame, text="Assign Targets", 
                                  command=lambda: self.assign_targets_from_fields(icao))
        assign_button.pack(pady=2, padx=2, fill="x")
        
        # Clear targets button
        clear_button = ttk.Button(buttons_frame, text="Clear All Targets", 
                                command=lambda: self.clear_all_targets())
        clear_button.pack(pady=2, padx=2, fill="x")
        
        # Store entry references for updates
        if not hasattr(self, 'target_entries'):
            self.target_entries = {}
        self.target_entries[icao] = {
            "speed": speed_entry,
            "heading": heading_entry,
            "vs": vs_entry,
            "altitude": alt_entry
        }

    def set_target(self, icao: str, target_type: str, value_str: str):
        """Set a target value for an aircraft. Empty values are ignored (target not set)."""
        # Handle empty values - if empty, don't set the target (leave it unset)
        if not value_str or not value_str.strip():
            # Empty value - remove this target if it exists
            if icao in self.aircraft_targets and target_type in self.aircraft_targets[icao]:
                del self.aircraft_targets[icao][target_type]
                # If no targets left for this aircraft, remove the entry
                if not any(k != "set_timestamp" for k in self.aircraft_targets[icao].keys()):
                    del self.aircraft_targets[icao]
                # Update markers
                data = self.udp_receiver.get_latest_data()
                if data['traffic']:
                    self.update_traffic_markers(data['traffic'])
            return
        
        try:
            value = int(value_str.strip())
            
            # Validate ranges
            if target_type == "ground_speed_kts":
                if not (SPEED_MIN <= value <= SPEED_MAX):
                    return
            elif target_type == "heading_deg":
                if not (HEADING_MIN <= value <= HEADING_MAX):
                    return
            elif target_type == "vertical_speed_ft_min":
                if not (VS_MIN <= value <= VS_MAX):
                    return
            elif target_type == "altitude_ft":
                if not (ALT_MIN <= value <= ALT_MAX):
                    return
                if value < 0:
                    return  # Altitude must be positive
            
            # Set target
            if icao not in self.aircraft_targets:
                self.aircraft_targets[icao] = {}
            
            self.aircraft_targets[icao][target_type] = value
            self.aircraft_targets[icao]["set_timestamp"] = time.time()
            
            # Update markers to show new target status
            data = self.udp_receiver.get_latest_data()
            if data['traffic']:
                self.update_traffic_markers(data['traffic'])
            
        except ValueError:
            pass  # Invalid input, ignore

    def assign_targets_from_fields(self, icao: str):
        """Read all target entry fields and assign them to the aircraft.
        Empty fields will clear that target (if it was previously set)."""
        if not hasattr(self, 'target_entries') or icao not in self.target_entries:
            return
        
        target_entries = self.target_entries[icao]
        any_changes = False
        
        # Read each field and set/clear target
        if "speed" in target_entries:
            speed_value = target_entries["speed"].get().strip()
            if speed_value:
                self.set_target(icao, "ground_speed_kts", speed_value)
                any_changes = True
            else:
                # Empty field - clear this target if it exists
                if icao in self.aircraft_targets and "ground_speed_kts" in self.aircraft_targets[icao]:
                    del self.aircraft_targets[icao]["ground_speed_kts"]
                    any_changes = True
        
        if "heading" in target_entries:
            heading_value = target_entries["heading"].get().strip()
            if heading_value:
                self.set_target(icao, "heading_deg", heading_value)
                any_changes = True
            else:
                # Empty field - clear this target if it exists
                if icao in self.aircraft_targets and "heading_deg" in self.aircraft_targets[icao]:
                    del self.aircraft_targets[icao]["heading_deg"]
                    any_changes = True
        
        if "vs" in target_entries:
            vs_value = target_entries["vs"].get().strip()
            if vs_value:
                self.set_target(icao, "vertical_speed_ft_min", vs_value)
                any_changes = True
            else:
                # Empty field - clear this target if it exists
                if icao in self.aircraft_targets and "vertical_speed_ft_min" in self.aircraft_targets[icao]:
                    del self.aircraft_targets[icao]["vertical_speed_ft_min"]
                    any_changes = True
        
        if "altitude" in target_entries:
            alt_value = target_entries["altitude"].get().strip()
            if alt_value:
                self.set_target(icao, "altitude_ft", alt_value)
                any_changes = True
            else:
                # Empty field - clear this target if it exists
                if icao in self.aircraft_targets and "altitude_ft" in self.aircraft_targets[icao]:
                    del self.aircraft_targets[icao]["altitude_ft"]
                    any_changes = True
        
        # Clean up empty target dicts
        if icao in self.aircraft_targets:
            # Remove timestamp-only entries
            if not any(k != "set_timestamp" for k in self.aircraft_targets[icao].keys()):
                del self.aircraft_targets[icao]
        
        # Update markers to show new target status
        if any_changes:
            data = self.udp_receiver.get_latest_data()
            if data['traffic']:
                self.update_traffic_markers(data['traffic'])

    def clear_all_targets(self):
        """Clear all targets for all aircraft."""
        self.aircraft_targets.clear()
        # Update markers
        data = self.udp_receiver.get_latest_data()
        if data['traffic']:
            self.update_traffic_markers(data['traffic'])

    def cycle_aircraft_forward(self, event=None):
        """Cycle to next aircraft in traffic."""
        data = self.udp_receiver.get_latest_data()
        if not data.get('traffic'):
            return
        
        traffic_list = list(data['traffic'].keys())
        if not traffic_list:
            return
        
        if self.selected_aircraft_icao:
            try:
                current_index = traffic_list.index(self.selected_aircraft_icao)
                next_index = (current_index + 1) % len(traffic_list)
            except ValueError:
                next_index = 0
        else:
            next_index = 0
        
        next_icao = traffic_list[next_index]
        self.select_aircraft(next_icao)

    def cycle_aircraft_backward(self, event=None):
        """Cycle to previous aircraft in traffic."""
        data = self.udp_receiver.get_latest_data()
        if not data.get('traffic'):
            return
        
        traffic_list = list(data['traffic'].keys())
        if not traffic_list:
            return
        
        if self.selected_aircraft_icao:
            try:
                current_index = traffic_list.index(self.selected_aircraft_icao)
                next_index = (current_index - 1) % len(traffic_list)
            except ValueError:
                next_index = len(traffic_list) - 1
        else:
            next_index = len(traffic_list) - 1
        
        next_icao = traffic_list[next_index]
        self.select_aircraft(next_icao)

    def open_traffic_list_window(self):
        """Open or update the traffic list window."""
        if self.traffic_list_window is not None:
            # Window exists, just update it
            self.update_traffic_list_window()
            try:
                self.traffic_list_window.lift()
                self.traffic_list_window.focus()
            except:
                # Window was closed, recreate it
                self.traffic_list_window = None
                self.open_traffic_list_window()
            return
        
        # Create new window
        self.traffic_list_window = tk.Toplevel(self.master)
        self.traffic_list_window.title("Traffic List - Rewinger")
        self.traffic_list_window.geometry(TRAFFIC_LIST_WINDOW_SIZE)
        
        # Create table
        self.traffic_list_tree = None
        self.setup_traffic_list_table()
        
        # Update button
        update_button = ttk.Button(self.traffic_list_window, text="Update", command=self.update_traffic_list_window)
        update_button.pack(pady=5)
        
        # Handle window close
        self.traffic_list_window.protocol("WM_DELETE_WINDOW", self.close_traffic_list_window)
        
        # Initial update
        self.update_traffic_list_window()
        
        # Auto-update every 2 seconds
        self.update_traffic_list_periodic()

    def setup_traffic_list_table(self):
        """Set up the traffic list table using Treeview."""
        # Create frame with scrollbars
        frame = ttk.Frame(self.traffic_list_window)
        frame.pack(fill="both", expand=True, padx=5, pady=5)
        
        # Create treeview
        columns = ("Squawk", "Callsign", "Target", "Altitude", "Speed", "Heading")
        self.traffic_list_tree = ttk.Treeview(frame, columns=columns, show="headings", height=15)
        
        # Configure columns
        self.traffic_list_tree.heading("Squawk", text="Squawk")
        self.traffic_list_tree.heading("Callsign", text="Callsign")
        self.traffic_list_tree.heading("Target", text="Target")
        self.traffic_list_tree.heading("Altitude", text="Altitude (ft)")
        self.traffic_list_tree.heading("Speed", text="Speed (kts)")
        self.traffic_list_tree.heading("Heading", text="Heading (°)")
        
        self.traffic_list_tree.column("Squawk", width=60)
        self.traffic_list_tree.column("Callsign", width=100)
        self.traffic_list_tree.column("Target", width=60)
        self.traffic_list_tree.column("Altitude", width=80)
        self.traffic_list_tree.column("Speed", width=80)
        self.traffic_list_tree.column("Heading", width=80)
        
        # Scrollbars
        v_scrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.traffic_list_tree.yview)
        h_scrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.traffic_list_tree.xview)
        self.traffic_list_tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
        
        # Grid layout
        self.traffic_list_tree.grid(row=0, column=0, sticky="nsew")
        v_scrollbar.grid(row=0, column=1, sticky="ns")
        h_scrollbar.grid(row=1, column=0, sticky="ew")
        
        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(0, weight=1)
        
        # Bind double-click to select aircraft
        self.traffic_list_tree.bind("<Double-1>", self.on_traffic_list_double_click)

    def on_traffic_list_double_click(self, event):
        """Handle double-click on traffic list item."""
        selection = self.traffic_list_tree.selection()
        if selection:
            item = self.traffic_list_tree.item(selection[0])
            icao = item.get("tags", [None])[0] if item.get("tags") else None
            if icao:
                self.select_aircraft(icao)
                self.master.lift()
                self.master.focus()

    def update_traffic_list_window(self):
        """Update the traffic list table with current traffic data."""
        if self.traffic_list_tree is None:
            return
        
        # Clear existing items
        for item in self.traffic_list_tree.get_children():
            self.traffic_list_tree.delete(item)
        
        # Get current traffic data
        data = self.udp_receiver.get_latest_data()
        if not data.get('traffic'):
            return
        
        # Add traffic items
        for icao, traffic_data in sorted(data['traffic'].items()):
            # Get target symbol
            target_symbol = get_target_status_symbol(
                icao,
                self.udp_receiver.traffic_data,
                self.aircraft_targets
            ) or "-"
            
            # Insert row
            self.traffic_list_tree.insert(
                "",
                tk.END,
                values=(
                    traffic_data.squawk_code or "0000",
                    traffic_data.callsign or "N/A",
                    target_symbol,
                    int(traffic_data.altitude_ft),
                    int(traffic_data.velocity_knots),
                    int(traffic_data.heading_true)
                ),
                tags=(icao,)  # Store ICAO in tags for double-click handler
            )

    def update_traffic_list_periodic(self):
        """Periodically update traffic list window."""
        if self.traffic_list_window is not None:
            try:
                if self.traffic_list_window.winfo_exists():
                    self.update_traffic_list_window()
                    self.traffic_list_window.after(2000, self.update_traffic_list_periodic)
            except:
                # Window was closed
                self.traffic_list_window = None

    def close_traffic_list_window(self):
        """Close the traffic list window."""
        if self.traffic_list_window:
            self.traffic_list_window.destroy()
            self.traffic_list_window = None

    def setup_map_selection_compact(self, parent):
        """Set up compact map selection in bottom stripe with dropdown."""
        map_frame = ttk.Frame(parent)
        map_frame.pack(pady=2)
        
        tk.Label(map_frame, text="Map:", font=("Arial", 8)).pack(side="left", padx=2)
        
        # Get map options
        map_options = self.get_map_options()
        map_names = [name for name, _ in map_options]
        
        # Create dropdown (OptionMenu)
        self.map_selection_var = tk.StringVar(value=map_names[0])  # Default to first option
        self.map_selection_dropdown = ttk.OptionMenu(map_frame, self.map_selection_var, *map_names, 
                                                     command=self.on_map_selection_change)
        self.map_selection_dropdown.config(width=15)
        self.map_selection_dropdown.pack(side="left", padx=2)
    
    def on_map_selection_change(self, selected_map_name):
        """Handle map selection change from dropdown."""
        map_options = self.get_map_options()
        # Find the tile server URL for the selected map name
        for name, tile_server in map_options:
            if name == selected_map_name:
                self.map_widget.set_tile_server(tile_server)
                break

    def setup_map_control_compact(self, parent):
        """Set up compact map controls in bottom stripe."""
        control_frame = ttk.Frame(parent)
        control_frame.pack(pady=2)
        
        self.follow_var = tk.BooleanVar(value=True)
        self.follow_checkbox = tk.Checkbutton(
            control_frame,
            text="Follow",
            variable=self.follow_var,
            command=self.toggle_follow_mode,
            font=("Arial", 8)
        )
        self.follow_checkbox.pack(side="left", padx=2)
        
        self.proximity_filter_var = tk.BooleanVar(value=True)
        self.proximity_filter_checkbox = tk.Checkbutton(
            control_frame,
            text="Filter",
            variable=self.proximity_filter_var,
            font=("Arial", 8)
        )
        self.proximity_filter_checkbox.pack(side="left", padx=2)

    def setup_http_api_controls_compact(self, parent):
        """Set up compact HTTP API controls in bottom stripe - horizontal layout."""
        api_frame = ttk.LabelFrame(parent, text="Multiplayer", relief=tk.GROOVE)
        api_frame.pack(fill="both", expand=True, padx=5, pady=2)
        
        # Use horizontal layout with pack
        # Row 1: URL and Key
        row1 = ttk.Frame(api_frame)
        row1.pack(fill="x", padx=2, pady=2)
        
        tk.Label(row1, text="URL:", font=("Arial", 7)).pack(side=tk.LEFT, padx=2)
        self.api_server_url_entry = ttk.Entry(row1, width=18)
        self.api_server_url_entry.pack(side=tk.LEFT, padx=2)
        self.api_server_url_entry.insert(0, SERVER_URL)
        
        tk.Label(row1, text="Key:", font=("Arial", 7)).pack(side=tk.LEFT, padx=(10,2))
        self.api_key_entry = ttk.Entry(row1, width=15, show="*")
        self.api_key_entry.pack(side=tk.LEFT, padx=2)
        butter_api_key = get_butter_api_key()
        if butter_api_key:
            self.api_key_entry.insert(0, butter_api_key)
        
        # Row 2: Callsign, Data Source, and IP
        row2 = ttk.Frame(api_frame)
        row2.pack(fill="x", padx=2, pady=2)
        
        ttk.Label(row2, text="Callsign:").pack(side=tk.LEFT, padx=2)
        self.callsign_entry = ttk.Entry(row2, width=12, state="readonly")
        self.callsign_entry.pack(side=tk.LEFT, padx=2)
        
        ttk.Label(row2, text="Source:").pack(side=tk.LEFT, padx=(10,2))
        self.data_source_var = tk.StringVar(value="udp")
        tk.Radiobutton(row2, text="UDP", variable=self.data_source_var, value="udp").pack(side=tk.LEFT)
        tk.Radiobutton(row2, text="FSW", variable=self.data_source_var, value="fswidget").pack(side=tk.LEFT)
        self.auto_detect_var = tk.BooleanVar(value=False)
        tk.Checkbutton(row2, text="Auto", variable=self.auto_detect_var).pack(side=tk.LEFT, padx=2)
        
        ttk.Label(row2, text="IP:").pack(side=tk.LEFT, padx=(10,2))
        self.fswidget_ip_entry = ttk.Entry(row2, width=12)
        self.fswidget_ip_entry.insert(0, "localhost")
        self.fswidget_ip_entry.pack(side=tk.LEFT, padx=2)
        
        def update_fswidget_field_state(*args):
            auto_detect = self.auto_detect_var.get()
            if self.data_source_var.get() == "fswidget":
                if auto_detect:
                    self.fswidget_ip_entry.config(state="disabled")
                    self.fswidget_ip_entry.delete(0, tk.END)
                    self.fswidget_ip_entry.insert(0, "auto...")
                else:
                    self.fswidget_ip_entry.config(state="normal")
            else:
                self.fswidget_ip_entry.config(state="disabled")
        
        self.data_source_var.trace_add("write", update_fswidget_field_state)
        self.auto_detect_var.trace_add("write", update_fswidget_field_state)
        update_fswidget_field_state()
        
        # Row 3: Radius, Poll, Observer Position, Status, and Connect button
        row3 = ttk.Frame(api_frame)
        row3.pack(fill="x", padx=2, pady=2)
        
        ttk.Label(row3, text="R:").pack(side=tk.LEFT, padx=2)
        self.traffic_radius_entry = ttk.Entry(row3, width=5)
        self.traffic_radius_entry.pack(side=tk.LEFT, padx=2)
        self.traffic_radius_entry.insert(0, str(DEFAULT_TRAFFIC_RADIUS))
        
        ttk.Label(row3, text="P:").pack(side=tk.LEFT, padx=(5,2))
        self.poll_interval_entry = ttk.Entry(row3, width=4)
        self.poll_interval_entry.pack(side=tk.LEFT, padx=2)
        self.poll_interval_entry.insert(0, str(HTTP_POLL_INTERVAL))
        
        ttk.Label(row3, text="Obs:").pack(side=tk.LEFT, padx=(10,2))
        ttk.Label(row3, text="Lat").pack(side=tk.LEFT, padx=1)
        self.obs_lat_entry = ttk.Entry(row3, width=6)
        self.obs_lat_entry.pack(side=tk.LEFT, padx=1)
        ttk.Label(row3, text="Lon").pack(side=tk.LEFT, padx=1)
        self.obs_lon_entry = ttk.Entry(row3, width=6)
        self.obs_lon_entry.pack(side=tk.LEFT, padx=1)
        
        # Status and Connect button on the right
        # Using tk.Label instead of ttk.Label for dynamic color changes (fg parameter)
        self.api_status_label = tk.Label(row3, text="Not connected", fg="gray", font=("Arial", 7))
        self.api_status_label.pack(side=tk.LEFT, padx=(10,5))
        
        self.api_connect_button = ttk.Button(row3, text="Connect", command=self.connect_http_api, width=15)
        self.api_connect_button.pack(side=tk.RIGHT, padx=2)

    def setup_flightplan_controls_compact(self, parent):
        """Set up compact flight plan controls in bottom stripe."""
        fp_frame = ttk.LabelFrame(parent, text="Flight Plan", relief=tk.GROOVE)
        fp_frame.pack(fill="y", padx=5, pady=2)
        
        self.load_kml_button = ttk.Button(fp_frame, text="Load KML", command=self.load_kml_file, width=15)
        self.load_kml_button.pack(pady=2, padx=2)
        
        self.show_flightplan_var = tk.BooleanVar(value=False)
        self.show_flightplan_checkbox = tk.Checkbutton(
            fp_frame, text="Show", variable=self.show_flightplan_var,
            command=self.toggle_flight_plan_display, font=("Arial", 7)
        )
        self.show_flightplan_checkbox.pack(pady=2)
        
        self.flightplan_status = tk.Label(fp_frame, text="No FP", fg="gray", font=("Arial", 7))
        self.flightplan_status.pack(pady=2)

    def setup_recording_controls_compact(self, parent):
        """Set up compact recording controls in bottom stripe."""
        rec_frame = ttk.LabelFrame(parent, text="Recording", relief=tk.GROOVE)
        rec_frame.pack(fill="y", padx=5, pady=2)
        
        self.record_var = tk.BooleanVar(value=False)
        self.armed_var = tk.BooleanVar(value=False)
        
        self.arm_button = ttk.Button(
            rec_frame, text="ARM",
            bg="#ff9900", fg="black", command=self.toggle_arm_recording, width=8
        )
        self.arm_button.pack(pady=2, padx=2)
        
        self.record_button = ttk.Button(
            rec_frame, text="REC",
            bg="#cccccc", fg="black", command=self.toggle_csv_logging, width=8
        )
        self.record_button.pack(pady=2, padx=2)
        
        self.recording_status = tk.Label(rec_frame, text="Ready", fg="black", font=("Arial", 7))
        self.recording_status.pack(pady=2)

    def setup_map_control(self):
        """Set up controls for map centering behavior (old method, kept for compatibility)."""
        # This method is now replaced by setup_map_control_compact, but kept for any references
        pass
        
    def toggle_follow_mode(self):
        """Toggle whether the map should automatically follow the aircraft."""
        self.follow_aircraft = self.follow_var.get()
        if not self.follow_aircraft:
            # Store current map center when disabling follow mode
            current_pos = self.map_widget.get_position()
            self.map_center = (current_pos[0], current_pos[1])
            #print(f"Follow mode disabled. Map center fixed at: {self.map_center}")
        else:
        # When re-enabling follow mode, if we have GPS data, immediately center on aircraft
            if self.udp_receiver.latest_gps_data:
                gps = self.udp_receiver.latest_gps_data
                self.map_widget.set_position(gps.latitude, gps.longitude)
                #print("Follow mode enabled. Centering on aircraft.")    
    
    def setup_http_api_controls(self):
        """Set up HTTP API controls for multiplayer traffic."""
        api_frame = ttk.Frame(self.control_frame, relief=tk.GROOVE, bd=2)
        api_frame.pack(pady=5, padx=10, fill="x")
        
        # Title label
        ttk.Label(api_frame, text="Multiplayer (HTTP API)").pack(pady=(5,2))
        
        # Server URL
        ttk.Label(api_frame, text="Server URL:").pack(anchor=tk.W, padx=5, pady=2)
        self.api_server_url_entry = ttk.Entry(api_frame, width=20)
        self.api_server_url_entry.pack(padx=5, pady=2, fill="x")
        self.api_server_url_entry.insert(0, SERVER_URL)
        
        # API Key
        ttk.Label(api_frame, text="API Key:").pack(anchor=tk.W, padx=5, pady=2)
        self.api_key_entry = ttk.Entry(api_frame, width=20, show="*")
        self.api_key_entry.pack(padx=5, pady=2, fill="x")
        # Try to load API key from butter.json
        butter_api_key = get_butter_api_key()
        if butter_api_key:
            self.api_key_entry.insert(0, butter_api_key)
        
        # Callsign (read-only, shows callsign from server)
        callsign_frame = ttk.Frame(api_frame)
        callsign_frame.pack(padx=5, pady=2, fill="x")
        ttk.Label(callsign_frame, text="Your Callsign:").pack(anchor=tk.W, padx=5, pady=2)
        self.callsign_entry = ttk.Entry(callsign_frame, width=20, state="readonly")
        self.callsign_entry.pack(padx=5, pady=2, fill="x")
        tk.Label(callsign_frame, text="(from server)", font=("Arial", 7), fg="gray").pack(anchor=tk.W, padx=5)
        
        # Simulator data source selection
        source_frame = ttk.Frame(api_frame)
        source_frame.pack(padx=5, pady=5, fill="x")
        
        # Label only (Connect button moved below)
        ttk.Label(source_frame, text="Simulator Data Source:").pack(anchor=tk.W, padx=5, pady=2)
        
        self.data_source_var = tk.StringVar(value="udp")
        udp_radio = tk.Radiobutton(
            source_frame,
            text="Aerofly UDP (default)",
            variable=self.data_source_var,
            value="udp",
            font=("Arial", 8)
        )
        udp_radio.pack(anchor=tk.W, padx=20)
        
        fswidget_radio = tk.Radiobutton(
            source_frame,
            text="FSWidget TCP",
            variable=self.data_source_var,
            value="fswidget",
            font=("Arial", 8)
        )
        fswidget_radio.pack(anchor=tk.W, padx=20)
        
        # Auto-detect checkbox
        self.auto_detect_var = tk.BooleanVar(value=False)
        auto_detect_checkbox = tk.Checkbutton(
            source_frame,
            text="Auto-detect simulator IP",
            variable=self.auto_detect_var,
            font=("Arial", 8)
        )
        auto_detect_checkbox.pack(anchor=tk.W, padx=20, pady=2)
        
        # FSWidget IP address field
        fswidget_frame = ttk.Frame(source_frame)
        fswidget_frame.pack(fill="x", padx=20, pady=2)
        ttk.Label(fswidget_frame, text="FSWidget IP:").pack(side=tk.LEFT)
        self.fswidget_ip_entry = ttk.Entry(fswidget_frame, width=15)
        self.fswidget_ip_entry.insert(0, "localhost")
        self.fswidget_ip_entry.pack(side=tk.LEFT, padx=5)
        
        # Update FSWidget field state based on selection and auto-detect
        def update_fswidget_field_state(*args):
            auto_detect = self.auto_detect_var.get()
            if self.data_source_var.get() == "fswidget":
                if auto_detect:
                    self.fswidget_ip_entry.config(state="disabled")
                    self.fswidget_ip_entry.delete(0, tk.END)
                    self.fswidget_ip_entry.insert(0, "auto-detecting...")
                else:
                    self.fswidget_ip_entry.config(state="normal")
            else:
                self.fswidget_ip_entry.config(state="disabled")
        
        # Use trace_add() for Tcl 9 compatibility (replaces deprecated trace())
        self.data_source_var.trace_add("write", update_fswidget_field_state)
        self.auto_detect_var.trace_add("write", update_fswidget_field_state)
        update_fswidget_field_state()  # Initial state
        
        # Traffic radius and polling interval
        settings_frame = ttk.Frame(api_frame)
        settings_frame.pack(padx=5, pady=2, fill="x")
        
        ttk.Label(settings_frame, text="Radius (miles):").pack(side=tk.LEFT)
        self.traffic_radius_entry = ttk.Entry(settings_frame, width=8)
        self.traffic_radius_entry.pack(side=tk.LEFT, padx=5)
        self.traffic_radius_entry.insert(0, str(DEFAULT_TRAFFIC_RADIUS))
        
        ttk.Label(settings_frame, text="Poll (sec):").pack(side=tk.LEFT, padx=(10,0))
        self.poll_interval_entry = ttk.Entry(settings_frame, width=6)
        self.poll_interval_entry.pack(side=tk.LEFT, padx=5)
        self.poll_interval_entry.insert(0, str(HTTP_POLL_INTERVAL))
        
        # Manual observation position (for observers without GPS/UDP)
        obs_frame = ttk.Frame(api_frame)
        obs_frame.pack(padx=5, pady=2, fill="x")
        
        ttk.Label(obs_frame, text="Observer Position (if no GPS):", font=("Arial", 9, "italic")).pack(anchor=tk.W, padx=5, pady=(5,2))
        
        obs_pos_frame = ttk.Frame(obs_frame)
        obs_pos_frame.pack(padx=5, pady=2, fill="x")
        
        ttk.Label(obs_pos_frame, text="Lat:").pack(side=tk.LEFT)
        self.obs_lat_entry = ttk.Entry(obs_pos_frame, width=15)
        self.obs_lat_entry.pack(side=tk.LEFT, padx=5)
        
        ttk.Label(obs_pos_frame, text="Lon:").pack(side=tk.LEFT, padx=(10,0))
        self.obs_lon_entry = ttk.Entry(obs_pos_frame, width=15)
        self.obs_lon_entry.pack(side=tk.LEFT, padx=5)
        
        # Status label
        # Using tk.Label instead of ttk.Label for dynamic color changes (fg parameter)
        self.api_status_label = tk.Label(
            api_frame,
            text="Not connected",
            fg="gray",
            font=("Arial", 8)
        )
        self.api_status_label.pack(pady=2)
        
        # Connect button below status (moved from header for better visibility)
        self.api_connect_button = ttk.Button(
            api_frame,
            text="Connect",
            command=self.connect_http_api,
            width=15
        )
        self.api_connect_button.pack(pady=5, padx=10, fill="x")

    def setup_flightplan_controls(self):
        """Set up flight plan loading and display controls."""
        fp_frame = ttk.Frame(self.control_frame, relief=tk.GROOVE, bd=2)
        fp_frame.pack(pady=5, padx=10, fill="x")
        
        # Title label
        ttk.Label(fp_frame, text="Flight Plan").pack(pady=(5,2))
        
        # Load button
        self.load_kml_button = ttk.Button(
            fp_frame,
            text="Load KML File",
            command=self.load_kml_file,
            width=15
        )
        self.load_kml_button.pack(pady=3, padx=10)
        
        # Toggle display checkbox
        self.show_flightplan_var = tk.BooleanVar(value=False)
        self.show_flightplan_checkbox = ttk.Checkbutton(
            fp_frame, 
            text="Show Flight Plan", 
            variable=self.show_flightplan_var,
            command=self.toggle_flight_plan_display
        )
        self.show_flightplan_checkbox.pack(pady=3)
        
        # Status label
        self.flightplan_status = tk.Label(
            fp_frame, 
            text="No flight plan loaded",
            fg="gray",
            font=("Arial", 9)
        )
        self.flightplan_status.pack(pady=3)

    def load_kml_file(self):
        """Open a file dialog to select and load a KML file."""
        file_path = filedialog.askopenfilename(
            title="Select SimBrief KML File",
            filetypes=[("KML files", "*.kml"), ("All files", "*.*")]
        )
        
        if file_path:
            try:
                # Store the file path
                self.current_kml_file = file_path
                
                # Parse the KML file
                self.flight_plan_waypoints = self.parse_kml_file(file_path)
                
                if self.flight_plan_waypoints:
                    # Update status
                    self.flightplan_status.config(
                        text=f"Loaded: {os.path.basename(file_path)}",
                        fg="green"
                    )
                    
                    # Set the checkbox to checked and draw the flight plan
                    self.show_flightplan_var.set(True)
                    self.draw_flight_plan(self.flight_plan_waypoints)
                else:
                    self.flightplan_status.config(
                        text="Error: No route found in KML",
                        fg="red"
                    )
            except Exception as e:
                self.flightplan_status.config(
                    text=f"Error loading KML",
                    fg="red"
                )
                #print(f"Error loading KML file: {e}")

    def parse_kml_file(self, kml_file_path):
        """
        Parse a KML file and extract flight plan coordinates.
        
        Args:
            kml_file_path: Path to the KML file
            
        Returns:
            List of (latitude, longitude) tuples representing the flight plan route
        """
        try:
            # Parse the KML file
            tree = ET.parse(kml_file_path)
            root = tree.getroot()
            
            # Define the namespace
            namespace = {'kml': 'http://www.opengis.net/kml/2.2'}
            
            # Find all LineString elements which contain the flight path
            coordinates_elements = root.findall('.//kml:LineString/kml:coordinates', namespace)
            
            waypoints = []
            for coord_element in coordinates_elements:
                # KML coordinates are in lon,lat,alt format
                coord_text = coord_element.text.strip()
                for point in coord_text.split():
                    parts = point.split(',')
                    if len(parts) >= 2:
                        lon, lat = float(parts[0]), float(parts[1])
                        waypoints.append((lat, lon))  # Note: tkintermapview uses (lat, lon) order
            
            return waypoints
        except Exception as e:
            #print(f"Error parsing KML file: {e}")
            return []

    def draw_flight_plan(self, waypoints):
        """
        Draw the flight plan on the map.
        
        Args:
            waypoints: List of (latitude, longitude) tuples
        """
        if not waypoints:
            return
            
        # Create a path with the waypoints
        self.flight_plan_path = self.map_widget.set_path(waypoints, 
                                                        width=3,
                                                        color="#3080FF")
                                                        
        # Fit the map to show the entire flight plan
        if self.follow_aircraft:
            # If following aircraft, don't zoom out to fit flight plan
            pass
        else:
            # Otherwise, fit the map to show the entire flight plan
            self.map_widget.fit_bounds(waypoints)

    def toggle_flight_plan_display(self):
        """Toggle the display of the flight plan on the map."""
        show_plan = self.show_flightplan_var.get()
        
        if hasattr(self, 'flight_plan_path') and self.flight_plan_path:
            # Remove existing path
            self.flight_plan_path.delete()
            self.flight_plan_path = None
            
        if show_plan and hasattr(self, 'flight_plan_waypoints') and self.flight_plan_waypoints:
            # Redraw the path
            self.draw_flight_plan(self.flight_plan_waypoints)

    def setup_recording_controls(self):
        """Set up modern recording controls."""
        # Create a frame for recording controls
        recording_frame = ttk.Frame(self.control_frame)
        recording_frame.pack(pady=10, fill="x")
        
        # Create variables to track button states
        self.record_var = tk.BooleanVar(value=False)
        self.armed_var = tk.BooleanVar(value=False)
        
        # Create a styled frame for buttons
        button_frame = ttk.Frame(recording_frame, relief=tk.GROOVE, bd=2)
        button_frame.pack(pady=5, padx=10, fill="x")
        
        # Title label
        ttk.Label(button_frame, text="Recording Controls").pack(pady=(5,2))
        
        # Create the arming button
        self.arm_button = tk.Button(
            button_frame,
            text="ARM RECORDING",
            font=("Arial", 9),
            bg="#ff9900",  # Orange for armed state
            fg="black",
            activebackground="#ffcc00",
            relief=tk.RAISED,
            command=self.toggle_arm_recording,
            width=15
        )
        self.arm_button.pack(pady=3, padx=10)
        
        # Create the record button
        self.record_button = ttk.Button(
            button_frame,
            text="START RECORDING",
            font=("Arial", 9),
            bg="#cccccc",  # Gray when not active
            fg="black",
            activebackground="#dddddd",
            relief=tk.RAISED,
            command=self.toggle_csv_logging,
            width=15
        )
        self.record_button.pack(pady=3, padx=10)
        
        # Create a status label
        self.recording_status = tk.Label(
            button_frame, 
            text="Status: Ready",
            font=("Arial", 9)
        )
        self.recording_status.pack(pady=5)

    def toggle_arm_recording(self):
        """Toggle the armed state for recording."""
        is_armed = not self.armed_var.get()
        self.armed_var.set(is_armed)
        
        # Update UI
        if is_armed:
            self.arm_button.config(
                bg="#ff9900",  # Orange
                text="ARMED",
                relief=tk.SUNKEN
            )
            self.record_button.config(
                bg="#cccccc",  # Gray
                text="START RECORDING",
                relief=tk.RAISED,
                state=tk.DISABLED
            )
            self.recording_status.config(text="Status: Armed for Recording", fg="orange")
            self.record_var.set(False)
            
            # Tell the UDP receiver to arm for recording
            self.udp_receiver.arm_recording()
        else:
            self.arm_button.config(
                bg="#dddddd",  # Light gray
                text="ARM RECORDING",
                relief=tk.RAISED
            )
            self.record_button.config(
                state=tk.NORMAL
            )
            self.recording_status.config(text="Status: Ready", fg="black")
            
            # Disarm recording
            self.udp_receiver.armed_for_recording = False

    def toggle_csv_logging(self):
        """Toggle CSV logging on or off and update button appearance."""
        # Don't allow toggling if armed
        if self.armed_var.get():
            return
            
        # Toggle the state
        is_logging = not self.record_var.get()
        self.record_var.set(is_logging)
        
        # Update buttons
        if is_logging:
            # Recording state
            self.record_button.config(
                bg="#ff3333",  # Red when recording
                text="STOP RECORDING",
                relief=tk.SUNKEN
            )
            self.arm_button.config(state=tk.DISABLED)
            self.recording_status.config(text="Status: Recording", fg="#ff3333")
        else:
            # Off state
            self.record_button.config(
                bg="#dddddd",  # Light gray
                text="START RECORDING",
                relief=tk.RAISED
            )
            self.arm_button.config(state=tk.NORMAL)
            self.recording_status.config(text="Status: Ready", fg="black")
        
        # Set CSV logging
        self.udp_receiver.set_csv_logging(is_logging)

    def setup_map_selection(self):
        """Set up the map selection listbox."""
        tk.Label(self.control_frame, text="Select Map:").pack(pady=(10, 5))

        listbox_frame = ttk.Frame(self.control_frame)
        listbox_frame.pack(padx=0, pady=5)

        self.map_listbox = tk.Listbox(listbox_frame, width=24, height=6)
        self.map_listbox.pack(side="left")

        for option, _ in self.get_map_options():
            self.map_listbox.insert(tk.END, option)

        self.map_listbox.bind('<<ListboxSelect>>', lambda e: self.change_map())

    def setup_info_display(self):
        """Set up the information display area - REMOVED to save space."""
        # Removed to make room for multiplayer API frame
        self.info_display = None

    def setup_aircraft_marker(self):
        """Set up the aircraft marker image and related variables."""
        self.aircraft_image = Image.open("aircraft_icon.png").resize((32, 32))
        self.rotated_image = ImageTk.PhotoImage(self.aircraft_image)
        self.aircraft_marker = None
        self.initial_position_set = False

    def update_aircraft_position(self):
        """
        Update the aircraft's position on the map and the information display.
        This method is called periodically to refresh the display.
        """
        data = self.udp_receiver.get_latest_data()
        
        # DEBUG: Show connection state and traffic count
        #print(f"DEBUG UPDATE: connected={data['connected']}, traffic_count={len(data['traffic'])}, gps={data['gps'] is not None}")
        
        # Check if we're connected to the simulator
        if data['connected']:
            if hasattr(self, 'connection_status'):
                self.connection_status.config(text="Connected", fg="green")
            
            # Update API status label dynamically based on GPS availability
            if hasattr(self, 'api_status_label') and self.udp_receiver.use_http_api:
                if data['gps']:
                    # GPS data available - using normal mode
                    self.api_status_label.config(text="Connected (GPS Mode)", fg="green")
                elif (self.udp_receiver.manual_obs_lat is not None and 
                      self.udp_receiver.manual_obs_lon is not None):
                    # No GPS but manual position set - observer mode
                    self.api_status_label.config(text="Connected (Observer Mode)", fg="orange")
                else:
                    # Connected but waiting for position
                    self.api_status_label.config(text="Connected (Waiting for position)", fg="gray")
            
            # Update callsign display from server (if available)
            if hasattr(self, 'callsign_entry') and self.udp_receiver.callsign:
                self.callsign_entry.config(state="normal")
                self.callsign_entry.delete(0, tk.END)
                self.callsign_entry.insert(0, self.udp_receiver.callsign)
                self.callsign_entry.config(state="readonly")
            
            # Check if selected aircraft disappeared
            if self.selected_aircraft_icao and self.selected_aircraft_icao not in data.get('traffic', {}):
                # Selected aircraft is no longer in traffic - clear selection
                self.selected_aircraft_icao = None
                self.setup_aircraft_info_panel()  # Show "NO DATA" message
            
            # Update traffic markers regardless of GPS data
            if data['traffic']:
                # If own aircraft is in traffic, remove GPS marker to prevent double marker
                if self.udp_receiver.own_icao and self.udp_receiver.own_icao in data['traffic']:
                    if self.aircraft_marker:
                        self.aircraft_marker.delete()
                        self.aircraft_marker = None
                # Save current focus before updating markers
                focused_widget = self.master.focus_get()
                focused_widget_path = None
                if focused_widget:
                    try:
                        focused_widget_path = str(focused_widget)
                    except:
                        pass
                
                #print(f"\nDEBUG UPDATE: Calling update_traffic_markers with {len(data['traffic'])} aircraft")
                #print(f"DEBUG UPDATE: Traffic dict keys: {list(data['traffic'].keys())}")
                #for icao, traffic_obj in data['traffic'].items():
                #    print(f"DEBUG UPDATE: Traffic[{icao}]: callsign='{traffic_obj.callsign}', dep={traffic_obj.departure_airport}, arr={traffic_obj.arrival_airport}")
                self.update_traffic_markers(data['traffic'])
                
                # Update selected aircraft info panel values (without recreating widgets)
                if self.selected_aircraft_icao and self.selected_aircraft_icao in data['traffic']:
                    # Check if panel exists (has been created)
                    if hasattr(self, 'aircraft_info_content') and self.aircraft_info_content:
                        # Panel exists, update display values only
                        self.update_aircraft_info_values(data['traffic'][self.selected_aircraft_icao])
                
                # Try to restore focus if it was on a widget in the aircraft info frame
                if focused_widget:
                    try:
                        if focused_widget.winfo_exists():
                            # Check if widget is part of aircraft_info_frame
                            if hasattr(self, 'aircraft_info_frame') and self.aircraft_info_frame:
                                focused_widget.focus_set()
                    except:
                        pass
                
                # If we haven't set an initial position and we have traffic,
                # use the first traffic position to center the map
                if not self.initial_position_set and self.follow_aircraft:
                    first_traffic = next(iter(data['traffic'].values()))
                    self.map_widget.set_position(first_traffic.latitude, first_traffic.longitude)
                    self.map_widget.set_zoom(10)
                    self.initial_position_set = True
                    self.map_center = (first_traffic.latitude, first_traffic.longitude)
            # Center on ATC/observer position when there is no traffic yet (unified Connect + ATC)
            if (
                not self.initial_position_set
                and self.follow_aircraft
                and not data.get('traffic')
                and self.udp_receiver.manual_obs_lat is not None
                and self.udp_receiver.manual_obs_lon is not None
            ):
                olat, olon = self.udp_receiver.manual_obs_lat, self.udp_receiver.manual_obs_lon
                self.map_widget.set_position(olat, olon)
                self.map_widget.set_zoom(10)
                self.initial_position_set = True
                self.map_center = (olat, olon)
            # If we have GPS data, update the aircraft marker
            # Attitude optional: use GPS track when missing (FSWidget support). Avoid double marker.
            if data['gps']:
                own_in_traffic = False
                if data.get('traffic') and self.udp_receiver.own_icao:
                    own_in_traffic = self.udp_receiver.own_icao in data['traffic']
                if not own_in_traffic:
                    self.update_aircraft_marker(data)
            elif (
                self.udp_receiver.manual_obs_lat is not None
                and self.udp_receiver.manual_obs_lon is not None
            ):
                own_in_traffic = False
                if data.get('traffic') and self.udp_receiver.own_icao:
                    own_in_traffic = self.udp_receiver.own_icao in data['traffic']
                if not own_in_traffic:
                    obs_gps = GPSData(
                        longitude=self.udp_receiver.manual_obs_lon,
                        latitude=self.udp_receiver.manual_obs_lat,
                        altitude=0.0,
                        track=0.0,
                        ground_speed=0.0,
                    )
                    self.update_aircraft_marker({**data, 'gps': obs_gps})

            # Follow aircraft: recenter map on own position every tick (GPS or traffic).
            # When own aircraft is in traffic we use the traffic marker only, so we skip
            # update_aircraft_marker — but we must still recenter here or the map won't follow.
            if self.follow_aircraft:
                own_lat, own_lon = None, None
                if self.udp_receiver.own_icao and data.get('traffic') and self.udp_receiver.own_icao in data['traffic']:
                    t = data['traffic'][self.udp_receiver.own_icao]
                    own_lat, own_lon = t.latitude, t.longitude
                elif data.get('gps'):
                    g = data['gps']
                    own_lat, own_lon = g.latitude, g.longitude
                elif (
                    self.udp_receiver.manual_obs_lat is not None
                    and self.udp_receiver.manual_obs_lon is not None
                ):
                    own_lat = self.udp_receiver.manual_obs_lat
                    own_lon = self.udp_receiver.manual_obs_lon
                if own_lat is not None and own_lon is not None:
                    self.map_widget.set_position(own_lat, own_lon)
        else:
            if hasattr(self, 'connection_status'):
                self.connection_status.config(text="Disconnected", fg="red")
            self.clear_info_display()
            
            # Keep traffic markers even when disconnected (just don't add new ones)
            # But clean up aircraft marker
            if self.aircraft_marker:
                self.aircraft_marker.delete()
                self.aircraft_marker = None

        # Check if armed recording should automatically start (recording controls removed, but UDP receiver still supports it)
        if hasattr(self, 'armed_var') and hasattr(self, 'record_var'):
            if self.armed_var.get() and not self.udp_receiver.armed_for_recording:
                # The UDPReceiver has detected data and auto-started recording
                if self.udp_receiver.log_to_csv:
                    self.armed_var.set(False)
                    self.record_var.set(True)
                    if hasattr(self, 'arm_button'):
                        self.arm_button.config(
                            bg="#dddddd",  # Light gray
                            text="ARM RECORDING",
                            relief=tk.RAISED,
                            state=tk.DISABLED
                        )
                    if hasattr(self, 'record_button'):
                        self.record_button.config(
                            bg="#ff3333",  # Red when recording
                            text="STOP RECORDING",
                            relief=tk.SUNKEN
                        )
                    if hasattr(self, 'recording_status'):
                        self.recording_status.config(text="Status: Recording", fg="#ff3333")
            
        self.master.after(UPDATE_INTERVAL, self.update_aircraft_position)

    def clear_info_display(self):
        """Clear the information display when disconnected - REMOVED to save space."""
        # Info display removed to save space for multiplayer API frame
        pass

    def set_http_api_from_client(
        self,
        server_url: str,
        api_key: str,
        manual_lat: Optional[float] = None,
        manual_lon: Optional[float] = None,
    ) -> None:
        """
        Set HTTP API from Connect tab (unified app handoff).
        Called when user connects in Connect tab - Map tab uses same server config.
        When manual_lat/manual_lon are set (ATC mode in unified app), traffic queries use that
        fixed observer position instead of simulator GPS.
        """
        if not server_url or not api_key:
            return
        if not server_url.startswith(('http://', 'https://')):
            server_url = f"https://{server_url}"
        try:
            radius = float(self.traffic_radius_entry.get() or DEFAULT_TRAFFIC_RADIUS)
        except (ValueError, AttributeError):
            radius = DEFAULT_TRAFFIC_RADIUS
        try:
            poll_interval = float(self.poll_interval_entry.get() or HTTP_POLL_INTERVAL)
            poll_interval = max(poll_interval, MIN_POLL_INTERVAL)
        except (ValueError, AttributeError):
            poll_interval = HTTP_POLL_INTERVAL
        self.udp_receiver.set_http_api(
            server_url, api_key, radius, None, poll_interval, manual_lat, manual_lon
        )
        if hasattr(self, 'api_status_label'):
            if manual_lat is not None and manual_lon is not None:
                self.api_status_label.config(text="Connected (Observer Mode)", fg="orange")
            else:
                self.api_status_label.config(text="Connected (GPS Mode)", fg="green")
        if hasattr(self, 'api_connect_button'):
            self.api_connect_button.config(text="Disconnect", command=self.disconnect_http_api)
        if hasattr(self, 'api_server_url_entry'):
            self.api_server_url_entry.delete(0, tk.END)
            self.api_server_url_entry.insert(0, server_url)
        if hasattr(self, 'api_key_entry'):
            self.api_key_entry.delete(0, tk.END)
            self.api_key_entry.insert(0, api_key)

    def connect_http_api(self):
        """Connect to HTTP API for traffic polling."""
        server_url = self.api_server_url_entry.get().strip()
        api_key = self.api_key_entry.get().strip()
        
        if not server_url:
            self.api_status_label.config(text="Error: No server URL", fg="red")
            return
        
        if not api_key:
            self.api_status_label.config(text="Error: No API key", fg="red")
            return
        
        # Get data source selection
        use_fswidget = (self.data_source_var.get() == "fswidget")
        auto_detect = self.auto_detect_var.get()
        
        # Get FSWidget IP (only used if not auto-detecting)
        if auto_detect:
            fswidget_ip = "localhost"  # Will be auto-detected
        else:
            fswidget_ip = self.fswidget_ip_entry.get().strip() or "localhost"
        
        # Reconfigure UDPReceiver with selected data source if it changed (skip when using shared_data_source)
        source_changed = (use_fswidget != (self.udp_receiver.simulator_data_source.source_type == DataSourceType.FSWIDGET))
        auto_detect_changed = (auto_detect != self.auto_detect)
        
        if (source_changed or auto_detect_changed) and self.shared_data_source is None:
            # Stop current receiver
            self.udp_receiver.stop()
            # Create new receiver with correct data source and auto-detect setting
            self.udp_receiver = UDPReceiver(use_fswidget=use_fswidget, fswidget_ip=fswidget_ip, auto_detect=auto_detect)
            self.udp_receiver.start_receiving()
            self.data_source_type = DataSourceType.FSWIDGET if use_fswidget else DataSourceType.UDP
            self.fswidget_ip = fswidget_ip
            self.auto_detect = auto_detect
        
        try:
            radius = float(self.traffic_radius_entry.get() or DEFAULT_TRAFFIC_RADIUS)
        except ValueError:
            radius = DEFAULT_TRAFFIC_RADIUS
        
        try:
            poll_interval = float(self.poll_interval_entry.get() or HTTP_POLL_INTERVAL)
            # Enforce minimum
            if poll_interval < MIN_POLL_INTERVAL:
                self.api_status_label.config(text=f"Poll interval must be >= {MIN_POLL_INTERVAL}s", fg="orange")
                poll_interval = MIN_POLL_INTERVAL
                self.poll_interval_entry.delete(0, tk.END)
                self.poll_interval_entry.insert(0, str(MIN_POLL_INTERVAL))
        except ValueError:
            poll_interval = HTTP_POLL_INTERVAL
        
        # Callsign is now read-only and comes from server, so we don't read it from the entry
        # (The entry will be updated when we receive our own traffic from the server)
        
        # Get manual observation position (optional, for observers)
        manual_lat = None
        manual_lon = None
        obs_lat_str = self.obs_lat_entry.get().strip()
        obs_lon_str = self.obs_lon_entry.get().strip()
        
        if obs_lat_str and obs_lon_str:
            try:
                manual_lat = float(obs_lat_str)
                manual_lon = float(obs_lon_str)
                # Validate range
                if not (-90 <= manual_lat <= 90):
                    self.api_status_label.config(text="Error: Latitude must be -90 to 90", fg="red")
                    return
                if not (-180 <= manual_lon <= 180):
                    self.api_status_label.config(text="Error: Longitude must be -180 to 180", fg="red")
                    return
                #print(f"Using manual observation position: {manual_lat:.6f}, {manual_lon:.6f}")
            except ValueError:
                self.api_status_label.config(text="Error: Invalid lat/lon format", fg="red")
                return
        
        # Ensure URL has protocol
        if not server_url.startswith(('http://', 'https://')):
            server_url = f"http://{server_url}"
        
        # Configure UDPReceiver with HTTP API (this will start the polling thread)
        # Note: callsign parameter is no longer used - it will come from server response
        self.udp_receiver.set_http_api(server_url, api_key, radius, None, poll_interval, manual_lat, manual_lon)
        
        # Initial status - will be updated dynamically in update_aircraft_position()
        if manual_lat is not None and manual_lon is not None:
            # Manual position provided, but check if GPS is available
            if self.udp_receiver.latest_gps_data:
                self.api_status_label.config(text="Connected (GPS Mode)", fg="green")
            else:
                self.api_status_label.config(text="Connected (Observer Mode)", fg="orange")
        else:
            # No manual position - will use GPS if available
            if self.udp_receiver.latest_gps_data:
                self.api_status_label.config(text="Connected (GPS Mode)", fg="green")
            else:
                self.api_status_label.config(text="Connected (Waiting for GPS)", fg="gray")
        self.api_connect_button.config(text="Disconnect", command=self.disconnect_http_api)
    
    def disconnect_http_api(self):
        """Disconnect from HTTP API and fall back to UDP."""
        self.udp_receiver.disable_http_api()
        self.api_status_label.config(text="Not connected", fg="gray")
        self.api_connect_button.config(text="Connect", command=self.connect_http_api)
        # Clear callsign display
        if hasattr(self, 'callsign_entry'):
            self.callsign_entry.config(state="normal")
            self.callsign_entry.delete(0, tk.END)
            self.callsign_entry.config(state="readonly")
    
    def update_traffic_markers(self, traffic_data):
        """Update the traffic markers on the map."""
        #print(f"\n{'='*80}")
        #print(f"DEBUG MARKERS: update_traffic_markers called with {len(traffic_data)} aircraft")
        #print(f"DEBUG MARKERS: Own ICAO: {self.udp_receiver.own_icao}")
        #print(f"DEBUG MARKERS: Current callsign from server: {self.udp_receiver.callsign}")
        
        # Get our own position if available (to filter out echoed traffic)
        # NOTE: In observer mode, we don't filter by own position - we want to see all traffic including ATC towers
        own_lat = None
        own_lon = None
        if self.udp_receiver.latest_gps_data:
            own_lat = self.udp_receiver.latest_gps_data.latitude
            own_lon = self.udp_receiver.latest_gps_data.longitude
            terminal_debug_print("rewinger", f"[DEBUG REWINGER] Filtering echoed traffic using own position: lat={own_lat:.6f}, lon={own_lon:.6f}")
        else:
            terminal_debug_print("rewinger", f"[DEBUG REWINGER] No own position available - showing all traffic (observer mode)")
        
        # Remove markers for traffic that's no longer present
        removed_count = 0
        for icao in list(self.traffic_markers.keys()):
            if icao not in traffic_data:
                self.traffic_markers[icao].delete()
                del self.traffic_markers[icao]
                removed_count += 1
        #if removed_count > 0:
        #    print(f"DEBUG MARKERS: Removed {removed_count} old markers")
        
        # Update existing markers and add new ones
        for idx, (icao, data) in enumerate(traffic_data.items()):
            #print(f"\nDEBUG MARKERS [{idx+1}/{len(traffic_data)}]: Processing aircraft ICAO: {icao}")
            
            # Determine if this traffic contact is "our" own aircraft (as identified by the server)
            # BUT: Don't filter out control towers even if they match own ICAO (they're fixed positions, not our aircraft)
            is_control_tower_marker = is_control_tower(data)
            is_own_aircraft = bool(self.udp_receiver.own_icao and icao == self.udp_receiver.own_icao and not is_control_tower_marker)
            if is_own_aircraft:
                terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Aircraft {icao} is own aircraft (filtered from display)")
            elif is_control_tower_marker and self.udp_receiver.own_icao and icao == self.udp_receiver.own_icao:
                terminal_debug_print("rewinger", f"[DEBUG REWINGER]   Aircraft {icao} matches own ICAO but is a control tower - showing anyway")
           # print(f"DEBUG MARKERS [{idx+1}]: Is own aircraft? {is_own_aircraft}")
            #print(f"DEBUG MARKERS [{idx+1}]: Traffic data fields:")
            #print(f"  ICAO: {icao}")
            #print(f"  Callsign: '{data.callsign}'")
            #print(f"  Position: ({data.latitude:.6f}, {data.longitude:.6f})")
            #print(f"  Altitude: {data.altitude_ft:.0f} ft")
            #print(f"  Departure: {data.departure_airport}")
            #print(f"  Arrival: {data.arrival_airport}")
            #print(f"  Model: {data.airplane_model}")
            #print(f"  Livery: {data.airplane_livery}")

            # Capture variables explicitly to avoid closure issues
            lat = data.latitude
            lon = data.longitude
            heading = data.heading_true
            callsign = data.callsign
            alt_ft = data.altitude_ft
            squawk_code = data.squawk_code  # API v2.2 - get squawk code (default: "0000")
            
            # For our own aircraft (identified via self.own_icao), use the same blue icon
            # as the local UDP aircraft marker instead of the orange traffic icon.
            # For control towers, use the control tower icon (fixed, no rotation).
            # Note: is_control_tower_marker was already calculated above
            if is_own_aircraft:
                rotated_image = self.rotate_image(heading)
            elif is_control_tower_marker:
                # Control tower icon: fixed orientation (no rotation, always heading 0)
                rotated_image = ImageTk.PhotoImage(self.control_tower_image)
            else:
                rotated_image = self.rotate_traffic_image(heading)
            
            # For control towers, use simplified marker text (reconstruct full identifier)
            if is_control_tower_marker:
                # Reconstruct full ATC identifier from airplane_model (ICAO) and airplane_livery (function)
                # Logic: Model = ICAO code, Livery = function (GROUND, TOWER, etc.)
                model = (data.airplane_model or "").strip().upper()
                livery = (data.airplane_livery or "").strip().upper()
                
                skip_values = ('N/A', 'N/C', 'NO DATA', '')
                import re
                icao_pattern = re.compile(r'^[A-Z0-9]{3,4}$')
                
                # Determine ICAO and function from model and livery
                icao_code = None
                function = None
                
                if model and model not in skip_values:
                    if icao_pattern.match(model):
                        icao_code = model
                    else:
                        # Model might be function if livery is ICAO
                        function = model
                
                if livery and livery not in skip_values:
                    if icao_pattern.match(livery):
                        if not icao_code:  # Use livery as ICAO if model wasn't
                            icao_code = livery
                    else:
                        if not function:  # Use livery as function if model wasn't
                            function = livery
                
                # Build marker text
                if icao_code and function:
                    marker_text = f"ATC_{icao_code}_{function}"
                elif icao_code:
                    marker_text = f"ATC_{icao_code}"
                elif function:
                    marker_text = f"ATC_{function}"
                else:
                    # Fallback to callsign or generic
                    marker_text = callsign.upper() if callsign else "TOWER"
            else:
                # Create new marker with squawk_code first, then callsign, departure/arrival airports, and altitude (API v2.2)
                # Format altitude: if >= 1000 ft, show as FL notation (divide by 100 and round)
                # Examples: 7000 ft = FL070, 9999 ft = FL100, 10000 ft = FL100, 32320 ft = FL323
                if alt_ft >= 1000:
                    fl_number = int(round(alt_ft / 100))  # Round to nearest 100 ft
                    altitude_text = f"FL{fl_number:03d}"  # Format with 3 digits (FL070, FL100, etc.)
                else:
                    altitude_text = f"{int(alt_ft)}'"  # Show as feet for altitudes < 1000
                # Format: "0001 UAL123 LOWG/EDDF 3500'" (squawk first, API v2.2 requirement)
                dep_airport = data.departure_airport
                arr_airport = data.arrival_airport
                
                # Squawk code is always first field (API v2.2 requirement) - always show even if "0000"
                squawk_code_display = squawk_code if squawk_code else "0000"
                
                if dep_airport != 'N/A' and arr_airport != 'N/A':
                    # Show departure/arrival airports (e.g., "0001 UAL123 LOWG/EDDF 3500'")
                    route_text = f"{dep_airport}/{arr_airport}"
                    if callsign:
                        marker_text = f"{squawk_code_display} {callsign} {route_text} {altitude_text}"
                    else:
                        marker_text = f"{squawk_code_display} {route_text} {altitude_text}"
                else:
                    # No flight plan airports, just show squawk, callsign and altitude
                    if callsign:
                        marker_text = f"{squawk_code_display} {callsign} {altitude_text}"
                    else:
                        marker_text = f"{squawk_code_display} {altitude_text}"
            
            # Get target status symbol if targets are set
            target_symbol = get_target_status_symbol(
                icao,
                self.udp_receiver.traffic_data,
                self.aircraft_targets
            )
            
            # Prepend target symbol to marker text if present
            if target_symbol:
                marker_text = f"{target_symbol} {marker_text}"
            
            # Determine if this is the selected aircraft
            is_selected = (icao == self.selected_aircraft_icao)
            
            # Use selected icon if this is the selected aircraft (but not for control towers)
            if is_selected and not is_control_tower_marker:
                try:
                    selected_image = Image.open("selected_traffic.png").resize((24, 24))
                    rotated_image = ImageTk.PhotoImage(selected_image.rotate(-heading))
                except FileNotFoundError:
                    # Fallback to regular traffic icon if selected_traffic.png doesn't exist
                    pass  # Use existing rotated_image
            
            # Update existing marker in place (smooth, no blink) or create new marker
            if icao in self.traffic_markers:
                marker = self.traffic_markers[icao]
                marker.set_position(lat, lon)
                marker.set_text(marker_text)
                try:
                    marker.change_icon(rotated_image)
                except AttributeError:
                    # Fallback: marker may not have icon (e.g. default pin) - recreate
                    marker.delete()
                    marker_data = {"icao": icao, "callsign": callsign}
                    self.traffic_markers[icao] = self.map_widget.set_marker(
                        lat, lon,
                        icon=rotated_image,
                        icon_anchor="center",
                        text=marker_text,
                        data=marker_data,
                        command=self.on_aircraft_marker_click
                    )
            else:
                # Create new marker
                marker_data = {"icao": icao, "callsign": callsign}
                self.traffic_markers[icao] = self.map_widget.set_marker(
                    lat, lon,
                    icon=rotated_image,
                    icon_anchor="center",
                    text=marker_text,
                    data=marker_data,
                    command=self.on_aircraft_marker_click
                )
        
        #print(f"{'='*80}\n")

    def on_aircraft_marker_click(self, marker):
        """Handle click on aircraft marker."""
        if marker.data and "icao" in marker.data:
            icao = marker.data["icao"]
            self.select_aircraft(icao)
            # Set focus to the aircraft info frame so user can immediately start typing in target fields
            if hasattr(self, 'aircraft_info_frame'):
                self.aircraft_info_frame.focus_set()
    
    def clear_aircraft_selection(self):
        """Clear the selected aircraft and show NO DATA message."""
        self.selected_aircraft_icao = None
        self.setup_aircraft_info_panel()  # Show "NO DATA" message
        
        # Update all markers to remove selected state (if any markers exist)
        if hasattr(self, 'udp_receiver') and self.udp_receiver.traffic_data:
            # Refresh markers to update their visual state
            traffic_dict = {}
            for icao, traffic_list in self.udp_receiver.traffic_data.items():
                if traffic_list:
                    traffic_dict[icao] = traffic_list[0]
            if traffic_dict:
                self.update_traffic_markers(traffic_dict)
    
    def select_aircraft(self, icao: str):
        """Select an aircraft and update the right panel."""
        # Check if aircraft exists in traffic data
        traffic_entry = self.udp_receiver.traffic_data.get(icao)
        if not traffic_entry:
            # Aircraft not in traffic - clear selection
            self.selected_aircraft_icao = None
            self.setup_aircraft_info_panel()  # Show "NO DATA" message
            return
        
        self.selected_aircraft_icao = icao
        traffic_data = traffic_entry[0]  # AirTrafficData object
        
        # Update right panel with aircraft information
        self.update_aircraft_info_panel(traffic_data)
        
        # Update markers to show selected state
        self.update_traffic_markers({icao: traffic_data})

    def rotate_traffic_image(self, angle: float) -> ImageTk.PhotoImage:
        """Rotate the traffic icon image by the given angle."""
        return ImageTk.PhotoImage(self.traffic_image.rotate(-angle))

    def update_aircraft_marker(self, data: Dict[str, Any]):
        """Update just the aircraft marker with the latest data."""
        gps_data: GPSData = data['gps']
        attitude_data: Optional[AttitudeData] = data.get('attitude')
        aircraft_data: Optional[AircraftData] = data.get('aircraft')
        
        if not self.initial_position_set:
            self.map_widget.set_position(gps_data.latitude, gps_data.longitude)
            self.map_widget.set_zoom(10)
            self.initial_position_set = True
            self.map_center = (gps_data.latitude, gps_data.longitude)
        heading = attitude_data.true_heading if attitude_data else getattr(gps_data, 'track', 0.0)
        self.rotated_image = self.rotate_image(heading)

        # Build marker text
        # Priority: 1) Aircraft data callsign, 2) Server callsign, 3) Simulator name or "Aircraft"
        if aircraft_data is not None:
            marker_text = (aircraft_data.FlightNumber + " " + aircraft_data.callsign) if aircraft_data.FlightNumber else aircraft_data.callsign
        elif self.udp_receiver.callsign:
            marker_text = self.udp_receiver.callsign
        elif self.udp_receiver.simulator_name and self.udp_receiver.simulator_name != "Unknown":
            marker_text = self.udp_receiver.simulator_name
        else:
            marker_text = "Aircraft"
        
        # Update existing marker in place (smooth, no blink) or create new marker
        if self.aircraft_marker:
            self.aircraft_marker.set_position(gps_data.latitude, gps_data.longitude)
            self.aircraft_marker.set_text(marker_text)
            try:
                self.aircraft_marker.change_icon(self.rotated_image)
            except AttributeError:
                self.aircraft_marker.delete()
                self.aircraft_marker = self.map_widget.set_marker(
                    gps_data.latitude, gps_data.longitude,
                    icon=self.rotated_image,
                    icon_anchor="center",
                    text=marker_text
                )
        else:
            self.aircraft_marker = self.map_widget.set_marker(
                gps_data.latitude, gps_data.longitude,
                icon=self.rotated_image,
                icon_anchor="center",
                text=marker_text
            )
        # Map recentering for follow mode is done in update_aircraft_position (handles both GPS and traffic)

    def update_info_display(self, data: Dict[str, Any]):
        """Update the information display with the latest aircraft data - REMOVED to save space."""
        # Info display removed to save space for multiplayer API frame
        pass

    def rotate_image(self, angle: float) -> ImageTk.PhotoImage:
        """Rotate the aircraft icon image by the given angle."""
        return ImageTk.PhotoImage(self.aircraft_image.rotate(-angle))

    def change_map(self):
        """Change the map tile server based on the user's selection."""
        selected_indices = self.map_listbox.curselection()
        if selected_indices:
            _, tile_server = self.get_map_options()[selected_indices[0]]
            self.map_widget.set_tile_server(tile_server)

    @staticmethod
    def get_map_options() -> List[Tuple[str, str]]:
        """Return a list of available map options with their tile server URLs."""
        return [
            ("OpenStreetMap", "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"),
            ("OpenStreetMap DE", "https://tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png"),
            ("OpenStreetMap FR", "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png"),
            ("OpenTopoMap", "https://a.tile.opentopomap.org/{z}/{x}/{y}.png"),
            ("Google Normal", "https://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}"),
            ("Google Satellite", "https://mt0.google.com/vt/lyrs=s&hl=en&x={x}&y={y}&z={z}"),
            ("Google Terrain", "https://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}"),
            ("Google Hybrid", "https://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}"),
            ("Carto Dark Matter", "https://cartodb-basemaps-a.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png"),
            ("Carto Positron", "https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"),
            ("ESRI World Imagery", "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"),
            ("ESRI World Street Map", "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}"),
            ("ESRI World Topo Map", "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}")
        ]

    def close_application(self):
        """Clean up resources and close the application."""
        # Stop HTTP API traffic polling (Rewinger doesn't send disconnect - it's a pure viewer)
        if self.udp_receiver.use_http_api:
            self.udp_receiver.disable_http_api()

        # Clean up flight plan path if it exists
        if hasattr(self, 'flight_plan_path') and self.flight_plan_path:
            self.flight_plan_path.delete()

        self.udp_receiver.stop()
        # When embedded, don't destroy - parent owns lifecycle
        if isinstance(self.container, tk.Tk):
            self.master.destroy()

if __name__ == "__main__":
    #print("Starting Aircraft Tracker / Rewinger")
    #print(f"Listening for simulator data on port {UDP_PORT}")
    #print(f"Listening for multiplayer traffic on port {TRAFFIC_PORT}")
    root = tk.Tk()
    app = AircraftTrackerApp(root)
    root.mainloop()