"""
宜宾五粮液机场29跑道点融合程序仿真系统
Point Merge Procedure Simulation for ZYCC RW29
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import math
import time
import json
import csv
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum

import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.lines import Line2D
import numpy as np
try:
    from scipy.interpolate import make_interp_spline
    HAS_SCIPY = True
except ImportError:
    HAS_SCIPY = False

# ============================================================
# 常量与数据定义
# ============================================================

EARTH_RADIUS = 6371.0  # km

# 航路点坐标 {名称: (纬度, 经度)}
WAYPOINTS = {
    "S1":    (28.421506, 104.869202),
    "S2":    (28.396336, 104.771942),
    "S3":    (28.400346, 104.670672),
    "S4":    (28.433126, 104.576382),
    "S5":    (28.491096, 104.499372),
    "S6":    (28.442478, 104.556792),
    "S7":    (28.507065, 104.484489),
    "S8":    (28.588187, 104.440076),
    "S9":    (28.677123, 104.427891),
    "S10":   (28.681946, 104.353826),
    "S11":   (28.460701, 104.353583),
    "N1":    (29.158001, 104.868956),
    "N2":    (29.158509, 104.766672),
    "N3":    (29.129722, 104.669846),
    "N4":    (29.07463,  104.588958),
    "N5":    (29.099569, 104.545047),
    "N6":    (29.15881,  104.621035),
    "N7":    (29.197444, 104.715671),
    "N8":    (29.209615, 104.819),
    "N9":    (28.8295,   104.837),
    "RW29":  (28.854861, 104.539417),
    "IGUNI": (29.125,    104.963333),
    "KAKMI": (28.685,    104.295),
    "OTGIN": (28.39,     104.886667),
    "VEDPO": (28.38,     104.311667),
    "YB614": (28.788889, 104.77925),
}

# 高度数据 (m)
ALTITUDES = {
    "outer_arc": 3000,   # 外排序边
    "inner_arc": 3600,   # 内排序边
    "merge_point": 2100, # 融合点
    "airport": 430,      # 机场
}

# 速度 (km/h)
SPEED_STAGE1 = 380.0   # 205 kt
SPEED_STAGE2 = 278.0   # 150 kt
DECEL_RATE = 2.0       # 匀减速率 km/h per second
MIN_SPEED_RATIO = 0.50 # 最低速度为阶段速度的50%

# 间隔 (km)
SEP_STAGE1 = 10.0
SEP_STAGE2 = 6.0

# 下降梯度
DESCENT_GRADIENT_OUTER = 0.0257  # 外排序边 2.57%
DESCENT_GRADIENT_INNER = 0.05    # 内排序边 约5%

# 融合点到机场距离
MERGE_TO_AIRPORT = 39.0  # km


# ============================================================
# 工具函数
# ============================================================

def haversine(lat1, lon1, lat2, lon2):
    """计算两个经纬度点之间的距离(km)"""
    r = EARTH_RADIUS
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    a = math.sin(dlat / 2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    return r * c


def latlon_to_local(lat, lon, ref_lat, ref_lon):
    """经纬度转局部坐标(km)，以ref为原点，东为X正，北为Y正"""
    x = haversine(ref_lat, ref_lon, ref_lat, lon) * (1 if lon >= ref_lon else -1)
    y = haversine(ref_lat, ref_lon, lat, ref_lon) * (1 if lat >= ref_lat else -1)
    return x, y


def lerp_point(p1, p2, t):
    """线性插值两点"""
    return (p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t)


# 使用文档提供的精确局部坐标 (x=东, y=北, km, RW29为原点)
REF_LAT, REF_LON = WAYPOINTS["RW29"]

def _ll2loc(name):
    return latlon_to_local(*WAYPOINTS[name], REF_LAT, REF_LON)

LOCAL_COORDS = {
    "S1":    (32.11, -48.23),
    "S2":    (22.52, -51.05),
    "S3":    (12.53, -50.57),
    "S4":    (3.25,  -46.82),
    "S5":    (-4.28, -40.24),
    "S6":    (-5.23, -46.55),
    "S7":    (-11.4, -38.68),
    "S8":    (-15.08,-29.39),
    "S9":    (-15.99,-19.43),
    "S11":   (-19.31,-41.94),
    "N1":    (32.66, 33.59),
    "N2":    (22.66, 33.81),
    "N3":    (13.15, 30.73),
    "N4":    (5.17,  24.7),
    "RW29":  (0.0,   0.0),
    "IGUNI": (42.03, 30.1),
    "KAKMI": (-23.98,-19.01),
    "OTGIN": (33.88, -51.7),
    "VEDPO": (-22.47,-52.79),
    "YB614": (23.37, -7.36),
    "YB618": (18.96, -21.26),
    "YB616": (27.01, 4.13),
}
# S10: 文档表中误录为S11值，从经纬度计算正确位置（约(-18, -19)，在KAKMI与S9之间）
LOCAL_COORDS["S10"] = _ll2loc("S10")
# N9: 用于IGUNI路径(YB616→N9→YB614)，从经纬度计算
LOCAL_COORDS["N9"] = _ll2loc("N9")

# 圆弧排序边参数 (圆心=融合点, 半径, 起始角, 终止角)
# S9-YB618-S6: 角度49.27°, 半径35km
# S1-YB618-S5: 角度76.87°, 半径30km
# IGUNI-YB616-N5(N4): 角度76.76°, 半径30km
ARC_PARAMS = {
    "inner_south": {"center": "YB618", "radius": 35.0, "angle_deg": 49.27},
    "outer_south": {"center": "YB618", "radius": 30.0, "angle_deg": 76.87},
    "outer_north": {"center": "YB616", "radius": 30.0, "angle_deg": 76.76},
}

def generate_arc_points(center_name, wp_start, wp_end, n_points=50):
    """生成从wp_start到wp_end绕center的圆弧插值点"""
    cx, cy = LOCAL_COORDS[center_name]
    sx, sy = LOCAL_COORDS[wp_start]
    ex, ey = LOCAL_COORDS[wp_end]
    a_start = math.atan2(sy - cy, sx - cx)
    a_end = math.atan2(ey - cy, ex - cx)
    # 确保沿较短弧走
    diff = a_end - a_start
    if diff > math.pi:
        diff -= 2 * math.pi
    elif diff < -math.pi:
        diff += 2 * math.pi
    r = math.sqrt((sx - cx)**2 + (sy - cy)**2)
    points = []
    for i in range(n_points + 1):
        t = i / n_points
        a = a_start + diff * t
        points.append((cx + r * math.cos(a), cy + r * math.sin(a)))
    return points, r, a_start, a_end, diff


# ============================================================
# 路径定义
# ============================================================

class Direction(Enum):
    KAKMI = "KAKMI"
    VEDPO = "VEDPO"
    OTGIN = "OTGIN"
    IGUNI = "IGUNI"

# 完整路径（按顺序的航路点名称列表）
FULL_PATHS = {
    Direction.KAKMI: ["KAKMI", "S10", "S9", "S8", "S7", "S6", "YB618", "YB614", "RW29"],
    Direction.VEDPO: ["VEDPO", "S11", "S7", "S6", "YB618", "YB614", "RW29"],
    Direction.OTGIN: ["OTGIN", "S1", "S2", "S3", "S4", "S5", "YB618", "YB614", "RW29"],
    Direction.IGUNI: ["IGUNI", "N1", "N2", "N3", "N4", "YB616", "N9", "YB614", "RW29"],
}

# 排序边定义（在排序边上的航路点，段的两端都在此列表中即为圆弧段）
# IGUNI本身也在排序边上（问题二），飞机在IGUNI就可直飞
SORTING_LEGS = {
    Direction.KAKMI: ["S9", "S8", "S7", "S6"],
    Direction.VEDPO: ["S7", "S6"],
    Direction.OTGIN: ["S1", "S2", "S3", "S4", "S5"],
    Direction.IGUNI: ["IGUNI", "N1", "N2", "N3", "N4"],
}

# 融合点
MERGE_POINTS = {
    Direction.KAKMI: "YB618",
    Direction.VEDPO: "YB618",
    Direction.OTGIN: "YB618",
    Direction.IGUNI: "YB616",
}

# 排序边类型（内/外）
ARC_TYPE = {
    Direction.KAKMI: "inner",  # 内排序边 3600m
    Direction.VEDPO: "inner",
    Direction.OTGIN: "outer",  # 外排序边 3000m
    Direction.IGUNI: "outer",
}

# 圆弧中心与半径 (排序边为圆弧，圆心为融合点)
ARC_CENTER = {
    Direction.KAKMI: ("YB618", 35.0),  # S9-S8-S7-S6 绕YB618, 半径35km
    Direction.VEDPO: ("YB618", 35.0),  # S7-S6 绕YB618, 半径35km
    Direction.OTGIN: ("YB618", 30.0),  # S1-S2-S3-S4-S5 绕YB618, 半径30km
    Direction.IGUNI: ("YB616", 30.0),  # IGUNI-N1-N2-N3-N4 绕YB616, 半径30km
}


def is_arc_segment(direction: "Direction", wp1: str, wp2: str) -> bool:
    """判断段(wp1,wp2)是否为排序边上的圆弧段"""
    sorting = SORTING_LEGS[direction]
    return wp1 in sorting and wp2 in sorting


def arc_segment_length(direction: "Direction", wp1: str, wp2: str) -> float:
    """计算圆弧段的弧长(km)"""
    center_name, radius = ARC_CENTER[direction]
    cx, cy = LOCAL_COORDS[center_name]
    p1 = LOCAL_COORDS[wp1]
    p2 = LOCAL_COORDS[wp2]
    a1 = math.atan2(p1[1] - cy, p1[0] - cx)
    a2 = math.atan2(p2[1] - cy, p2[0] - cx)
    diff = a2 - a1
    if diff > math.pi:
        diff -= 2 * math.pi
    elif diff < -math.pi:
        diff += 2 * math.pi
    return abs(diff) * radius


def arc_interp(direction: "Direction", wp1: str, wp2: str, t: float) -> Tuple[float, float]:
    """圆弧段参数t∈[0,1]处的位置"""
    center_name, radius = ARC_CENTER[direction]
    cx, cy = LOCAL_COORDS[center_name]
    p1 = LOCAL_COORDS[wp1]
    p2 = LOCAL_COORDS[wp2]
    a1 = math.atan2(p1[1] - cy, p1[0] - cx)
    a2 = math.atan2(p2[1] - cy, p2[0] - cx)
    diff = a2 - a1
    if diff > math.pi:
        diff -= 2 * math.pi
    elif diff < -math.pi:
        diff += 2 * math.pi
    a = a1 + diff * t
    return (cx + radius * math.cos(a), cy + radius * math.sin(a))


# ============================================================
# 飞机类
# ============================================================

class FlightStage(Enum):
    APPROACHING = 0   # 飞向排序边
    ON_ARC = 1        # 在排序边上
    DIRECT = 2        # 直飞融合点
    STAGE2 = 3        # 第二阶段（通过融合点后）
    LANDED = 4        # 已落地

@dataclass
class Aircraft:
    flight_id: str
    direction: Direction
    entry_time: float            # 进入入口点的时间(秒)
    path: List[str] = field(default_factory=list)
    current_segment: int = 0     # 当前路径段索引
    segment_progress: float = 0.0  # 当前段进度 [0,1]
    stage: FlightStage = FlightStage.APPROACHING
    speed: float = SPEED_STAGE1  # km/h
    altitude: float = 3000.0     # m
    x: float = 0.0
    y: float = 0.0
    total_distance: float = 0.0  # 总飞行距离
    elapsed_time: float = 0.0    # 飞行时间
    arc_entry_order: int = -1    # 进入弧的编号
    is_slowed: bool = False      # 是否被减速
    active: bool = False         # 是否已激活（到达入口点时间）
    finished: bool = False
    actual_path: List[Tuple[float, float]] = field(default_factory=list)  # 实际轨迹
    virtual_coords: Dict[str, Tuple[float, float]] = field(default_factory=dict)  # 虚拟航点（用于直飞起点）
    descent_gradient: float = 0.0  # 动态计算的下降梯度

    def get_coord(self, name: str) -> Tuple[float, float]:
        """获取航点坐标（支持虚拟航点）"""
        if name in self.virtual_coords:
            return self.virtual_coords[name]
        return LOCAL_COORDS[name]

    def _seg_length(self, wp1: str, wp2: str) -> float:
        """计算一个段的长度（圆弧或直线）"""
        if is_arc_segment(self.direction, wp1, wp2):
            return arc_segment_length(self.direction, wp1, wp2)
        p1 = self.get_coord(wp1)
        p2 = self.get_coord(wp2)
        return math.sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)

    def distance_to_end(self):
        """计算到终点的距离（不含排序边长度）"""
        if self.finished:
            return 0
        dist = 0
        # 当前段剩余
        if self.current_segment < len(self.path) - 1:
            wp1 = self.path[self.current_segment]
            wp2 = self.path[self.current_segment + 1]
            # 当前段在排序边上则不计入
            if not is_arc_segment(self.direction, wp1, wp2):
                seg_len = self._seg_length(wp1, wp2)
                dist += seg_len * (1 - self.segment_progress)
            # 后续段
            for i in range(self.current_segment + 1, len(self.path) - 1):
                wp1 = self.path[i]
                wp2 = self.path[i + 1]
                # 跳过排序边段的距离
                if is_arc_segment(self.direction, wp1, wp2):
                    continue
                dist += self._seg_length(wp1, wp2)
        return dist


# ============================================================
# 仿真引擎
# ============================================================

class SimulationEngine:
    def __init__(self):
        self.aircraft_list: List[Aircraft] = []
        self.sim_time: float = 0.0   # 仿真时间(秒)
        self.dt: float = 1.0         # 时间步长(秒)
        self.arc_entry_counter: Dict[str, int] = {}  # 每个融合点的排序编号
        self.running: bool = False
        self.landed_times: List[float] = []  # 落地时刻列表

    def reset(self):
        self.aircraft_list.clear()
        self.sim_time = 0.0
        self.arc_entry_counter.clear()
        self.running = False
        self.landed_times.clear()

    def add_aircraft(self, direction: Direction, entry_time: float, flight_id: str = ""):
        idx = len(self.aircraft_list) + 1
        if not flight_id:
            flight_id = f"{direction.value}{idx:03d}"
        path = list(FULL_PATHS[direction])
        ac = Aircraft(
            flight_id=flight_id,
            direction=direction,
            entry_time=entry_time,
            path=path,
        )
        # 设置初始高度
        if ARC_TYPE[direction] == "inner":
            ac.altitude = ALTITUDES["inner_arc"]
        else:
            ac.altitude = ALTITUDES["outer_arc"]
        # 设置初始位置
        start = LOCAL_COORDS[path[0]]
        ac.x, ac.y = start
        self.aircraft_list.append(ac)
        return ac

    def step(self):
        """执行一个仿真步"""
        self.sim_time += self.dt

        for ac in self.aircraft_list:
            if ac.finished:
                continue
            # 检查是否激活
            if not ac.active:
                if self.sim_time >= ac.entry_time:
                    ac.active = True
                    start = LOCAL_COORDS[ac.path[0]]
                    ac.x, ac.y = start
                    ac.actual_path.append((ac.x, ac.y))
                continue

            self._update_stage(ac)
            self._check_direct_flight(ac)
            self._check_separation(ac)
            self._move_aircraft(ac)
            ac.elapsed_time += self.dt

    def _update_stage(self, ac: Aircraft):
        """更新飞行阶段"""
        current_wp = ac.path[ac.current_segment]
        merge = MERGE_POINTS[ac.direction]
        sorting = SORTING_LEGS[ac.direction]

        if ac.stage == FlightStage.LANDED:
            return

        # 检查是否在排序边上
        if current_wp in sorting and ac.stage == FlightStage.APPROACHING:
            ac.stage = FlightStage.ON_ARC
            merge_key = merge
            if merge_key not in self.arc_entry_counter:
                self.arc_entry_counter[merge_key] = 0
            self.arc_entry_counter[merge_key] += 1
            ac.arc_entry_order = self.arc_entry_counter[merge_key]

        # 检查是否通过融合点
        if ac.current_segment < len(ac.path) - 1:
            next_wp = ac.path[ac.current_segment + 1] if ac.current_segment + 1 < len(ac.path) else None
            if current_wp == merge or (next_wp and ac.path[ac.current_segment] == merge):
                if ac.stage in (FlightStage.ON_ARC, FlightStage.DIRECT, FlightStage.APPROACHING):
                    ac.stage = FlightStage.STAGE2
                    ac.speed = SPEED_STAGE2

    def _effective_dist_to_end(self, ac: Aircraft) -> float:
        """计算等效到终点距离（跨扇区可比较的统一度量）
        对DIRECT/STAGE2: 使用实际distance_to_end
        对APPROACHING/ON_ARC: 计算假设直飞的距离(当前位置→融合点→...→RW29)
        """
        if ac.stage in (FlightStage.DIRECT, FlightStage.STAGE2, FlightStage.LANDED):
            return ac.distance_to_end()
        # APPROACHING 或 ON_ARC：计算假设直飞距离
        merge = MERGE_POINTS[ac.direction]
        mx, my = LOCAL_COORDS[merge]
        direct_to_merge = math.sqrt((ac.x - mx)**2 + (ac.y - my)**2)
        # 融合点到终点距离
        path = FULL_PATHS[ac.direction]
        merge_idx = None
        for i, wp in enumerate(path):
            if wp == merge:
                merge_idx = i
                break
        if merge_idx is None:
            return ac.distance_to_end()
        post_merge_dist = 0.0
        for i in range(merge_idx, len(path) - 1):
            p1 = LOCAL_COORDS[path[i]]
            p2 = LOCAL_COORDS[path[i + 1]]
            post_merge_dist += math.sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)
        return direct_to_merge + post_merge_dist

    def _check_direct_flight(self, ac: Aircraft):
        """检查直飞条件（跨扇区同步：同时检查南北两侧所有飞机）"""
        if ac.stage != FlightStage.ON_ARC:
            return

        merge = MERGE_POINTS[ac.direction]
        my_dist = self._effective_dist_to_end(ac)

        # 检查所有在途飞机（跨扇区），是否有飞机在 [my_dist-10, my_dist) 范围内
        for other in self.aircraft_list:
            if other is ac or other.finished or not other.active:
                continue
            if other.stage == FlightStage.APPROACHING:
                continue
            other_dist = self._effective_dist_to_end(other)
            # 前方飞机（更接近终点）距离差不足10km，不允许直飞
            if other_dist < my_dist and (my_dist - other_dist) < SEP_STAGE1:
                return

        self._set_direct(ac, merge)

    def _set_direct(self, ac: Aircraft, merge: str):
        """设置直飞路径（从飞机当前(x,y)位置直飞融合点）"""
        ac.stage = FlightStage.DIRECT
        merge_idx = None
        for i, wp in enumerate(ac.path):
            if wp == merge:
                merge_idx = i
                break
        if merge_idx is None:
            return
        # 用虚拟航点标记当前实际位置，支持从圆弧任意位置直飞
        virt_name = f"__DIRECT_{ac.flight_id}__"
        ac.virtual_coords[virt_name] = (ac.x, ac.y)
        ac.path = [virt_name, merge] + ac.path[merge_idx + 1:]
        ac.current_segment = 0
        ac.segment_progress = 0.0
        # 动态计算下降梯度：确保到达RW29时高度=430m
        remaining_dist = ac.distance_to_end()
        if remaining_dist > 0:
            ac.descent_gradient = (ac.altitude - ALTITUDES["airport"]) / (remaining_dist * 1000)
        else:
            ac.descent_gradient = DESCENT_GRADIENT_INNER if ARC_TYPE[ac.direction] == "inner" else DESCENT_GRADIENT_OUTER

    def _check_separation(self, ac: Aircraft):
        """检查间隔并处理冲突（基于到终点距离差，跨扇区统一判定）
        Stage1 (进近/排序/直飞): 到终点距离差 ≥ 10km
        Stage2 (汇聚航路): 到终点距离差 ≥ 6km
        """
        if ac.finished or not ac.active:
            return

        is_stage1 = ac.stage in (FlightStage.APPROACHING, FlightStage.ON_ARC, FlightStage.DIRECT)
        required_sep = SEP_STAGE1 if is_stage1 else SEP_STAGE2
        stage_speed = SPEED_STAGE1 if is_stage1 else SPEED_STAGE2
        min_speed = stage_speed * MIN_SPEED_RATIO

        my_dist = self._effective_dist_to_end(ac)
        need_slow = False

        for other in self.aircraft_list:
            if other is ac or other.finished or not other.active:
                continue

            other_dist = self._effective_dist_to_end(other)
            dist_diff = my_dist - other_dist

            # 仅检查前方飞机（比我更接近终点的）
            if dist_diff <= 0:
                continue

            # 到终点距离差不足间隔要求，后方飞机减速
            if dist_diff < required_sep:
                need_slow = True
                break

        if need_slow:
            # 匀减速：每秒减少 DECEL_RATE km/h
            ac.speed -= DECEL_RATE * self.dt
            ac.speed = max(ac.speed, min_speed)
            ac.is_slowed = True
        elif ac.is_slowed:
            # 间距满足要求，立即恢复当前阶段最大速度
            ac.speed = stage_speed
            ac.is_slowed = False

    def _move_aircraft(self, ac: Aircraft):
        """移动飞机（支持圆弧段和直线段）"""
        if ac.current_segment >= len(ac.path) - 1:
            ac.finished = True
            ac.stage = FlightStage.LANDED
            self.landed_times.append(self.sim_time)
            return

        p1_name = ac.path[ac.current_segment]
        p2_name = ac.path[ac.current_segment + 1]
        on_arc = is_arc_segment(ac.direction, p1_name, p2_name)

        # 计算段长（圆弧或直线）
        seg_len = ac._seg_length(p1_name, p2_name)
        if seg_len < 0.001:
            ac.current_segment += 1
            ac.segment_progress = 0.0
            return

        # 速度 km/h -> km/s
        speed_kms = ac.speed / 3600.0
        distance_step = speed_kms * self.dt
        progress_step = distance_step / seg_len

        ac.segment_progress += progress_step
        ac.total_distance += distance_step

        # 更新高度（仅在直飞/第二阶段下降，排序边保持高度）
        if ac.stage in (FlightStage.DIRECT, FlightStage.STAGE2) and ac.descent_gradient > 0:
            ac.altitude -= distance_step * ac.descent_gradient * 1000  # km->m
            ac.altitude = max(ac.altitude, ALTITUDES["airport"])

        if ac.segment_progress >= 1.0:
            overflow = ac.segment_progress - 1.0
            ac.current_segment += 1
            ac.segment_progress = 0.0

            if ac.current_segment >= len(ac.path) - 1:
                ac.finished = True
                ac.stage = FlightStage.LANDED
                end = ac.get_coord(ac.path[-1])
                ac.x, ac.y = end
                ac.actual_path.append((ac.x, ac.y))
                self.landed_times.append(self.sim_time)
                return

            # 检查是否通过融合点
            merge = MERGE_POINTS[ac.direction]
            if ac.path[ac.current_segment] == merge:
                ac.stage = FlightStage.STAGE2
                ac.speed = SPEED_STAGE2
                ac.is_slowed = False

            ac.segment_progress = overflow

        # 更新位置
        if ac.current_segment < len(ac.path) - 1:
            cur_p1 = ac.path[ac.current_segment]
            cur_p2 = ac.path[ac.current_segment + 1]
            if is_arc_segment(ac.direction, cur_p1, cur_p2):
                # 沿圆弧插值
                ac.x, ac.y = arc_interp(ac.direction, cur_p1, cur_p2, ac.segment_progress)
            else:
                # 直线插值
                p1 = ac.get_coord(cur_p1)
                p2 = ac.get_coord(cur_p2)
                ac.x = p1[0] + (p2[0] - p1[0]) * ac.segment_progress
                ac.y = p1[1] + (p2[1] - p1[1]) * ac.segment_progress
        else:
            end = ac.get_coord(ac.path[-1])
            ac.x, ac.y = end

        ac.actual_path.append((ac.x, ac.y))

    def get_hourly_capacity(self):
        """计算一小时最大容量"""
        if not self.landed_times and not any(ac.active for ac in self.aircraft_list):
            return 0

        # 统计当前时刻的状态
        landed_count = sum(1 for ac in self.aircraft_list if ac.finished)
        in_flight = sum(1 for ac in self.aircraft_list if ac.active and not ac.finished)

        # 如果仿真时间不足1小时，直接返回当前数据
        one_hour = 3600.0
        if self.sim_time <= one_hour:
            return landed_count + in_flight

        # 滑动窗口：找最大的1小时容量
        max_cap = 0
        window_start = 0
        while window_start <= self.sim_time - one_hour:
            window_end = window_start + one_hour
            # 在窗口内落地的
            landed_in_window = sum(1 for t in self.landed_times if window_start <= t <= window_end)
            # 在窗口结束时仍在飞的
            in_flight_at_end = 0
            for ac in self.aircraft_list:
                if ac.entry_time <= window_end and not (ac.finished and any(t <= window_end for t in self.landed_times if t == self._get_land_time(ac))):
                    if ac.entry_time <= window_end:
                        in_flight_at_end += 1
            cap = landed_in_window + in_flight_at_end
            max_cap = max(max_cap, cap)
            window_start += 60  # 每分钟滑动
        return max_cap

    def _get_land_time(self, ac):
        if ac.finished:
            return ac.entry_time + ac.elapsed_time
        return float('inf')


# ============================================================
# Tkinter 仿真界面
# ============================================================

class SimulationGUI:
    # 画布参数
    CANVAS_W = 900
    CANVAS_H = 700
    MARGIN = 60
    BG_COLOR = "#f5f7fa"
    GRID_COLOR = "#e0e4e8"

    # 路径颜色
    COLOR_OUTER_ARC = "#1976D2"   # 蓝色 - 外排序边
    COLOR_INNER_ARC = "#2E7D32"   # 绿色 - 内排序边
    COLOR_DIRECT = "#43A047"      # 浅绿 - 直飞
    COLOR_FINAL = "#E65100"       # 橙色 - 最终进近
    COLOR_STAGE2 = "#7B1FA2"      # 紫色 - 第二阶段
    COLOR_WAYPOINT = "#333333"
    COLOR_AIRCRAFT_S1 = "#0277BD" # 第一阶段飞机
    COLOR_AIRCRAFT_S2 = "#8E24AA" # 第二阶段飞机
    COLOR_AIRCRAFT_SLOW = "#D32F2F" # 减速飞机
    COLOR_MERGE = "#E64A19"       # 融合点

    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("宜宾五粮液机场RW29点融合程序仿真系统")
        self.root.geometry("1400x820")
        self.root.configure(bg="#f0f2f5")
        self.root.resizable(True, True)

        self.engine = SimulationEngine()
        self.speed_multiplier = 1
        self.is_paused = False
        self.animation_id = None

        # 计算坐标范围
        self._calc_bounds()

        self._build_ui()
        self._draw_static()

    def _calc_bounds(self):
        xs = [c[0] for c in LOCAL_COORDS.values()]
        ys = [c[1] for c in LOCAL_COORDS.values()]
        pad = 5
        self.x_min, self.x_max = min(xs) - pad, max(xs) + pad
        self.y_min, self.y_max = min(ys) - pad, max(ys) + pad

    def _world_to_canvas(self, wx, wy):
        """世界坐标(km) -> 画布像素"""
        cx = self.MARGIN + (wx - self.x_min) / (self.x_max - self.x_min) * (self.CANVAS_W - 2 * self.MARGIN)
        cy = self.CANVAS_H - self.MARGIN - (wy - self.y_min) / (self.y_max - self.y_min) * (self.CANVAS_H - 2 * self.MARGIN)
        return cx, cy

    # ---- UI构建 ----
    def _build_ui(self):
        style = ttk.Style()
        style.theme_use("clam")
        style.configure("Dark.TFrame", background="#f0f2f5")
        style.configure("Dark.TLabel", background="#f0f2f5", foreground="#333333", font=("Microsoft YaHei", 9))
        style.configure("Title.TLabel", background="#f0f2f5", foreground="#1565C0", font=("Microsoft YaHei", 11, "bold"))
        style.configure("Stat.TLabel", background="#ffffff", foreground="#E65100", font=("Microsoft YaHei", 12, "bold"))
        style.configure("Dark.TButton", font=("Microsoft YaHei", 9))
        style.configure("Dark.TLabelframe", background="#f0f2f5", foreground="#1565C0")
        style.configure("Dark.TLabelframe.Label", background="#f0f2f5", foreground="#1565C0", font=("Microsoft YaHei", 10, "bold"))

        # 顶部工具栏
        toolbar = tk.Frame(self.root, bg="#ffffff", height=45, relief="groove", bd=1)
        toolbar.pack(fill=tk.X, padx=5, pady=(5,2))
        toolbar.pack_propagate(False)

        btn_cfg = {"bg": "#e3f2fd", "fg": "#1565C0", "activebackground": "#1976D2",
                   "activeforeground": "#fff", "font": ("Microsoft YaHei", 9, "bold"),
                   "relief": "flat", "padx": 10, "pady": 3, "cursor": "hand2"}

        tk.Button(toolbar, text="📂 加载航班", command=self._load_flights, **btn_cfg).pack(side=tk.LEFT, padx=5, pady=6)
        tk.Button(toolbar, text="➕ 添加航班", command=self._add_flight_dialog, **btn_cfg).pack(side=tk.LEFT, padx=5, pady=6)
        tk.Button(toolbar, text="▶ 开始仿真", command=self._start_sim, **btn_cfg).pack(side=tk.LEFT, padx=5, pady=6)
        tk.Button(toolbar, text="⏸ 暂停", command=self._pause_sim, **btn_cfg).pack(side=tk.LEFT, padx=5, pady=6)
        tk.Button(toolbar, text="🔄 重置", command=self._reset_sim, **btn_cfg).pack(side=tk.LEFT, padx=5, pady=6)
        tk.Button(toolbar, text="💾 导出结果", command=self._export_results, **btn_cfg).pack(side=tk.LEFT, padx=5, pady=6)
        tk.Button(toolbar, text="📈 轨迹总览", command=self._show_trajectory_plot, **btn_cfg).pack(side=tk.LEFT, padx=5, pady=6)

        # 速度调节
        tk.Label(toolbar, text="仿真速度:", bg="#ffffff", fg="#555", font=("Microsoft YaHei", 9)).pack(side=tk.LEFT, padx=(20, 5), pady=6)
        self.speed_var = tk.IntVar(value=1)
        speed_scale = tk.Scale(toolbar, from_=1, to=100, orient=tk.HORIZONTAL, variable=self.speed_var,
                               bg="#ffffff", fg="#1565C0", troughcolor="#e0e4e8", highlightthickness=0,
                               length=150, command=self._on_speed_change)
        speed_scale.pack(side=tk.LEFT, padx=5, pady=6)
        self.speed_label = tk.Label(toolbar, text="x1", bg="#ffffff", fg="#E65100", font=("Microsoft YaHei", 9, "bold"))
        self.speed_label.pack(side=tk.LEFT, padx=5, pady=6)

        # 主体
        main = tk.Frame(self.root, bg="#f0f2f5")
        main.pack(fill=tk.BOTH, expand=True, padx=5, pady=2)

        # 左侧：画布
        left = tk.Frame(main, bg="#f0f2f5")
        left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.canvas = tk.Canvas(left, width=self.CANVAS_W, height=self.CANVAS_H,
                                bg=self.BG_COLOR, highlightthickness=1, highlightbackground="#90CAF9")
        self.canvas.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
        self.canvas.bind("<Configure>", self._on_canvas_resize)
        self.canvas.bind("<Motion>", self._on_mouse_move)

        # 悬浮信息
        self.tooltip = tk.Label(self.canvas, text="", bg="#ffffff", fg="#333333",
                                font=("Consolas", 8), relief="solid", bd=1)

        # 右侧面板
        right = tk.Frame(main, bg="#f0f2f5", width=420)
        right.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,5))
        right.pack_propagate(False)

        # 统计信息
        stat_frame = tk.LabelFrame(right, text=" 📊 统计信息 ", bg="#ffffff", fg="#1565C0",
                                   font=("Microsoft YaHei", 10, "bold"), labelanchor="n")
        stat_frame.pack(fill=tk.X, padx=5, pady=5)

        self.stat_labels = {}
        stats = [("sim_time", "仿真时间"), ("total", "航班总数"), ("active", "在途数量"),
                 ("landed", "已落地"), ("capacity", "1h最大容量"), ("avg_time", "平均飞行时间"),
                 ("avg_dist", "平均飞行距离")]
        for i, (key, label) in enumerate(stats):
            tk.Label(stat_frame, text=f"{label}:", bg="#ffffff", fg="#555555",
                     font=("Microsoft YaHei", 9), anchor="w").grid(row=i, column=0, sticky="w", padx=10, pady=2)
            val = tk.Label(stat_frame, text="--", bg="#ffffff", fg="#E65100",
                           font=("Consolas", 10, "bold"), anchor="e")
            val.grid(row=i, column=1, sticky="e", padx=10, pady=2)
            self.stat_labels[key] = val

        stat_frame.columnconfigure(1, weight=1)

        # 图例
        legend_frame = tk.LabelFrame(right, text=" 🎨 图例 ", bg="#ffffff", fg="#1565C0",
                                     font=("Microsoft YaHei", 10, "bold"), labelanchor="n")
        legend_frame.pack(fill=tk.X, padx=5, pady=5)
        legends = [
            (self.COLOR_OUTER_ARC, "外排序边 (3000m)"),
            (self.COLOR_INNER_ARC, "内排序边 (3600m)"),
            (self.COLOR_DIRECT, "直飞路径"),
            (self.COLOR_FINAL, "最终进近"),
            (self.COLOR_STAGE2, "第二阶段"),
            (self.COLOR_MERGE, "融合点 (YB616/YB618)"),
        ]
        for i, (color, text) in enumerate(legends):
            c = tk.Canvas(legend_frame, width=20, height=12, bg="#ffffff", highlightthickness=0)
            c.create_rectangle(2, 2, 18, 10, fill=color, outline="")
            c.grid(row=i, column=0, padx=(10,5), pady=2)
            tk.Label(legend_frame, text=text, bg="#ffffff", fg="#333333",
                     font=("Microsoft YaHei", 8)).grid(row=i, column=1, sticky="w", pady=2)

        # 航班列表
        flight_frame = tk.LabelFrame(right, text=" ✈ 航班列表 ", bg="#ffffff", fg="#1565C0",
                                     font=("Microsoft YaHei", 10, "bold"), labelanchor="n")
        flight_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        cols = ("id", "dir", "stage", "speed", "alt", "dist")
        self.flight_tree = ttk.Treeview(flight_frame, columns=cols, show="headings", height=15)
        self.flight_tree.heading("id", text="编号")
        self.flight_tree.heading("dir", text="方向")
        self.flight_tree.heading("stage", text="阶段")
        self.flight_tree.heading("speed", text="速度km/h")
        self.flight_tree.heading("alt", text="高度m")
        self.flight_tree.heading("dist", text="到终点km")
        for col in cols:
            self.flight_tree.column(col, width=65, anchor="center")
        self.flight_tree.column("id", width=75)

        scrollbar = ttk.Scrollbar(flight_frame, orient=tk.VERTICAL, command=self.flight_tree.yview)
        self.flight_tree.configure(yscrollcommand=scrollbar.set)
        self.flight_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        # 底部状态栏
        status_bar = tk.Frame(self.root, bg="#e8eaf0", height=28, relief="groove", bd=1)
        status_bar.pack(fill=tk.X, padx=5, pady=(2,5))
        status_bar.pack_propagate(False)
        self.status_label = tk.Label(status_bar, text="就绪 | 请加载或添加航班数据后开始仿真",
                                     bg="#e8eaf0", fg="#555555", font=("Microsoft YaHei", 9), anchor="w")
        self.status_label.pack(fill=tk.X, padx=10, pady=3)

    def _on_canvas_resize(self, event):
        self.CANVAS_W = event.width
        self.CANVAS_H = event.height
        self._draw_static()

    def _on_speed_change(self, val):
        self.speed_multiplier = int(val)
        self.speed_label.config(text=f"x{self.speed_multiplier}")

    # ---- 静态绘制 ----
    def _draw_static(self):
        self.canvas.delete("static")
        self._draw_grid()
        self._draw_paths()
        self._draw_waypoints()

    def _draw_grid(self):
        """绘制坐标网格"""
        step = 10  # km
        x = math.ceil(self.x_min / step) * step
        while x <= self.x_max:
            cx, _ = self._world_to_canvas(x, 0)
            self.canvas.create_line(cx, self.MARGIN, cx, self.CANVAS_H - self.MARGIN,
                                    fill=self.GRID_COLOR, dash=(2, 4), tags="static")
            _, cy = self._world_to_canvas(0, 0)
            self.canvas.create_text(cx, self.CANVAS_H - self.MARGIN + 15, text=f"{x:.0f}",
                                    fill="#999999", font=("Consolas", 7), tags="static")
            x += step
        y = math.ceil(self.y_min / step) * step
        while y <= self.y_max:
            _, cy = self._world_to_canvas(0, y)
            self.canvas.create_line(self.MARGIN, cy, self.CANVAS_W - self.MARGIN, cy,
                                    fill=self.GRID_COLOR, dash=(2, 4), tags="static")
            self.canvas.create_text(self.MARGIN - 20, cy, text=f"{y:.0f}",
                                    fill="#999999", font=("Consolas", 7), tags="static")
            y += step

        # 坐标轴标签
        self.canvas.create_text(self.CANVAS_W // 2, self.CANVAS_H - 10,
                                text="East Distance (km)", fill="#666666", font=("Consolas", 8), tags="static")
        self.canvas.create_text(15, self.CANVAS_H // 2,
                                text="North (km)", fill="#666666", font=("Consolas", 8), angle=90, tags="static")

    def _draw_paths(self):
        """绘制飞行路径（排序边为圆弧）"""
        for direction, path in FULL_PATHS.items():
            sorting = SORTING_LEGS[direction]
            merge = MERGE_POINTS[direction]
            arc_type = ARC_TYPE[direction]

            for i in range(len(path) - 1):
                wp1, wp2 = path[i], path[i + 1]
                if wp1 not in LOCAL_COORDS or wp2 not in LOCAL_COORDS:
                    continue

                # 确定颜色
                on_arc = is_arc_segment(direction, wp1, wp2)
                if on_arc:
                    color = self.COLOR_INNER_ARC if arc_type == "inner" else self.COLOR_OUTER_ARC
                    width = 2
                    dash = ()
                elif wp1 == merge or wp2 == "RW29":
                    color = self.COLOR_FINAL
                    width = 2
                    dash = ()
                else:
                    color = self.COLOR_DIRECT
                    width = 1
                    dash = (4, 3)

                if on_arc:
                    # 绘制圆弧：用折线逼近
                    pts, _, _, _, _ = generate_arc_points(
                        ARC_CENTER[direction][0], wp1, wp2, n_points=30)
                    canvas_pts = []
                    for px, py in pts:
                        canvas_pts.extend(self._world_to_canvas(px, py))
                    if len(canvas_pts) >= 4:
                        self.canvas.create_line(*canvas_pts, fill=color, width=width,
                                                smooth=True, tags="static")
                else:
                    x1, y1 = self._world_to_canvas(*LOCAL_COORDS[wp1])
                    x2, y2 = self._world_to_canvas(*LOCAL_COORDS[wp2])
                    self.canvas.create_line(x1, y1, x2, y2, fill=color, width=width,
                                            dash=dash, tags="static")

    def _draw_waypoints(self):
        """绘制航路点"""
        for name, (wx, wy) in LOCAL_COORDS.items():
            cx, cy = self._world_to_canvas(wx, wy)
            r = 4
            if name in ("YB618", "YB616"):
                color = self.COLOR_MERGE
                r = 6
                self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill=color, outline="#333", width=1, tags="static")
            elif name == "RW29":
                color = "#D32F2F"
                r = 7
                self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill=color, outline="#333", width=2, tags="static")
            elif name in ("KAKMI", "VEDPO", "OTGIN", "IGUNI"):
                color = "#F9A825"
                r = 5
                self.canvas.create_rectangle(cx-r, cy-r, cx+r, cy+r, fill=color, outline="#333", width=1, tags="static")
            elif name == "YB614":
                color = self.COLOR_FINAL
                r = 5
                self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill=color, outline="#333", width=1, tags="static")
            else:
                color = "#78909c"
                r = 3
                self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill=color, outline="#555", tags="static")

            # 标签
            if name in ("RW29", "YB618", "YB616", "YB614", "KAKMI", "VEDPO", "OTGIN", "IGUNI",
                         "S1", "S3", "S5", "S6", "S7", "S8", "S9", "S10", "S11",
                         "N1", "N2", "N3", "N4", "N9"):
                offset_x, offset_y = 12, -10
                self.canvas.create_text(cx + offset_x, cy + offset_y, text=name,
                                        fill=color, font=("Consolas", 7, "bold"), anchor="w", tags="static")

    # ---- 鼠标悬浮 ----
    def _on_mouse_move(self, event):
        for name, (wx, wy) in LOCAL_COORDS.items():
            cx, cy = self._world_to_canvas(wx, wy)
            if abs(event.x - cx) < 8 and abs(event.y - cy) < 8:
                lat, lon = WAYPOINTS.get(name, (0, 0))
                self.tooltip.config(text=f" {name}\n Lat:{lat:.4f} Lon:{lon:.4f}\n Local:({wx:.1f},{wy:.1f})km ")
                self.tooltip.place(x=event.x + 15, y=event.y - 30)
                return
        # 检查飞机
        for ac in self.engine.aircraft_list:
            if ac.active and not ac.finished:
                cx, cy = self._world_to_canvas(ac.x, ac.y)
                if abs(event.x - cx) < 10 and abs(event.y - cy) < 10:
                    stage_name = {FlightStage.APPROACHING: "进近", FlightStage.ON_ARC: "排序边",
                                  FlightStage.DIRECT: "直飞", FlightStage.STAGE2: "第二阶段",
                                  FlightStage.LANDED: "已落地"}
                    self.tooltip.config(text=f" {ac.flight_id}\n 阶段:{stage_name[ac.stage]}\n"
                                            f" 速度:{ac.speed:.0f}km/h\n 高度:{ac.altitude:.0f}m\n"
                                            f" 距终点:{ac.distance_to_end():.1f}km ")
                    self.tooltip.place(x=event.x + 15, y=event.y - 50)
                    return
        self.tooltip.place_forget()

    # ---- 动态绘制 ----
    def _draw_aircraft(self):
        self.canvas.delete("aircraft")
        self.canvas.delete("trail")
        for ac in self.engine.aircraft_list:
            if not ac.active or ac.finished:
                continue
            cx, cy = self._world_to_canvas(ac.x, ac.y)

            # 轨迹
            if len(ac.actual_path) > 1:
                pts = []
                step = max(1, len(ac.actual_path) // 100)
                for i in range(0, len(ac.actual_path), step):
                    px, py = self._world_to_canvas(*ac.actual_path[i])
                    pts.extend([px, py])
                if len(pts) >= 4:
                    trail_color = self.COLOR_AIRCRAFT_S2 if ac.stage == FlightStage.STAGE2 else self.COLOR_AIRCRAFT_S1
                    self.canvas.create_line(*pts, fill=trail_color, width=1, dash=(2,2), tags="trail")

            # 飞机图标（三角形）
            if ac.is_slowed:
                color = self.COLOR_AIRCRAFT_SLOW
            elif ac.stage == FlightStage.STAGE2:
                color = self.COLOR_AIRCRAFT_S2
            else:
                color = self.COLOR_AIRCRAFT_S1

            s = 6
            # 计算朝向
            if ac.current_segment < len(ac.path) - 1:
                next_name = ac.path[ac.current_segment + 1]
                if is_arc_segment(ac.direction, ac.path[ac.current_segment], next_name):
                    # 圆弧上：朝向为切线方向
                    cn, cr = ARC_CENTER[ac.direction]
                    ccx, ccy = LOCAL_COORDS[cn]
                    dx_r = ac.x - ccx
                    dy_r = ac.y - ccy
                    # 判断圆弧方向
                    p1c = LOCAL_COORDS[ac.path[ac.current_segment]]
                    p2c = LOCAL_COORDS[next_name]
                    a1 = math.atan2(p1c[1]-ccy, p1c[0]-ccx)
                    a2 = math.atan2(p2c[1]-ccy, p2c[0]-ccx)
                    diff = a2 - a1
                    if diff > math.pi: diff -= 2*math.pi
                    elif diff < -math.pi: diff += 2*math.pi
                    sign = 1 if diff > 0 else -1
                    dx = -dy_r * sign
                    dy = dx_r * sign
                else:
                    p2 = ac.get_coord(next_name)
                    dx = p2[0] - ac.x
                    dy = p2[1] - ac.y
                angle = math.atan2(dy, dx)
            else:
                angle = 0

            pts = [
                cx + s * math.cos(angle), cy - s * math.sin(angle),
                cx + s * math.cos(angle + 2.5), cy - s * math.sin(angle + 2.5),
                cx + s * math.cos(angle - 2.5), cy - s * math.sin(angle - 2.5),
            ]
            self.canvas.create_polygon(*pts, fill=color, outline="#fff", width=1, tags="aircraft")

            # 标签
            self.canvas.create_text(cx + 10, cy - 10, text=ac.flight_id,
                                    fill=color, font=("Consolas", 7), anchor="w", tags="aircraft")

    def _update_stats(self):
        landed = sum(1 for ac in self.engine.aircraft_list if ac.finished)
        active = sum(1 for ac in self.engine.aircraft_list if ac.active and not ac.finished)
        total = len(self.engine.aircraft_list)

        hours = int(self.engine.sim_time // 3600)
        mins = int((self.engine.sim_time % 3600) // 60)
        secs = int(self.engine.sim_time % 60)

        self.stat_labels["sim_time"].config(text=f"{hours:02d}:{mins:02d}:{secs:02d}")
        self.stat_labels["total"].config(text=str(total))
        self.stat_labels["active"].config(text=str(active))
        self.stat_labels["landed"].config(text=str(landed))

        # 容量
        cap = self.engine.get_hourly_capacity()
        self.stat_labels["capacity"].config(text=str(cap))

        # 平均值
        finished = [ac for ac in self.engine.aircraft_list if ac.finished]
        if finished:
            avg_t = sum(ac.elapsed_time for ac in finished) / len(finished)
            avg_d = sum(ac.total_distance for ac in finished) / len(finished)
            self.stat_labels["avg_time"].config(text=f"{avg_t/60:.1f} min")
            self.stat_labels["avg_dist"].config(text=f"{avg_d:.1f} km")
        else:
            self.stat_labels["avg_time"].config(text="--")
            self.stat_labels["avg_dist"].config(text="--")

    def _update_flight_list(self):
        for item in self.flight_tree.get_children():
            self.flight_tree.delete(item)

        stage_map = {FlightStage.APPROACHING: "进近", FlightStage.ON_ARC: "排序边",
                     FlightStage.DIRECT: "直飞", FlightStage.STAGE2: "第二阶段",
                     FlightStage.LANDED: "已落地"}

        for ac in self.engine.aircraft_list:
            if not ac.active:
                continue
            self.flight_tree.insert("", tk.END, values=(
                ac.flight_id,
                ac.direction.value,
                stage_map[ac.stage],
                f"{ac.speed:.0f}",
                f"{ac.altitude:.0f}",
                f"{ac.distance_to_end():.1f}" if not ac.finished else "0.0"
            ))

    # ---- 仿真控制 ----
    def _start_sim(self):
        if not self.engine.aircraft_list:
            messagebox.showwarning("提示", "请先添加或加载航班数据！")
            return
        self.engine.running = True
        self.is_paused = False
        self.status_label.config(text="仿真运行中...")
        self._run_loop()

    def _pause_sim(self):
        self.is_paused = not self.is_paused
        if self.is_paused:
            self.status_label.config(text="仿真已暂停")
        else:
            self.status_label.config(text="仿真运行中...")
            self._run_loop()

    def _reset_sim(self):
        if self.animation_id:
            self.root.after_cancel(self.animation_id)
            self.animation_id = None
        self.engine.reset()
        self.canvas.delete("aircraft")
        self.canvas.delete("trail")
        self._update_stats()
        self._update_flight_list()
        self._draw_static()
        self.status_label.config(text="已重置 | 请加载或添加航班数据后开始仿真")

    def _run_loop(self):
        if not self.engine.running or self.is_paused:
            return

        for _ in range(self.speed_multiplier):
            self.engine.step()

        self._draw_aircraft()
        self._update_stats()

        # 每10帧刷新列表（减少开销）
        if int(self.engine.sim_time) % 10 == 0:
            self._update_flight_list()

        # 检查是否全部完成
        all_done = all(ac.finished for ac in self.engine.aircraft_list) and \
                   self.engine.sim_time > max(ac.entry_time for ac in self.engine.aircraft_list)
        if all_done:
            self.engine.running = False
            self._update_flight_list()
            self.status_label.config(text=f"仿真完成 | 总时间: {self.engine.sim_time/60:.1f}分钟")
            messagebox.showinfo("仿真完成", f"所有航班已落地！\n总仿真时间: {self.engine.sim_time/60:.1f} 分钟")
            return

        self.animation_id = self.root.after(30, self._run_loop)

    # ---- 航班数据 ----
    def _add_flight_dialog(self):
        dialog = tk.Toplevel(self.root)
        dialog.title("添加航班")
        dialog.geometry("350x250")
        dialog.configure(bg="#f0f2f5")
        dialog.transient(self.root)
        dialog.grab_set()

        tk.Label(dialog, text="航班编号:", bg="#f0f2f5", fg="#333",
                 font=("Microsoft YaHei", 9)).grid(row=0, column=0, padx=15, pady=10, sticky="w")
        id_entry = tk.Entry(dialog, bg="#ffffff", fg="#333", insertbackground="#333", width=20)
        id_entry.grid(row=0, column=1, padx=10, pady=10)

        tk.Label(dialog, text="进入方向:", bg="#f0f2f5", fg="#333",
                 font=("Microsoft YaHei", 9)).grid(row=1, column=0, padx=15, pady=10, sticky="w")
        dir_var = tk.StringVar(value="KAKMI")
        dir_combo = ttk.Combobox(dialog, textvariable=dir_var, values=["KAKMI", "VEDPO", "OTGIN", "IGUNI"],
                                 state="readonly", width=17)
        dir_combo.grid(row=1, column=1, padx=10, pady=10)

        tk.Label(dialog, text="进入时间(秒):", bg="#f0f2f5", fg="#333",
                 font=("Microsoft YaHei", 9)).grid(row=2, column=0, padx=15, pady=10, sticky="w")
        time_entry = tk.Entry(dialog, bg="#ffffff", fg="#333", insertbackground="#333", width=20)
        time_entry.insert(0, "0")
        time_entry.grid(row=2, column=1, padx=10, pady=10)

        def confirm():
            fid = id_entry.get().strip()
            direction = Direction(dir_var.get())
            try:
                entry_t = float(time_entry.get().strip())
            except ValueError:
                messagebox.showerror("错误", "请输入有效的时间（秒）")
                return
            self.engine.add_aircraft(direction, entry_t, fid)
            self._update_flight_list()
            self._update_stats()
            self.status_label.config(text=f"已添加航班 {fid or direction.value} | 共{len(self.engine.aircraft_list)}架")
            dialog.destroy()

        tk.Button(dialog, text="确认添加", command=confirm,
                  bg="#1976D2", fg="#fff", font=("Microsoft YaHei", 10, "bold"),
                  relief="flat", padx=20, pady=5, cursor="hand2").grid(row=3, column=0, columnspan=2, pady=20)

    def _load_flights(self):
        """从CSV或JSON加载航班数据"""
        filepath = filedialog.askopenfilename(
            title="选择航班数据文件",
            filetypes=[("CSV文件", "*.csv"), ("JSON文件", "*.json"), ("所有文件", "*.*")]
        )
        if not filepath:
            return

        try:
            if filepath.endswith(".json"):
                with open(filepath, "r", encoding="utf-8") as f:
                    data = json.load(f)
                for item in data:
                    self.engine.add_aircraft(
                        Direction(item["direction"]),
                        float(item["entry_time"]),
                        item.get("flight_id", "")
                    )
            elif filepath.endswith(".csv"):
                with open(filepath, "r", encoding="utf-8") as f:
                    reader = csv.DictReader(f)
                    for row in reader:
                        self.engine.add_aircraft(
                            Direction(row["direction"]),
                            float(row["entry_time"]),
                            row.get("flight_id", "")
                        )

            self._update_flight_list()
            self._update_stats()
            n = len(self.engine.aircraft_list)
            self.status_label.config(text=f"已加载 {n} 架航班 | 点击'开始仿真'运行")
            messagebox.showinfo("加载成功", f"成功加载 {n} 架航班")
        except Exception as e:
            messagebox.showerror("加载失败", str(e))

    def _show_trajectory_plot(self):
        """弹出matplotlib轨迹总览图"""
        win = tk.Toplevel(self.root)
        win.title("Trajectory Overview")
        win.geometry("1100x800")
        win.configure(bg="#ffffff")

        fig, ax = plt.subplots(1, 1, figsize=(12, 9))
        fig.patch.set_facecolor('#ffffff')
        ax.set_facecolor('#fafafa')

        C_OUTER = '#2196F3'
        C_INNER = '#4CAF50'
        C_FINAL = '#FF9800'
        C_DECEL = '#9C27B0'

        def _arc_plot(center_name, wp_start, wp_end, color, label):
            """绘制圆弧排序边"""
            pts, _, _, _, _ = generate_arc_points(center_name, wp_start, wp_end, n_points=80)
            xs = [p[0] for p in pts]
            ys = [p[1] for p in pts]
            ax.plot(xs, ys, color=color, linewidth=3, solid_capstyle='round', label=label, zorder=3)

        # 排序边（圆弧）
        _arc_plot("YB618", "S1", "S5", C_OUTER, 'Outer Arc (S1-S5, 3000m)')
        _arc_plot("YB618", "S9", "S6", C_INNER, 'Inner Arc (S9-S6, 3600m)')
        _arc_plot("YB616", "IGUNI", "N4", C_OUTER, 'Outer Arc (IGUNI-N4, 3000m)')

        # 直飞虚线（径向线）
        yb618 = LOCAL_COORDS["YB618"]
        yb616 = LOCAL_COORDS["YB616"]
        for wp in ["S1","S2","S3","S4","S5"]:
            p = LOCAL_COORDS[wp]
            ax.plot([p[0], yb618[0]], [p[1], yb618[1]], color=C_OUTER, lw=0.8, ls='--', alpha=0.5, zorder=2)
        for wp in ["S9","S8","S7","S6"]:
            p = LOCAL_COORDS[wp]
            ax.plot([p[0], yb618[0]], [p[1], yb618[1]], color=C_INNER, lw=0.8, ls='--', alpha=0.5, zorder=2)
        for wp in ["IGUNI","N1","N2","N3","N4"]:
            p = LOCAL_COORDS[wp]
            ax.plot([p[0], yb616[0]], [p[1], yb616[1]], color=C_OUTER, lw=0.8, ls='--', alpha=0.5, zorder=2)

        # 最终进近
        ax.plot([yb618[0], LOCAL_COORDS["YB614"][0], LOCAL_COORDS["RW29"][0]],
                [yb618[1], LOCAL_COORDS["YB614"][1], LOCAL_COORDS["RW29"][1]],
                color=C_FINAL, lw=2.5, label='Final Approach (South)', zorder=4)
        ax.plot([yb616[0], LOCAL_COORDS["N9"][0], LOCAL_COORDS["YB614"][0]],
                [yb616[1], LOCAL_COORDS["N9"][1], LOCAL_COORDS["YB614"][1]],
                color=C_FINAL, lw=2.5, label='Final Approach (North)', zorder=4)

        # 入口连接线（IGUNI已在弧上，不画入口连接线）
        for pts, c in [
            (["KAKMI","S10","S9"], C_INNER),
            (["VEDPO","S11","S7"], C_INNER),
            (["OTGIN","S1"], C_OUTER),
        ]:
            xs = [LOCAL_COORDS[p][0] for p in pts]
            ys = [LOCAL_COORDS[p][1] for p in pts]
            ax.plot(xs, ys, color=c, lw=1.5, ls='-.', alpha=0.7, zorder=2)

        # 减速圆
        cx_d, cy_d = LOCAL_COORDS["YB614"]
        circle = plt.Circle((cx_d, cy_d), 5.6, fill=False, color=C_DECEL, lw=1.5, ls=':', alpha=0.7, zorder=3)
        ax.add_patch(circle)

        # 航路点标记
        for wp_name in ["S1","S2","S3","S4","S5"]:
            x, y = LOCAL_COORDS[wp_name]
            ax.plot(x, y, 'o', color=C_OUTER, ms=5, mec='white', mew=0.5, zorder=5)
        for wp_name in ["S9","S8","S7","S6"]:
            x, y = LOCAL_COORDS[wp_name]
            ax.plot(x, y, 'o', color=C_INNER, ms=5, mec='white', mew=0.5, zorder=5)
        for wp_name in ["IGUNI","N1","N2","N3","N4"]:
            x, y = LOCAL_COORDS[wp_name]
            if wp_name == "IGUNI":
                continue  # drawn as entry point
            ax.plot(x, y, 'o', color=C_OUTER, ms=5, mec='white', mew=0.5, zorder=5)
        for name in ["S1","S5","S6","S9","S7","N1","N4","N2","S3"]:
            x, y = LOCAL_COORDS[name]
            c = C_INNER if name in ("S6","S7","S8","S9") else C_OUTER
            ax.annotate(name, (x,y), textcoords="offset points", xytext=(-2,1.5) if name not in ("S1","N1") else (1,1.5),
                        fontsize=7, color=c, fontweight='bold', zorder=6)

        # 融合点
        for name in ["YB618", "YB616"]:
            x, y = LOCAL_COORDS[name]
            ax.plot(x, y, 's', color=C_FINAL, ms=9, mec='white', mew=1.5, zorder=6)
            ax.annotate(f'{name}\n(Merge)', (x,y), textcoords="offset points", xytext=(8,5),
                        fontsize=8, color='#E65100', fontweight='bold', zorder=6)

        # YB614, N9
        for name in ["YB614", "N9"]:
            x, y = LOCAL_COORDS[name]
            ax.plot(x, y, 'D' if name=="YB614" else 'o', color=C_FINAL, ms=6, mec='white', mew=0.8, zorder=6)
            ax.annotate(name, (x,y), textcoords="offset points", xytext=(6,4), fontsize=7, color=C_FINAL, zorder=6)

        # RW29
        x, y = LOCAL_COORDS["RW29"]
        ax.plot(x, y, '*', color='#F44336', ms=15, mec='white', mew=1, zorder=7)
        ax.annotate('RW29\n(Airport)', (x,y), textcoords="offset points", xytext=(10,5),
                    fontsize=9, color='#F44336', fontweight='bold', zorder=7)

        # 入口点
        for name in ["KAKMI","VEDPO","OTGIN","IGUNI"]:
            x, y = LOCAL_COORDS[name]
            ax.plot(x, y, '^', color='#FFC107', ms=9, mec='#333', mew=0.8, zorder=6)
            ha = 'right' if name in ('KAKMI','VEDPO') else 'left'
            off = (-8,3) if name in ('KAKMI',) else (-8,-8) if name=='VEDPO' else (8,3)
            ax.annotate(name, (x,y), textcoords="offset points", xytext=off, fontsize=8,
                        color='#F57F17', fontweight='bold', ha=ha, zorder=6)

        # S10, S11
        for name in ["S10","S11"]:
            x, y = LOCAL_COORDS[name]
            ax.plot(x, y, 'o', color='#78909C', ms=4, zorder=5)
            ax.annotate(name, (x,y), textcoords="offset points", xytext=(5,3), fontsize=6, color='#78909C', zorder=6)

        # 已飞轨迹（如果仿真已运行）
        for ac in self.engine.aircraft_list:
            if len(ac.actual_path) > 2:
                step = max(1, len(ac.actual_path) // 200)
                txs = [ac.actual_path[i][0] for i in range(0, len(ac.actual_path), step)]
                tys = [ac.actual_path[i][1] for i in range(0, len(ac.actual_path), step)]
                c = '#ce93d8' if ac.stage == FlightStage.STAGE2 or ac.finished else '#29b6f6'
                ax.plot(txs, tys, color=c, lw=0.6, alpha=0.4, zorder=1)

        ax.set_xlabel('East Distance (km)', fontsize=11, fontweight='bold', labelpad=10)
        ax.set_ylabel('North Distance (km)', fontsize=11, fontweight='bold', labelpad=10)
        ax.set_title('Trajectory Overview (Local Coordinates, RW29 as Origin)', fontsize=14, fontweight='bold', pad=15)
        ax.set_aspect('equal')
        ax.grid(True, alpha=0.3, ls='--', lw=0.5)
        ax.tick_params(labelsize=9)

        legend_el = [
            Line2D([0],[0], color=C_OUTER, lw=3, label='Outer Arc (3000m)'),
            Line2D([0],[0], color=C_INNER, lw=3, label='Inner Arc (3600m)'),
            Line2D([0],[0], color=C_OUTER, lw=1, ls='--', alpha=0.6, label='Direct Flight Path'),
            Line2D([0],[0], color=C_FINAL, lw=2.5, label='Final Approach'),
            Line2D([0],[0], color=C_DECEL, lw=1.5, ls=':', label='Decel Circle (R=5.6km)'),
            Line2D([0],[0], marker='^', color='w', markerfacecolor='#FFC107', ms=8, label='Entry Points', mec='#333'),
            Line2D([0],[0], marker='s', color='w', markerfacecolor=C_FINAL, ms=8, label='Merge Points', mec='white'),
            Line2D([0],[0], marker='*', color='w', markerfacecolor='#F44336', ms=12, label='RW29 (Airport)', mec='white'),
        ]
        ax.legend(handles=legend_el, loc='center left', bbox_to_anchor=(1.02, 0.5),
                  fontsize=8, framealpha=0.9, edgecolor='#ccc', fancybox=True)
        fig.tight_layout(rect=[0, 0, 0.82, 1])

        canvas_fig = FigureCanvasTkAgg(fig, master=win)
        canvas_fig.draw()
        canvas_fig.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        # 保存按钮
        def save_fig():
            fp = filedialog.asksaveasfilename(title="保存轨迹图", defaultextension=".png",
                                              filetypes=[("PNG", "*.png"),("PDF", "*.pdf")])
            if fp:
                fig.savefig(fp, dpi=150, bbox_inches='tight', facecolor='white')
                messagebox.showinfo("保存成功", f"已保存至:\n{fp}")

        tk.Button(win, text="💾 保存图片", command=save_fig,
                  bg="#2196F3", fg="white", font=("Microsoft YaHei", 10, "bold"),
                  relief="flat", padx=15, pady=5, cursor="hand2").pack(pady=5)

    def _export_results(self):
        """导出仿真结果"""
        if not self.engine.aircraft_list:
            messagebox.showwarning("提示", "没有仿真数据可导出")
            return

        filepath = filedialog.asksaveasfilename(
            title="导出结果",
            defaultextension=".csv",
            filetypes=[("CSV文件", "*.csv")]
        )
        if not filepath:
            return

        try:
            with open(filepath, "w", newline="", encoding="utf-8-sig") as f:
                writer = csv.writer(f)
                writer.writerow(["航班编号", "进入方向", "进入时间(s)", "飞行阶段",
                                 "飞行时间(s)", "飞行时间(min)", "飞行距离(km)",
                                 "最终速度(km/h)", "最终高度(m)"])
                stage_map = {FlightStage.APPROACHING: "进近", FlightStage.ON_ARC: "排序边",
                             FlightStage.DIRECT: "直飞", FlightStage.STAGE2: "第二阶段",
                             FlightStage.LANDED: "已落地"}
                for ac in self.engine.aircraft_list:
                    writer.writerow([
                        ac.flight_id, ac.direction.value, f"{ac.entry_time:.0f}",
                        stage_map[ac.stage], f"{ac.elapsed_time:.0f}",
                        f"{ac.elapsed_time/60:.1f}", f"{ac.total_distance:.1f}",
                        f"{ac.speed:.0f}", f"{ac.altitude:.0f}"
                    ])
                # 写统计信息
                writer.writerow([])
                writer.writerow(["统计信息"])
                writer.writerow(["一小时最大容量", self.engine.get_hourly_capacity()])
                finished = [ac for ac in self.engine.aircraft_list if ac.finished]
                if finished:
                    writer.writerow(["平均飞行时间(min)", f"{sum(ac.elapsed_time for ac in finished)/len(finished)/60:.1f}"])
                    writer.writerow(["平均飞行距离(km)", f"{sum(ac.total_distance for ac in finished)/len(finished):.1f}"])

            messagebox.showinfo("导出成功", f"结果已保存至:\n{filepath}")
        except Exception as e:
            messagebox.showerror("导出失败", str(e))


# ============================================================
# 示例数据生成
# ============================================================

def generate_sample_data():
    """生成示例航班数据用于测试"""
    sample = [
        {"flight_id": "CZ3401", "direction": "KAKMI", "entry_time": 0},
        {"flight_id": "MU5521", "direction": "OTGIN", "entry_time": 60},
        {"flight_id": "CA1234", "direction": "IGUNI", "entry_time": 120},
        {"flight_id": "3U8801", "direction": "VEDPO", "entry_time": 180},
        {"flight_id": "CZ3402", "direction": "KAKMI", "entry_time": 300},
        {"flight_id": "MU5522", "direction": "OTGIN", "entry_time": 360},
        {"flight_id": "CA1235", "direction": "IGUNI", "entry_time": 480},
        {"flight_id": "HU7601", "direction": "KAKMI", "entry_time": 600},
        {"flight_id": "SC1234", "direction": "OTGIN", "entry_time": 720},
        {"flight_id": "GJ8801", "direction": "IGUNI", "entry_time": 840},
        {"flight_id": "CZ3403", "direction": "VEDPO", "entry_time": 960},
        {"flight_id": "MU5523", "direction": "KAKMI", "entry_time": 1080},
    ]
    return sample


# ============================================================
# 主入口
# ============================================================

if __name__ == "__main__":
    root = tk.Tk()
    app = SimulationGUI(root)

    # 预加载示例数据
    sample = generate_sample_data()
    for item in sample:
        app.engine.add_aircraft(
            Direction(item["direction"]),
            float(item["entry_time"]),
            item.get("flight_id", "")
        )
    app._update_flight_list()
    app._update_stats()
    app.status_label.config(text=f"已预载 {len(sample)} 架示例航班 | 点击'开始仿真'运行")

    root.mainloop()
