565 lines
21 KiB
Python
565 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
航线抽象基类(核心)
|
||
─────────────────────────────────────────────────────────────────
|
||
BaseRoute —— 所有具体航线类型的统一抽象基类
|
||
|
||
继承体系:
|
||
BaseRoute(ABC)
|
||
├── AirRoute 空中飞行航线(适配 FlightRoute)
|
||
├── GroundRoute 陆地航线
|
||
└── WaterRoute 水中航线
|
||
|
||
设计原则:
|
||
1. 开闭原则:新增航线类型只需继承 BaseRoute,实现抽象方法
|
||
2. 接口隔离:公共接口定义在基类,特有接口在子类扩展
|
||
3. 里氏替换:所有子类实例均可作为 BaseRoute 使用
|
||
4. PyQt5 兼容:继承 QObject 以支持信号槽体系
|
||
─────────────────────────────────────────────────────────────────
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import time
|
||
from abc import abstractmethod
|
||
from typing import Iterator, List, Optional, Tuple
|
||
|
||
from PyQt5.QtCore import QObject, pyqtSignal
|
||
|
||
from uas.route.base_params import BaseParams
|
||
from uas.route.base_segment import BaseSegment, BaseTransition
|
||
from uas.route.base_waypoint import BaseWaypoint, EquipmentDomain
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# 航线元数据
|
||
# ─────────────────────────────────────────────────────────────────
|
||
|
||
class RouteMetadata:
|
||
"""航线附加元数据(非必须,按需使用)"""
|
||
def __init__(self):
|
||
self.name: str = ""
|
||
self.description: str = ""
|
||
self.author: str = ""
|
||
self.created_at: float = time.time()
|
||
self.tags: list = []
|
||
self.extra: dict = {}
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# 航线抽象基类
|
||
# ─────────────────────────────────────────────────────────────────
|
||
|
||
class BaseRoute(QObject):
|
||
"""
|
||
航线公共抽象基类
|
||
|
||
── 内部数据结构(交替排列)────────────────────────────────────
|
||
segments[0] → 直线段 0
|
||
transitions[0] → 过渡段 0(可为 None)
|
||
segments[1] → 直线段 1
|
||
transitions[1] → 过渡段 1(可为 None)
|
||
...
|
||
segments[N-1] → 最后直线段(无后续过渡段)
|
||
|
||
── 时间轴────────────────────────────────────────────────────
|
||
t0─[seg0]─t1─[trans0]─t2─[seg1]─t3─[trans1]─t4─...
|
||
|
||
── 子类必须实现的抽象方法 ────────────────────────────────────
|
||
_rebuild() 重建所有段(核心算法)
|
||
_make_segment(...) 构造具体 Segment 实例
|
||
_make_transition(...) 构造具体 Transition 实例(可返回 None)
|
||
_interpolate_segment(...) 段内插值
|
||
_state_at_start() 起点状态
|
||
_state_at_end() 终点状态
|
||
domain 属性:返回 EquipmentDomain
|
||
"""
|
||
|
||
# ── PyQt5 信号 ────────────────────────────────────────────────
|
||
route_changed = pyqtSignal() # 航线发生变化(重建后)
|
||
waypoint_added = pyqtSignal(object) # 新增路径点(BaseWaypoint)
|
||
route_cleared = pyqtSignal() # 航线被清空
|
||
|
||
def __init__(
|
||
self,
|
||
start_time: float = 0.0,
|
||
parent: QObject = None,
|
||
):
|
||
super().__init__(parent)
|
||
self.start_time: float = start_time
|
||
self.total_duration: float = 0.0
|
||
self.metadata: RouteMetadata = RouteMetadata()
|
||
|
||
# 内部数据(子类通过 _rebuild() 填充)
|
||
self._waypoints: List[BaseWaypoint] = []
|
||
self._segments: List[BaseSegment] = []
|
||
self._transitions: List[Optional[BaseTransition]] = []
|
||
|
||
self.logger = logging.getLogger(self.__class__.__name__)
|
||
|
||
# ── 公共属性(只读视图)──────────────────────────────────────
|
||
|
||
@property
|
||
def waypoints(self) -> List[BaseWaypoint]:
|
||
return list(self._waypoints)
|
||
|
||
@property
|
||
def segments(self) -> List[BaseSegment]:
|
||
return list(self._segments)
|
||
|
||
@property
|
||
def transitions(self) -> List[Optional[BaseTransition]]:
|
||
return list(self._transitions)
|
||
|
||
@property
|
||
def waypoint_count(self) -> int:
|
||
return len(self._waypoints)
|
||
|
||
@property
|
||
def segment_count(self) -> int:
|
||
return len(self._segments)
|
||
|
||
@property
|
||
def t_start(self) -> float:
|
||
return self.start_time
|
||
|
||
@property
|
||
def t_end(self) -> float:
|
||
return self.start_time + self.total_duration
|
||
|
||
@property
|
||
def is_empty(self) -> bool:
|
||
return len(self._segments) == 0
|
||
|
||
@property
|
||
def total_distance(self) -> float:
|
||
"""总水平距离(米)"""
|
||
return sum(s.distance for s in self._segments)
|
||
|
||
@property
|
||
def total_transition_time(self) -> float:
|
||
"""所有过渡段总时长(秒)"""
|
||
return sum(
|
||
t.duration for t in self._transitions if t is not None
|
||
)
|
||
|
||
# ── 抽象属性 ──────────────────────────────────────────────────
|
||
|
||
@property
|
||
@abstractmethod
|
||
def domain(self) -> EquipmentDomain:
|
||
"""返回本航线所属的装备域"""
|
||
...
|
||
|
||
# ── 路径点管理(公共接口)────────────────────────────────────
|
||
|
||
def add_waypoint(self, wp: BaseWaypoint) -> "BaseRoute":
|
||
"""
|
||
添加单个路径点
|
||
:return: self(支持链式调用)
|
||
"""
|
||
self._validate_waypoint_domain(wp)
|
||
self._waypoints.append(wp)
|
||
self.waypoint_added.emit(wp)
|
||
if len(self._waypoints) >= 2:
|
||
self._rebuild()
|
||
self.route_changed.emit()
|
||
self.logger.debug(f"添加路径点: {wp.identifier}")
|
||
return self
|
||
|
||
def add_waypoints(self, wps: List[BaseWaypoint]) -> "BaseRoute":
|
||
"""
|
||
批量添加路径点(一次性重建,效率更高)
|
||
:return: self(支持链式调用)
|
||
"""
|
||
for wp in wps:
|
||
self._validate_waypoint_domain(wp)
|
||
self._waypoints.extend(wps)
|
||
if len(self._waypoints) >= 2:
|
||
self._rebuild()
|
||
self.route_changed.emit()
|
||
self.logger.debug(f"批量添加 {len(wps)} 个路径点")
|
||
return self
|
||
|
||
def load_waypoints(self, wps: List[BaseWaypoint]) -> "BaseRoute":
|
||
"""
|
||
替换全部路径点(先清空再加载)
|
||
:return: self(支持链式调用)
|
||
"""
|
||
self._waypoints = list(wps)
|
||
for wp in self._waypoints:
|
||
self._validate_waypoint_domain(wp)
|
||
if len(self._waypoints) >= 2:
|
||
self._rebuild()
|
||
self.route_changed.emit()
|
||
return self
|
||
|
||
def remove_waypoint(self, index: int) -> "BaseRoute":
|
||
"""
|
||
移除指定索引的路径点
|
||
:return: self(支持链式调用)
|
||
"""
|
||
if 0 <= index < len(self._waypoints):
|
||
removed = self._waypoints.pop(index)
|
||
self.logger.debug(f"移除路径点: {removed.identifier}")
|
||
if len(self._waypoints) >= 2:
|
||
self._rebuild()
|
||
else:
|
||
self._segments.clear()
|
||
self._transitions.clear()
|
||
self.total_duration = 0.0
|
||
self.route_changed.emit()
|
||
return self
|
||
|
||
def replace_waypoint(self, index: int,
|
||
wp: BaseWaypoint) -> "BaseRoute":
|
||
"""
|
||
替换指定索引的路径点
|
||
:return: self(支持链式调用)
|
||
"""
|
||
self._validate_waypoint_domain(wp)
|
||
if 0 <= index < len(self._waypoints):
|
||
self._waypoints[index] = wp
|
||
if len(self._waypoints) >= 2:
|
||
self._rebuild()
|
||
self.route_changed.emit()
|
||
return self
|
||
|
||
def clear(self) -> "BaseRoute":
|
||
"""
|
||
清空航线(路径点 + 所有段)
|
||
:return: self(支持链式调用)
|
||
"""
|
||
self._waypoints.clear()
|
||
self._segments.clear()
|
||
self._transitions.clear()
|
||
self.total_duration = 0.0
|
||
self.route_cleared.emit()
|
||
self.logger.debug("航线已清空")
|
||
return self
|
||
|
||
# ── 查询接口(公共)──────────────────────────────────────────
|
||
|
||
def query(self, t: float) -> BaseParams:
|
||
"""
|
||
查询时刻 t 的装备精确状态
|
||
|
||
时间轴遍历:seg[0]→trans[0]→seg[1]→trans[1]→...→seg[N-1]
|
||
边界处理:t < t_start → 起点状态;t > t_end → 终点状态
|
||
"""
|
||
if self.is_empty:
|
||
raise ValueError(
|
||
f"{self.__class__.__name__}: 航线为空,"
|
||
f"请先添加至少两个路径点"
|
||
)
|
||
|
||
# 边界处理
|
||
if t <= self._segments[0].t_start:
|
||
return self._state_at_start()
|
||
if t >= self.t_end:
|
||
return self._state_at_end()
|
||
|
||
# 遍历所有段
|
||
for i, seg in enumerate(self._segments):
|
||
if seg.contains_time(t):
|
||
return self._interpolate_segment(seg, t)
|
||
if i < len(self._transitions):
|
||
trans = self._transitions[i]
|
||
if trans is not None and trans.contains_time(t):
|
||
return trans.query(t)
|
||
|
||
# 兜底(理论上不会到达)
|
||
return self._state_at_end()
|
||
|
||
def query_range(
|
||
self,
|
||
t_from: float,
|
||
t_to: float,
|
||
dt: float = 1.0,
|
||
) -> List[BaseParams]:
|
||
"""
|
||
查询时间范围内的状态序列
|
||
:param t_from: 起始时刻
|
||
:param t_to: 结束时刻
|
||
:param dt: 采样间隔(秒)
|
||
"""
|
||
results = []
|
||
t = t_from
|
||
while t <= t_to + 1e-9:
|
||
results.append(self.query(t))
|
||
t += dt
|
||
return results
|
||
|
||
def find_segment_at(
|
||
self, t: float
|
||
) -> Tuple[Optional[BaseSegment], Optional[BaseTransition]]:
|
||
"""
|
||
查找 t 时刻所在的段
|
||
:return: (BaseSegment, None) 或 (None, BaseTransition)
|
||
"""
|
||
for i, seg in enumerate(self._segments):
|
||
if seg.contains_time(t):
|
||
return seg, None
|
||
if i < len(self._transitions):
|
||
trans = self._transitions[i]
|
||
if trans is not None and trans.contains_time(t):
|
||
return None, trans
|
||
return None, None
|
||
|
||
def iter_segments(
|
||
self,
|
||
) -> Iterator[Tuple[BaseSegment, Optional[BaseTransition]]]:
|
||
"""
|
||
迭代所有 (直线段, 后续过渡段) 对
|
||
:yields: (BaseSegment, Optional[BaseTransition])
|
||
"""
|
||
for i, seg in enumerate(self._segments):
|
||
trans = (self._transitions[i]
|
||
if i < len(self._transitions) else None)
|
||
yield seg, trans
|
||
|
||
# ── 轨迹生成(公共)──────────────────────────────────────────
|
||
|
||
def generate_trajectory(
|
||
self, dt: float = 1.0
|
||
) -> List[BaseParams]:
|
||
"""
|
||
按均匀时间步长采样完整轨迹
|
||
:param dt: 采样间隔(秒)
|
||
:return: BaseParams 列表(含起点和终点)
|
||
"""
|
||
if self.is_empty:
|
||
return []
|
||
trajectory: List[BaseParams] = []
|
||
t = self.t_start
|
||
while t <= self.t_end + 1e-9:
|
||
trajectory.append(self.query(t))
|
||
t += dt
|
||
# 确保终点被包含
|
||
if trajectory and trajectory[-1].timestamp < self.t_end - 1e-9:
|
||
trajectory.append(self.query(self.t_end))
|
||
return trajectory
|
||
|
||
def generate_trajectory_by_distance(
|
||
self, step_m: float = 1000.0
|
||
) -> List[BaseParams]:
|
||
"""
|
||
按距离步长采样轨迹(适用于地图绘制)
|
||
:param step_m: 采样距离间隔(米)
|
||
"""
|
||
if self.is_empty:
|
||
return []
|
||
trajectory: List[BaseParams] = []
|
||
accumulated_dist: float = 0.0
|
||
next_sample_dist: float = 0.0
|
||
prev_state: Optional[BaseParams] = None
|
||
dt = 0.5
|
||
t = self.t_start
|
||
while t <= self.t_end + 1e-9:
|
||
state = self.query(t)
|
||
if prev_state is not None:
|
||
d = state.distance_to(prev_state)
|
||
accumulated_dist += d
|
||
if accumulated_dist >= next_sample_dist:
|
||
trajectory.append(state)
|
||
next_sample_dist += step_m
|
||
else:
|
||
trajectory.append(state) # 起点
|
||
prev_state = state
|
||
t += dt
|
||
return trajectory
|
||
|
||
# ── 统计与摘要(公共)────────────────────────────────────────
|
||
|
||
def statistics(self) -> dict:
|
||
"""返回航线统计数据字典"""
|
||
if self.is_empty:
|
||
return {}
|
||
segs = self._segments
|
||
return {
|
||
"domain": self.domain.value,
|
||
"waypoint_count": len(self._waypoints),
|
||
"segment_count": len(segs),
|
||
"transition_count": sum(1 for t in self._transitions
|
||
if t is not None),
|
||
"total_distance_km": self.total_distance / 1000,
|
||
"total_duration_s": self.total_duration,
|
||
"total_duration_min": self.total_duration / 60,
|
||
"transition_time_s": self.total_transition_time,
|
||
"climb_distance_km": sum(s.distance for s in segs
|
||
if s.is_climb) / 1000,
|
||
"descent_distance_km": sum(s.distance for s in segs
|
||
if s.is_descent) / 1000,
|
||
"level_distance_km": sum(s.distance for s in segs
|
||
if s.is_level) / 1000,
|
||
"max_altitude_m": max(max(s.start_alt, s.end_alt)
|
||
for s in segs),
|
||
"min_altitude_m": min(min(s.start_alt, s.end_alt)
|
||
for s in segs),
|
||
"avg_speed_ms": (
|
||
sum(s.avg_speed * s.duration for s in segs) /
|
||
sum(s.duration for s in segs)
|
||
if segs else 0.0
|
||
),
|
||
}
|
||
|
||
def info(self) -> str:
|
||
"""返回航线完整摘要字符串"""
|
||
if self.is_empty:
|
||
return f"{self.__class__.__name__}: 航线为空"
|
||
st = self.statistics()
|
||
lines = [
|
||
f"{'═'*65}",
|
||
f" {self.__class__.__name__} 航线概览 [{self.domain.value}]",
|
||
f"{'═'*65}",
|
||
f" 路径点数 : {st['waypoint_count']}",
|
||
f" 直线段数 : {st['segment_count']}",
|
||
f" 过渡段数 : {st['transition_count']}",
|
||
f" 总距离 : {st['total_distance_km']:.3f} km",
|
||
f" 总时长 : {st['total_duration_s']:.1f}s "
|
||
f"({st['total_duration_min']:.2f} min)",
|
||
f" 平均速度 : {st['avg_speed_ms']:.1f} m/s",
|
||
f" 高度范围 : {st['min_altitude_m']:.0f}m ~ "
|
||
f"{st['max_altitude_m']:.0f}m",
|
||
f"{'─'*65}",
|
||
f" {'#':>3} {'起点':>6}→{'终点':<6} "
|
||
f"{'距离(km)':>9} {'方位(°)':>8} "
|
||
f"{'时长(s)':>8} {'速度(m/s)':>12}",
|
||
f"{'─'*65}",
|
||
]
|
||
for i, (seg, trans) in enumerate(self.iter_segments()):
|
||
lines.append(
|
||
f" {i+1:>3} "
|
||
f"{seg.wp_from.identifier:>6}→{seg.wp_to.identifier:<6} "
|
||
f"{seg.distance/1000:>9.3f} "
|
||
f"{seg.bearing:>8.1f} "
|
||
f"{seg.duration:>8.1f} "
|
||
f"{seg.v_start:>5.1f}→{seg.v_end:<5.1f}"
|
||
)
|
||
if trans is not None:
|
||
dir_str = "右转" if trans.turn_direction > 0 else "左转"
|
||
lines.append(
|
||
f" ↩ {dir_str}"
|
||
f"{abs(trans.turn_angle):.1f}° "
|
||
f"({trans.duration:.1f}s)"
|
||
)
|
||
lines.append(f"{'═'*65}")
|
||
return "\n".join(lines)
|
||
|
||
def validate(self) -> list:
|
||
"""
|
||
校验整条航线合法性
|
||
:return: 错误信息列表(空列表=合法)
|
||
"""
|
||
errors = []
|
||
if len(self._waypoints) < 2:
|
||
errors.append("路径点数量不足(至少需要2个)")
|
||
for i, wp in enumerate(self._waypoints):
|
||
wp_errors = wp.validate()
|
||
for e in wp_errors:
|
||
errors.append(f"路径点[{i}] {wp.identifier}: {e}")
|
||
for i, seg in enumerate(self._segments):
|
||
seg_errors = seg.validate()
|
||
for e in seg_errors:
|
||
errors.append(f"航段[{i}]: {e}")
|
||
return errors
|
||
|
||
# ── 抽象方法(子类必须实现)──────────────────────────────────
|
||
|
||
@abstractmethod
|
||
def _rebuild(self) -> None:
|
||
"""
|
||
重建所有直线段和过渡段(核心算法)
|
||
|
||
实现要求:
|
||
1. 清空 _segments 和 _transitions
|
||
2. 遍历 _waypoints,逐段构建 BaseSegment
|
||
3. 在需要转弯处插入 BaseTransition
|
||
4. 更新 total_duration
|
||
"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def _make_segment(
|
||
self,
|
||
wp_from: BaseWaypoint,
|
||
wp_to: BaseWaypoint,
|
||
start_lat: float, start_lon: float, start_alt: float,
|
||
end_lat: float, end_lon: float, end_alt: float,
|
||
bearing: float,
|
||
t_start: float,
|
||
) -> BaseSegment:
|
||
"""
|
||
构造具体 Segment 实例(工厂方法)
|
||
子类返回对应域的 Segment 子类实例
|
||
"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def _make_transition(
|
||
self,
|
||
entry_lat: float, entry_lon: float, entry_alt: float,
|
||
entry_heading: float, exit_heading: float,
|
||
speed: float, altitude: float,
|
||
t_start: float,
|
||
) -> Optional[BaseTransition]:
|
||
"""
|
||
构造具体 Transition 实例(工厂方法)
|
||
子类返回对应域的 Transition 子类实例,
|
||
若不需要过渡段则返回 None
|
||
"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def _interpolate_segment(
|
||
self, seg: BaseSegment, t: float
|
||
) -> BaseParams:
|
||
"""
|
||
在直线段内对 t 时刻进行插值
|
||
子类实现具体的运动学模型(匀速/匀加速等)
|
||
"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def _state_at_start(self) -> BaseParams:
|
||
"""返回航线起点状态"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def _state_at_end(self) -> BaseParams:
|
||
"""返回航线终点状态"""
|
||
...
|
||
|
||
# ── 内部工具 ──────────────────────────────────────────────────
|
||
|
||
def _validate_waypoint_domain(self, wp: BaseWaypoint) -> None:
|
||
"""
|
||
校验路径点域与航线域是否匹配
|
||
(宽松检查:domain=AIR 的基类 WP 可用于任何航线)
|
||
"""
|
||
pass # 子类可覆盖以实现严格检查
|
||
|
||
def _update_total_duration(self) -> None:
|
||
"""根据所有段重新计算 total_duration"""
|
||
if not self._segments:
|
||
self.total_duration = 0.0
|
||
return
|
||
seg_time = sum(s.duration for s in self._segments)
|
||
trans_time = sum(
|
||
t.duration for t in self._transitions if t is not None
|
||
)
|
||
self.total_duration = seg_time + trans_time
|
||
|
||
def __repr__(self) -> str:
|
||
return (f"{self.__class__.__name__}("
|
||
f"domain={self.domain.value}, "
|
||
f"wps={len(self._waypoints)}, "
|
||
f"segs={len(self._segments)}, "
|
||
f"dist={self.total_distance/1000:.1f}km, "
|
||
f"dur={self.total_duration:.0f}s)")
|
||
|
||
class FlightRoute(BaseRoute):
|
||
pass |