541 lines
20 KiB
Python
541 lines
20 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
空中航线(AirRoute)
|
|||
|
|
─────────────────────────────────────────────────────────────────
|
|||
|
|
继承 BaseRoute,适配已有 FlightRoute 的实现逻辑。
|
|||
|
|
FlightRoute 只需 `class FlightRoute(AirRoute): pass` 即可适配。
|
|||
|
|
─────────────────────────────────────────────────────────────────
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import math
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
from dataclasses import dataclass, field
|
|||
|
|
from typing import List, Optional
|
|||
|
|
|
|||
|
|
from PyQt5.QtCore import QObject
|
|||
|
|
|
|||
|
|
from uas.route.base_params import AirParams
|
|||
|
|
from uas.route.base_route import BaseRoute
|
|||
|
|
from uas.route.base_segment import BaseSegment, BaseTransition, SegmentKind
|
|||
|
|
from uas.route.base_waypoint import EquipmentDomain
|
|||
|
|
from uas.route.routes.arinc424 import ARINC424Parser
|
|||
|
|
from uas.utils.geo_utils import GeoUtils
|
|||
|
|
|
|||
|
|
DEG2RAD = math.pi / 180.0
|
|||
|
|
RAD2DEG = 180.0 / math.pi
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 空中直线段
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class AirSegment(BaseSegment):
|
|||
|
|
"""
|
|||
|
|
空中直线航段
|
|||
|
|
完全兼容原 RouteSegment,可直接替换。
|
|||
|
|
"""
|
|||
|
|
domain: EquipmentDomain = field(default=EquipmentDomain.AIR, repr=False)
|
|||
|
|
kind: SegmentKind = field(default=SegmentKind.STRAIGHT, repr=False)
|
|||
|
|
|
|||
|
|
def query(self, t: float) -> AirParams:
|
|||
|
|
return _air_seg_interpolate(self, t)
|
|||
|
|
|
|||
|
|
def interpolate(self, ratio: float) -> AirParams:
|
|||
|
|
t = self.t_start + max(0.0, min(1.0, ratio)) * self.duration
|
|||
|
|
return _air_seg_interpolate(self, t)
|
|||
|
|
|
|||
|
|
def validate(self) -> list:
|
|||
|
|
errors = []
|
|||
|
|
if self.distance < 0:
|
|||
|
|
errors.append("距离不能为负")
|
|||
|
|
if self.duration < 0:
|
|||
|
|
errors.append("时长不能为负")
|
|||
|
|
if self.v_start < 0 or self.v_end < 0:
|
|||
|
|
errors.append("速度不能为负")
|
|||
|
|
return errors
|
|||
|
|
|
|||
|
|
# ── 衍生属性(覆盖基类,使用 alt_start/alt_end 字段名)────────
|
|||
|
|
@property
|
|||
|
|
def alt_start(self) -> float:
|
|||
|
|
return self.start_alt
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def alt_end(self) -> float:
|
|||
|
|
return self.end_alt
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def avg_speed(self) -> float:
|
|||
|
|
return (self.v_start + self.v_end) / 2.0
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def vertical_speed(self) -> float:
|
|||
|
|
if self.duration < 1e-6:
|
|||
|
|
return 0.0
|
|||
|
|
return (self.end_alt - self.start_alt) / self.duration
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _air_seg_interpolate(seg: AirSegment, t: float) -> AirParams:
|
|||
|
|
"""空中直线段内匀加速插值"""
|
|||
|
|
dt = t - seg.t_start
|
|||
|
|
ratio = dt / seg.duration if seg.duration > 1e-6 else 1.0
|
|||
|
|
ratio = max(0.0, min(1.0, ratio))
|
|||
|
|
|
|||
|
|
# 匀加速位移:s = v0*t + 0.5*a*t²
|
|||
|
|
dist = seg.v_start * dt + 0.5 * seg.acceleration * dt * dt
|
|||
|
|
dist = max(0.0, min(dist, seg.distance))
|
|||
|
|
|
|||
|
|
new_lat, new_lon = GeoUtils.offset_position(
|
|||
|
|
seg.start_lat, seg.start_lon, seg.bearing, dist
|
|||
|
|
)
|
|||
|
|
speed = max(seg.v_start + seg.acceleration * dt, 0.0)
|
|||
|
|
alt = seg.start_alt + (seg.end_alt - seg.start_alt) * ratio
|
|||
|
|
vs = seg.vertical_speed
|
|||
|
|
|
|||
|
|
#TODO: 计算phase
|
|||
|
|
# phase = determine_phase(...)
|
|||
|
|
|
|||
|
|
return AirParams(
|
|||
|
|
timestamp = t,
|
|||
|
|
latitude = new_lat,
|
|||
|
|
longitude = new_lon,
|
|||
|
|
altitude = alt,
|
|||
|
|
heading = seg.bearing,
|
|||
|
|
speed = speed,
|
|||
|
|
acceleration = seg.acceleration,
|
|||
|
|
airspeed = speed,
|
|||
|
|
vertical_speed = vs,
|
|||
|
|
# phase = phase,
|
|||
|
|
domain = EquipmentDomain.AIR,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 空中转弯过渡段(Fly-by 圆弧)
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class AirTurnSegment(BaseTransition):
|
|||
|
|
"""
|
|||
|
|
空中 Fly-by 圆弧转弯段
|
|||
|
|
完全兼容原 TurnSegment,可直接替换。
|
|||
|
|
"""
|
|||
|
|
turn_radius: float = 2000.0 # 转弯半径(米)
|
|||
|
|
domain: EquipmentDomain = field(
|
|||
|
|
default=EquipmentDomain.AIR, repr=False
|
|||
|
|
)
|
|||
|
|
kind: SegmentKind = field(
|
|||
|
|
default=SegmentKind.TURN, repr=False
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 圆心坐标(__post_init__ 计算)
|
|||
|
|
_center_lat: float = field(default=0.0, init=False, repr=False)
|
|||
|
|
_center_lon: float = field(default=0.0, init=False, repr=False)
|
|||
|
|
_exit_lat: float = field(default=0.0, init=False, repr=False)
|
|||
|
|
_exit_lon: float = field(default=0.0, init=False, repr=False)
|
|||
|
|
|
|||
|
|
def __post_init__(self):
|
|||
|
|
self._compute_geometry()
|
|||
|
|
|
|||
|
|
def _compute_geometry(self):
|
|||
|
|
"""计算圆弧圆心、出口坐标和时长"""
|
|||
|
|
angle = self.turn_angle # 有符号转弯角(度)
|
|||
|
|
r = self.turn_radius
|
|||
|
|
|
|||
|
|
# 圆心方向:转弯方向的垂直方向
|
|||
|
|
if self.turn_direction >= 0:
|
|||
|
|
center_bearing = (self.entry_heading + 90) % 360 # 右转
|
|||
|
|
else:
|
|||
|
|
center_bearing = (self.entry_heading - 90) % 360 # 左转
|
|||
|
|
|
|||
|
|
self._center_lat, self._center_lon = GeoUtils.offset_position(
|
|||
|
|
self.entry_lat, self.entry_lon, center_bearing, r
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 出口坐标:圆心到出口的方向
|
|||
|
|
if self.turn_direction >= 0:
|
|||
|
|
exit_bearing = (center_bearing + 180 + angle) % 360
|
|||
|
|
else:
|
|||
|
|
exit_bearing = (center_bearing + 180 + angle) % 360
|
|||
|
|
|
|||
|
|
self._exit_lat, self._exit_lon = GeoUtils.offset_position(
|
|||
|
|
self._center_lat, self._center_lon,
|
|||
|
|
(exit_bearing + 180) % 360, r
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 圆弧时长
|
|||
|
|
arc_len = abs(math.radians(angle)) * r
|
|||
|
|
self.duration = arc_len / self.speed if self.speed > 1e-6 else 0.0
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def exit_lat(self) -> float:
|
|||
|
|
return self._exit_lat
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def exit_lon(self) -> float:
|
|||
|
|
return self._exit_lon
|
|||
|
|
|
|||
|
|
def query(self, t: float) -> AirParams:
|
|||
|
|
"""圆弧段内按角速度插值"""
|
|||
|
|
dt = t - self.t_start
|
|||
|
|
ratio = dt / self.duration if self.duration > 1e-6 else 1.0
|
|||
|
|
ratio = max(0.0, min(1.0, ratio))
|
|||
|
|
|
|||
|
|
# 已转过的角度
|
|||
|
|
swept_angle = self.turn_angle * ratio
|
|||
|
|
|
|||
|
|
# 当前位置:从圆心出发,按已转角度计算
|
|||
|
|
if self.turn_direction >= 0:
|
|||
|
|
center_bearing = (self.entry_heading + 90) % 360
|
|||
|
|
else:
|
|||
|
|
center_bearing = (self.entry_heading - 90) % 360
|
|||
|
|
|
|||
|
|
entry_from_center = (center_bearing + 180) % 360
|
|||
|
|
current_from_center = (entry_from_center + swept_angle) % 360
|
|||
|
|
|
|||
|
|
cur_lat, cur_lon = GeoUtils.offset_position(
|
|||
|
|
self._center_lat, self._center_lon,
|
|||
|
|
current_from_center, self.turn_radius
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 当前航向 = 入口航向 + 已转角度
|
|||
|
|
cur_heading = (self.entry_heading + swept_angle) % 360
|
|||
|
|
|
|||
|
|
# 转弯率(°/s)
|
|||
|
|
turn_rate = (self.turn_angle / self.duration
|
|||
|
|
if self.duration > 1e-6 else 0.0)
|
|||
|
|
|
|||
|
|
# 坡度角(bank angle)
|
|||
|
|
g = 9.81
|
|||
|
|
bank_rad = math.atan(
|
|||
|
|
self.speed * abs(math.radians(turn_rate)) / g
|
|||
|
|
)
|
|||
|
|
bank_angle = math.degrees(bank_rad) * self.turn_direction
|
|||
|
|
|
|||
|
|
return AirParams(
|
|||
|
|
timestamp = t,
|
|||
|
|
latitude = cur_lat,
|
|||
|
|
longitude = cur_lon,
|
|||
|
|
altitude = self.altitude,
|
|||
|
|
heading = cur_heading,
|
|||
|
|
roll = bank_angle,
|
|||
|
|
speed = self.speed,
|
|||
|
|
airspeed = self.speed,
|
|||
|
|
vertical_speed = 0.0,
|
|||
|
|
turn_rate = turn_rate,
|
|||
|
|
turn_radius = self.turn_radius,
|
|||
|
|
bank_angle = bank_angle,
|
|||
|
|
phase = "CRUISE",
|
|||
|
|
domain = EquipmentDomain.AIR,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def validate(self) -> list:
|
|||
|
|
errors = []
|
|||
|
|
if self.turn_radius <= 0:
|
|||
|
|
errors.append(f"转弯半径必须为正: {self.turn_radius}")
|
|||
|
|
if self.speed <= 0:
|
|||
|
|
errors.append(f"速度必须为正: {self.speed}")
|
|||
|
|
return errors
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 空中航线(AirRoute)
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class AirRoute(BaseRoute):
|
|||
|
|
"""
|
|||
|
|
空中飞行航线
|
|||
|
|
|
|||
|
|
实现 BaseRoute 所有抽象方法,
|
|||
|
|
算法逻辑与原 FlightRoute._rebuild() 完全一致。
|
|||
|
|
|
|||
|
|
适配方式:
|
|||
|
|
原 FlightRoute 只需改为:
|
|||
|
|
class FlightRoute(AirRoute):
|
|||
|
|
pass
|
|||
|
|
即可纳入 BaseRoute 继承体系,无需其他改动。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
start_time: float = 0.0,
|
|||
|
|
default_turn_radius: float = 2000.0,
|
|||
|
|
min_turn_radius: float = 500.0,
|
|||
|
|
parent: QObject = None,
|
|||
|
|
):
|
|||
|
|
super().__init__(start_time=start_time, parent=parent)
|
|||
|
|
self.default_turn_radius = default_turn_radius
|
|||
|
|
self.min_turn_radius = min_turn_radius
|
|||
|
|
|
|||
|
|
# ── 域标识 ────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def domain(self) -> EquipmentDomain:
|
|||
|
|
return EquipmentDomain.AIR
|
|||
|
|
|
|||
|
|
# ── 核心重建算法 ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _rebuild(self) -> None:
|
|||
|
|
"""
|
|||
|
|
重建所有直线段和圆弧转弯段
|
|||
|
|
算法与原 FlightRoute._rebuild() 完全一致
|
|||
|
|
"""
|
|||
|
|
self._segments.clear()
|
|||
|
|
self._transitions.clear()
|
|||
|
|
|
|||
|
|
n = len(self._waypoints)
|
|||
|
|
if n < 2:
|
|||
|
|
self.total_duration = 0.0
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Step 1:预计算所有段方位角
|
|||
|
|
bearings: List[float] = []
|
|||
|
|
for i in range(n - 1):
|
|||
|
|
brg = GeoUtils.bearing(
|
|||
|
|
self._waypoints[i].latitude, self._waypoints[i].longitude,
|
|||
|
|
self._waypoints[i+1].latitude, self._waypoints[i+1].longitude,
|
|||
|
|
)
|
|||
|
|
bearings.append(brg)
|
|||
|
|
|
|||
|
|
t_cursor = self.start_time
|
|||
|
|
prev_exit_lat = self._waypoints[0].latitude
|
|||
|
|
prev_exit_lon = self._waypoints[0].longitude
|
|||
|
|
|
|||
|
|
# Step 2-5:逐段构建
|
|||
|
|
for i in range(n - 1):
|
|||
|
|
wp_a = self._waypoints[i]
|
|||
|
|
wp_b = self._waypoints[i + 1]
|
|||
|
|
brg_in = bearings[i]
|
|||
|
|
brg_out = bearings[i + 1] if (i + 1) < len(bearings) else brg_in
|
|||
|
|
|
|||
|
|
# 确定转弯半径
|
|||
|
|
r = getattr(wp_b, "turn_radius", 0.0)
|
|||
|
|
if r <= 0:
|
|||
|
|
r = self.default_turn_radius
|
|||
|
|
r = max(r, self.min_turn_radius)
|
|||
|
|
|
|||
|
|
# 转弯角
|
|||
|
|
turn_angle = GeoUtils.heading_diff(brg_in, brg_out)
|
|||
|
|
|
|||
|
|
# 判断是否需要转弯
|
|||
|
|
fly_over = getattr(wp_b, "fly_over", False)
|
|||
|
|
need_turn = (
|
|||
|
|
i < n - 2
|
|||
|
|
and not fly_over
|
|||
|
|
and abs(turn_angle) > 0.5
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 计算 TIP(提前转弯点)
|
|||
|
|
if need_turn:
|
|||
|
|
tip_dist_m = r * abs(math.tan(math.radians(turn_angle / 2)))
|
|||
|
|
tip_lat, tip_lon = GeoUtils.offset_position(
|
|||
|
|
wp_b.latitude, wp_b.longitude,
|
|||
|
|
(brg_in + 180) % 360, tip_dist_m
|
|||
|
|
)
|
|||
|
|
# 安全检查:TIP 不能超过航路点
|
|||
|
|
seg_total_dist = GeoUtils.haversine(
|
|||
|
|
prev_exit_lat, prev_exit_lon,
|
|||
|
|
wp_b.latitude, wp_b.longitude,
|
|||
|
|
)
|
|||
|
|
tip_check_dist = GeoUtils.haversine(
|
|||
|
|
wp_b.latitude, wp_b.longitude,
|
|||
|
|
tip_lat, tip_lon,
|
|||
|
|
)
|
|||
|
|
if tip_check_dist > seg_total_dist * 0.95:
|
|||
|
|
need_turn = False
|
|||
|
|
tip_lat, tip_lon = wp_b.latitude, wp_b.longitude
|
|||
|
|
else:
|
|||
|
|
tip_lat, tip_lon = wp_b.latitude, wp_b.longitude
|
|||
|
|
|
|||
|
|
# 构建直线段
|
|||
|
|
seg_dist = GeoUtils.haversine(
|
|||
|
|
prev_exit_lat, prev_exit_lon, tip_lat, tip_lon
|
|||
|
|
)
|
|||
|
|
v0 = wp_a.speed
|
|||
|
|
v1 = wp_b.speed
|
|||
|
|
v_avg = (v0 + v1) / 2.0
|
|||
|
|
if v_avg < 1e-6:
|
|||
|
|
v_avg = 1.0
|
|||
|
|
duration = seg_dist / v_avg if v_avg > 1e-6 else 0.0
|
|||
|
|
acc = (v1 - v0) / duration if duration > 1e-6 else 0.0
|
|||
|
|
|
|||
|
|
seg = self._make_segment(
|
|||
|
|
wp_from = wp_a,
|
|||
|
|
wp_to = wp_b,
|
|||
|
|
start_lat = prev_exit_lat,
|
|||
|
|
start_lon = prev_exit_lon,
|
|||
|
|
start_alt = wp_a.altitude,
|
|||
|
|
end_lat = tip_lat,
|
|||
|
|
end_lon = tip_lon,
|
|||
|
|
end_alt = wp_b.altitude,
|
|||
|
|
bearing = brg_in,
|
|||
|
|
t_start = t_cursor,
|
|||
|
|
v_start = v0,
|
|||
|
|
v_end = v1,
|
|||
|
|
acc = acc,
|
|||
|
|
dist = seg_dist,
|
|||
|
|
duration = duration,
|
|||
|
|
)
|
|||
|
|
self._segments.append(seg)
|
|||
|
|
t_cursor += duration
|
|||
|
|
|
|||
|
|
# 构建转弯段
|
|||
|
|
if need_turn:
|
|||
|
|
trans = self._make_transition(
|
|||
|
|
entry_lat = tip_lat,
|
|||
|
|
entry_lon = tip_lon,
|
|||
|
|
entry_alt = wp_b.altitude,
|
|||
|
|
entry_heading = brg_in,
|
|||
|
|
exit_heading = brg_out,
|
|||
|
|
speed = wp_b.speed,
|
|||
|
|
altitude = wp_b.altitude,
|
|||
|
|
t_start = t_cursor,
|
|||
|
|
turn_radius = r,
|
|||
|
|
)
|
|||
|
|
self._transitions.append(trans)
|
|||
|
|
if trans is not None:
|
|||
|
|
t_cursor += trans.duration
|
|||
|
|
prev_exit_lat = trans.exit_lat
|
|||
|
|
prev_exit_lon = trans.exit_lon
|
|||
|
|
else:
|
|||
|
|
prev_exit_lat = tip_lat
|
|||
|
|
prev_exit_lon = tip_lon
|
|||
|
|
else:
|
|||
|
|
self._transitions.append(None)
|
|||
|
|
prev_exit_lat = tip_lat
|
|||
|
|
prev_exit_lon = tip_lon
|
|||
|
|
|
|||
|
|
self._update_total_duration()
|
|||
|
|
|
|||
|
|
# ── 工厂方法 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _make_segment(
|
|||
|
|
self,
|
|||
|
|
wp_from, wp_to,
|
|||
|
|
start_lat, start_lon, start_alt,
|
|||
|
|
end_lat, end_lon, end_alt,
|
|||
|
|
bearing, t_start,
|
|||
|
|
v_start=0.0, v_end=0.0, acc=0.0,
|
|||
|
|
dist=0.0, duration=0.0,
|
|||
|
|
**kwargs,
|
|||
|
|
) -> AirSegment:
|
|||
|
|
return AirSegment(
|
|||
|
|
wp_from = wp_from,
|
|||
|
|
wp_to = wp_to,
|
|||
|
|
start_lat = start_lat,
|
|||
|
|
start_lon = start_lon,
|
|||
|
|
start_alt = start_alt,
|
|||
|
|
end_lat = end_lat,
|
|||
|
|
end_lon = end_lon,
|
|||
|
|
end_alt = end_alt,
|
|||
|
|
distance = dist,
|
|||
|
|
bearing = bearing,
|
|||
|
|
t_start = t_start,
|
|||
|
|
t_end = t_start + duration,
|
|||
|
|
duration = duration,
|
|||
|
|
v_start = v_start,
|
|||
|
|
v_end = v_end,
|
|||
|
|
acceleration = acc,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _make_transition(
|
|||
|
|
self,
|
|||
|
|
entry_lat, entry_lon, entry_alt,
|
|||
|
|
entry_heading, exit_heading,
|
|||
|
|
speed, altitude, t_start,
|
|||
|
|
turn_radius=2000.0,
|
|||
|
|
**kwargs,
|
|||
|
|
) -> Optional[AirTurnSegment]:
|
|||
|
|
return AirTurnSegment(
|
|||
|
|
entry_lat = entry_lat,
|
|||
|
|
entry_lon = entry_lon,
|
|||
|
|
entry_alt = entry_alt,
|
|||
|
|
entry_heading = entry_heading,
|
|||
|
|
exit_heading = exit_heading,
|
|||
|
|
speed = speed,
|
|||
|
|
altitude = altitude,
|
|||
|
|
t_start = t_start,
|
|||
|
|
turn_radius = turn_radius,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _interpolate_segment(
|
|||
|
|
self, seg: BaseSegment, t: float
|
|||
|
|
) -> AirParams:
|
|||
|
|
return _air_seg_interpolate(seg, t) # type: ignore[arg-type]
|
|||
|
|
|
|||
|
|
def _state_at_start(self) -> AirParams:
|
|||
|
|
seg = self._segments[0]
|
|||
|
|
wp = self._waypoints[0]
|
|||
|
|
vs = seg.vertical_speed
|
|||
|
|
return AirParams(
|
|||
|
|
timestamp = self.t_start,
|
|||
|
|
latitude = seg.start_lat,
|
|||
|
|
longitude = seg.start_lon,
|
|||
|
|
altitude = seg.start_alt,
|
|||
|
|
heading = seg.bearing,
|
|||
|
|
speed = seg.v_start,
|
|||
|
|
airspeed = seg.v_start,
|
|||
|
|
vertical_speed = vs,
|
|||
|
|
# phase = determine_phase(
|
|||
|
|
# seg.start_alt, seg.end_alt, vs
|
|||
|
|
# ),
|
|||
|
|
domain = EquipmentDomain.AIR,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _state_at_end(self) -> AirParams:
|
|||
|
|
last_trans = self._transitions[-1] if self._transitions else None
|
|||
|
|
if last_trans is not None:
|
|||
|
|
s = last_trans.query(last_trans.t_end)
|
|||
|
|
s.timestamp = self.t_end
|
|||
|
|
s.phase = "LAND"
|
|||
|
|
return s
|
|||
|
|
last_seg = self._segments[-1]
|
|||
|
|
return AirParams(
|
|||
|
|
timestamp = self.t_end,
|
|||
|
|
latitude = last_seg.end_lat,
|
|||
|
|
longitude = last_seg.end_lon,
|
|||
|
|
altitude = last_seg.end_alt,
|
|||
|
|
heading = last_seg.bearing,
|
|||
|
|
speed = last_seg.v_end,
|
|||
|
|
airspeed = last_seg.v_end,
|
|||
|
|
vertical_speed = 0.0,
|
|||
|
|
phase = "LAND",
|
|||
|
|
domain = EquipmentDomain.AIR,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# FlightRoute 适配(最小改动)
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class FlightRoute(AirRoute):
|
|||
|
|
"""
|
|||
|
|
原 FlightRoute 适配类
|
|||
|
|
继承 AirRoute,保持与原有代码的完全兼容。
|
|||
|
|
原有代码中所有对 FlightRoute 的引用无需修改。
|
|||
|
|
"""
|
|||
|
|
@staticmethod
|
|||
|
|
def load_from_arinc424(route_file, start_time: float = 0.0,
|
|||
|
|
default_turn_radius: float = 2000.0,
|
|||
|
|
min_turn_radius: float = 500.0,) -> FlightRoute:
|
|||
|
|
"""加载航线:优先从文件,否则使用内置示例"""
|
|||
|
|
parser = ARINC424Parser(default_speed_ms=220.0, default_altitude_m=10000.0)
|
|||
|
|
if not os.path.isfile(route_file):
|
|||
|
|
print(f"[错误] 航线文件不存在: {route_file}", file=sys.stderr)
|
|||
|
|
sys.exit(1)
|
|||
|
|
with open(route_file, "r", encoding="utf-8") as f:
|
|||
|
|
content = f.read()
|
|||
|
|
ext = os.path.splitext(route_file)[1].lower()
|
|||
|
|
if ext == ".csv":
|
|||
|
|
waypoints = parser.parse_csv(content)
|
|||
|
|
else:
|
|||
|
|
waypoints = parser.parse_arinc424(content)
|
|||
|
|
print(f" [航线] 已从文件加载: {route_file}({len(waypoints)} 个航路点)")
|
|||
|
|
route = FlightRoute(start_time=start_time,
|
|||
|
|
default_turn_radius=default_turn_radius,
|
|||
|
|
min_turn_radius=min_turn_radius)
|
|||
|
|
route.add_waypoints(waypoints)
|
|||
|
|
return route
|