|
|
#!/usr/bin/env python3
|
|
|
"""
|
|
|
任務群組模組
|
|
|
管理多任務群組的資料結構與無人機分配對話框
|
|
|
"""
|
|
|
from PyQt6.QtWidgets import (
|
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
|
QComboBox, QDialog, QCheckBox, QScrollArea, QFrame, QLineEdit
|
|
|
)
|
|
|
from PyQt6.QtCore import Qt, pyqtSignal
|
|
|
from mission_executor import MissionExecutor, MissionState
|
|
|
|
|
|
|
|
|
# 群組顏色(循環使用)
|
|
|
GROUP_COLORS = [
|
|
|
'#4A9EFF', # 藍
|
|
|
'#FF8C42', # 橘
|
|
|
'#56C87A', # 綠
|
|
|
'#E85D75', # 紅
|
|
|
'#B07CED', # 紫
|
|
|
'#F5C542', # 黃
|
|
|
'#42C9C9', # 青
|
|
|
'#FF6B9D', # 粉
|
|
|
]
|
|
|
|
|
|
|
|
|
class MissionGroup:
|
|
|
"""單一任務群組的資料"""
|
|
|
|
|
|
def __init__(self, group_id, color):
|
|
|
self.group_id = group_id # 'A', 'B', 'C', ...
|
|
|
self.color = color # 顏色 hex
|
|
|
self.drone_ids = set() # 已分配的無人機 ID
|
|
|
self.mission_type = 'M_FORMATION' # 預設任務類型
|
|
|
self.planned_waypoints = None # 規劃結果 dict
|
|
|
self.executor = None # MissionExecutor 實例(延遲建立)
|
|
|
self.center_origin = None # 規劃原點
|
|
|
|
|
|
@property
|
|
|
def state(self):
|
|
|
if self.executor is None:
|
|
|
return MissionState.IDLE
|
|
|
return self.executor.state
|
|
|
|
|
|
@property
|
|
|
def display_name(self):
|
|
|
return f"Group {self.group_id}"
|
|
|
|
|
|
|
|
|
class DroneAssignDialog(QDialog):
|
|
|
"""無人機分配對話框"""
|
|
|
|
|
|
def __init__(self, parent, all_drone_ids, current_assigned, other_assigned):
|
|
|
"""
|
|
|
Args:
|
|
|
parent: 父 widget
|
|
|
all_drone_ids: 所有可用無人機 ID 列表
|
|
|
current_assigned: 當前群組已分配的無人機 set
|
|
|
other_assigned: 其他群組已佔用的無人機 dict {drone_id: group_id}
|
|
|
"""
|
|
|
super().__init__(parent)
|
|
|
self.setWindowTitle("分配無人機")
|
|
|
self.setMinimumWidth(280)
|
|
|
self.setStyleSheet("""
|
|
|
QDialog { background-color: #2B2B2B; }
|
|
|
QLabel { color: #DDD; }
|
|
|
QCheckBox { color: #DDD; spacing: 8px; padding: 4px; }
|
|
|
QCheckBox::indicator { width: 16px; height: 16px; }
|
|
|
QCheckBox:disabled { color: #666; }
|
|
|
""")
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
title = QLabel("選擇要分配到此群組的無人機:")
|
|
|
title.setStyleSheet("font-size: 13px; font-weight: bold; padding-bottom: 6px;")
|
|
|
layout.addWidget(title)
|
|
|
|
|
|
# 滾動區域
|
|
|
scroll = QScrollArea()
|
|
|
scroll.setWidgetResizable(True)
|
|
|
scroll.setMaximumHeight(300)
|
|
|
scroll_widget = QWidget()
|
|
|
scroll_layout = QVBoxLayout(scroll_widget)
|
|
|
scroll_layout.setSpacing(2)
|
|
|
|
|
|
self.checkboxes = {}
|
|
|
sorted_ids = sorted(all_drone_ids, key=lambda x: (x.split('_')[0], int(x.split('_')[1])))
|
|
|
|
|
|
for drone_id in sorted_ids:
|
|
|
cb = QCheckBox(drone_id)
|
|
|
|
|
|
if drone_id in current_assigned:
|
|
|
cb.setChecked(True)
|
|
|
elif drone_id in other_assigned:
|
|
|
cb.setEnabled(False)
|
|
|
cb.setToolTip(f"已分配到 Group {other_assigned[drone_id]}")
|
|
|
cb.setText(f"{drone_id} (Group {other_assigned[drone_id]})")
|
|
|
|
|
|
self.checkboxes[drone_id] = cb
|
|
|
scroll_layout.addWidget(cb)
|
|
|
|
|
|
scroll_layout.addStretch()
|
|
|
scroll.setWidget(scroll_widget)
|
|
|
layout.addWidget(scroll)
|
|
|
|
|
|
# 按鈕
|
|
|
btn_layout = QHBoxLayout()
|
|
|
ok_btn = QPushButton("確定")
|
|
|
ok_btn.setStyleSheet("""
|
|
|
QPushButton { background-color: #4A9EFF; color: white; border: none;
|
|
|
padding: 8px 20px; border-radius: 4px; font-weight: bold; }
|
|
|
QPushButton:hover { background-color: #3A8EEF; }
|
|
|
""")
|
|
|
ok_btn.clicked.connect(self.accept)
|
|
|
|
|
|
cancel_btn = QPushButton("取消")
|
|
|
cancel_btn.setStyleSheet("""
|
|
|
QPushButton { background-color: #555; color: #DDD; border: none;
|
|
|
padding: 8px 20px; border-radius: 4px; }
|
|
|
QPushButton:hover { background-color: #666; }
|
|
|
""")
|
|
|
cancel_btn.clicked.connect(self.reject)
|
|
|
|
|
|
btn_layout.addStretch()
|
|
|
btn_layout.addWidget(cancel_btn)
|
|
|
btn_layout.addWidget(ok_btn)
|
|
|
layout.addLayout(btn_layout)
|
|
|
|
|
|
def get_selected(self):
|
|
|
"""回傳勾選的無人機 ID set"""
|
|
|
return {did for did, cb in self.checkboxes.items() if cb.isChecked() and cb.isEnabled()}
|
|
|
|
|
|
|
|
|
class GroupPanel(QWidget):
|
|
|
"""單一群組的 UI 面板(嵌入到 tab 中)— 三欄式佈局"""
|
|
|
|
|
|
# 信號
|
|
|
assign_drones_requested = pyqtSignal(str) # group_id
|
|
|
mission_type_changed = pyqtSignal(str, str) # group_id, mission_type
|
|
|
start_requested = pyqtSignal(str) # group_id
|
|
|
pause_requested = pyqtSignal(str) # group_id
|
|
|
stop_requested = pyqtSignal(str) # group_id
|
|
|
mode_change_requested = pyqtSignal(str, str) # group_id, mode
|
|
|
arm_requested = pyqtSignal(str) # group_id
|
|
|
takeoff_requested = pyqtSignal(str, float) # group_id, altitude
|
|
|
box_select_requested = pyqtSignal(str) # group_id — 框選直接分配
|
|
|
select_all_requested = pyqtSignal(str) # group_id — 全選直接分配
|
|
|
clear_group_requested = pyqtSignal(str) # group_id — 清除分組
|
|
|
|
|
|
BUTTON_STYLE = """
|
|
|
QPushButton {{ background-color: {bg}; color: {fg}; border: none;
|
|
|
padding: 5px 8px; border-radius: 4px; font-size: 11px; }}
|
|
|
QPushButton:hover {{ background-color: {hover}; }}
|
|
|
QPushButton:disabled {{ background-color: #444; color: #666; }}
|
|
|
"""
|
|
|
|
|
|
def __init__(self, group: MissionGroup, parent=None):
|
|
|
super().__init__(parent)
|
|
|
self.group = group
|
|
|
self._build_ui()
|
|
|
|
|
|
def _make_sep(self):
|
|
|
"""建立垂直分隔線"""
|
|
|
sep = QFrame()
|
|
|
sep.setFrameShape(QFrame.Shape.VLine)
|
|
|
sep.setStyleSheet("color: #444;")
|
|
|
return sep
|
|
|
|
|
|
def _build_ui(self):
|
|
|
layout = QVBoxLayout(self)
|
|
|
layout.setContentsMargins(6, 4, 6, 4)
|
|
|
layout.setSpacing(0)
|
|
|
|
|
|
COMBO = ("QComboBox { background-color: #333; color: #DDD; "
|
|
|
"border-radius: 3px; padding: 2px 6px; font-size: 11px; }")
|
|
|
BTN = self.BUTTON_STYLE
|
|
|
LBL = "color: #AAA; font-size: 11px;"
|
|
|
TITLE = "color: #DDD; font-size: 11px; font-weight: bold; padding-bottom: 2px;"
|
|
|
|
|
|
# ── 三欄主佈局 ──
|
|
|
cols = QHBoxLayout()
|
|
|
cols.setSpacing(6)
|
|
|
|
|
|
# ============================
|
|
|
# 左欄:控制指令
|
|
|
# ============================
|
|
|
left = QVBoxLayout()
|
|
|
left.setSpacing(3)
|
|
|
|
|
|
left_title = QLabel("控制指令")
|
|
|
left_title.setStyleSheet(TITLE)
|
|
|
left.addWidget(left_title)
|
|
|
|
|
|
# 模式切換
|
|
|
mode_row = QHBoxLayout()
|
|
|
mode_row.setSpacing(3)
|
|
|
self.mode_combo = QComboBox()
|
|
|
self.mode_combo.addItems([
|
|
|
"GUIDED", "AUTO", "LAND", "LOITER",
|
|
|
"STABILIZE", "ALT_HOLD", "RTL", "POSHOLD", "SMART_RTL"
|
|
|
])
|
|
|
self.mode_combo.setStyleSheet(COMBO)
|
|
|
mode_btn = QPushButton("切換")
|
|
|
mode_btn.setStyleSheet(BTN.format(bg='#555', fg='#DDD', hover='#666'))
|
|
|
mode_btn.clicked.connect(
|
|
|
lambda: self.mode_change_requested.emit(
|
|
|
self.group.group_id, self.mode_combo.currentText()))
|
|
|
mode_row.addWidget(self.mode_combo, 1)
|
|
|
mode_row.addWidget(mode_btn)
|
|
|
left.addLayout(mode_row)
|
|
|
|
|
|
# 解鎖
|
|
|
arm_btn = QPushButton("解鎖")
|
|
|
arm_btn.setStyleSheet(BTN.format(bg='#555', fg='#DDD', hover='#666'))
|
|
|
arm_btn.clicked.connect(
|
|
|
lambda: self.arm_requested.emit(self.group.group_id))
|
|
|
left.addWidget(arm_btn)
|
|
|
|
|
|
# 起飛
|
|
|
takeoff_row = QHBoxLayout()
|
|
|
takeoff_row.setSpacing(3)
|
|
|
self.alt_input = QComboBox()
|
|
|
self.alt_input.setEditable(True)
|
|
|
self.alt_input.addItems(["5", "10", "15", "20"])
|
|
|
self.alt_input.setCurrentText("10")
|
|
|
self.alt_input.setStyleSheet(COMBO)
|
|
|
alt_lbl = QLabel("m")
|
|
|
alt_lbl.setStyleSheet(LBL)
|
|
|
takeoff_btn = QPushButton("起飛")
|
|
|
takeoff_btn.setStyleSheet(BTN.format(bg='#555', fg='#DDD', hover='#666'))
|
|
|
takeoff_btn.clicked.connect(self._on_takeoff)
|
|
|
takeoff_row.addWidget(self.alt_input, 1)
|
|
|
takeoff_row.addWidget(alt_lbl)
|
|
|
takeoff_row.addWidget(takeoff_btn)
|
|
|
left.addLayout(takeoff_row)
|
|
|
|
|
|
left.addStretch()
|
|
|
|
|
|
# ============================
|
|
|
# 中欄:任務規劃(左右分割)
|
|
|
# ============================
|
|
|
mid = QVBoxLayout()
|
|
|
mid.setSpacing(2)
|
|
|
|
|
|
mid_title = QLabel("任務規劃")
|
|
|
mid_title.setStyleSheet(TITLE)
|
|
|
mid.addWidget(mid_title)
|
|
|
|
|
|
mid_body = QHBoxLayout()
|
|
|
mid_body.setSpacing(4)
|
|
|
|
|
|
# 左側:類型 + 狀態 + 座標
|
|
|
mid_left = QVBoxLayout()
|
|
|
mid_left.setSpacing(2)
|
|
|
self.type_combo = QComboBox()
|
|
|
self.type_combo.addItems([
|
|
|
"M_FORMATION", "CIRCLE_FORMATION", "LEADER_FOLLOWER", "GRID_SWEEP"
|
|
|
])
|
|
|
self.type_combo.setStyleSheet(COMBO)
|
|
|
self.type_combo.currentTextChanged.connect(
|
|
|
lambda t: self.mission_type_changed.emit(self.group.group_id, t))
|
|
|
mid_left.addWidget(self.type_combo)
|
|
|
self.status_label = QLabel("○ 未規劃")
|
|
|
self.status_label.setStyleSheet("color: #888; font-size: 11px;")
|
|
|
mid_left.addWidget(self.status_label)
|
|
|
self.center_label = QLabel("中心: --")
|
|
|
self.center_label.setStyleSheet("color: #AAA; font-size: 11px;")
|
|
|
mid_left.addWidget(self.center_label)
|
|
|
self.target_label = QLabel("目標: --")
|
|
|
self.target_label.setStyleSheet("color: #AAA; font-size: 11px;")
|
|
|
mid_left.addWidget(self.target_label)
|
|
|
mid_left.addStretch()
|
|
|
|
|
|
# 右側:執行 / 暫停 / 停止(垂直排列)
|
|
|
mid_right = QVBoxLayout()
|
|
|
mid_right.setSpacing(3)
|
|
|
self.start_btn = QPushButton("▶ 執行")
|
|
|
self.start_btn.setStyleSheet(BTN.format(bg='#2E7D32', fg='white', hover='#388E3C'))
|
|
|
self.start_btn.clicked.connect(
|
|
|
lambda: self.start_requested.emit(self.group.group_id))
|
|
|
self.pause_btn = QPushButton("⏸ 暫停")
|
|
|
self.pause_btn.setStyleSheet(BTN.format(bg='#F57F17', fg='white', hover='#F9A825'))
|
|
|
self.pause_btn.clicked.connect(
|
|
|
lambda: self.pause_requested.emit(self.group.group_id))
|
|
|
self.stop_btn = QPushButton("■ 停止")
|
|
|
self.stop_btn.setStyleSheet(BTN.format(bg='#C62828', fg='white', hover='#D32F2F'))
|
|
|
self.stop_btn.clicked.connect(
|
|
|
lambda: self.stop_requested.emit(self.group.group_id))
|
|
|
mid_right.addWidget(self.start_btn)
|
|
|
mid_right.addWidget(self.pause_btn)
|
|
|
mid_right.addWidget(self.stop_btn)
|
|
|
mid_right.addStretch()
|
|
|
|
|
|
mid_body.addLayout(mid_left, 1)
|
|
|
mid_body.addLayout(mid_right)
|
|
|
mid.addLayout(mid_body)
|
|
|
|
|
|
# ============================
|
|
|
# 選取與分組(2x2 按鈕)
|
|
|
# ============================
|
|
|
right = QVBoxLayout()
|
|
|
right.setSpacing(3)
|
|
|
|
|
|
right_title = QLabel("選取與分組")
|
|
|
right_title.setStyleSheet(TITLE)
|
|
|
right.addWidget(right_title)
|
|
|
|
|
|
self.drone_list_label = QLabel("尚未分配")
|
|
|
self.drone_list_label.setStyleSheet("color: #888; font-size: 11px;")
|
|
|
self.drone_list_label.setWordWrap(True)
|
|
|
right.addWidget(self.drone_list_label)
|
|
|
|
|
|
# 2x2 按鈕網格
|
|
|
grid_r1 = QHBoxLayout()
|
|
|
grid_r1.setSpacing(3)
|
|
|
assign_btn = QPushButton("編輯分配")
|
|
|
assign_btn.setStyleSheet(BTN.format(bg='#555', fg='#DDD', hover='#666'))
|
|
|
assign_btn.clicked.connect(
|
|
|
lambda: self.assign_drones_requested.emit(self.group.group_id))
|
|
|
clear_btn = QPushButton("清除分組")
|
|
|
clear_btn.setStyleSheet(BTN.format(bg='#777', fg='white', hover='#888'))
|
|
|
clear_btn.clicked.connect(
|
|
|
lambda: self.clear_group_requested.emit(self.group.group_id))
|
|
|
grid_r1.addWidget(assign_btn)
|
|
|
grid_r1.addWidget(clear_btn)
|
|
|
right.addLayout(grid_r1)
|
|
|
|
|
|
grid_r2 = QHBoxLayout()
|
|
|
grid_r2.setSpacing(3)
|
|
|
box_btn = QPushButton("框選")
|
|
|
box_btn.setStyleSheet(BTN.format(bg='#64B5F6', fg='white', hover='#42A5F5'))
|
|
|
box_btn.clicked.connect(
|
|
|
lambda: self.box_select_requested.emit(self.group.group_id))
|
|
|
all_btn = QPushButton("全選")
|
|
|
all_btn.setStyleSheet(BTN.format(bg='#64B5F6', fg='white', hover='#42A5F5'))
|
|
|
all_btn.clicked.connect(
|
|
|
lambda: self.select_all_requested.emit(self.group.group_id))
|
|
|
grid_r2.addWidget(box_btn)
|
|
|
grid_r2.addWidget(all_btn)
|
|
|
right.addLayout(grid_r2)
|
|
|
|
|
|
right.addStretch()
|
|
|
|
|
|
# ============================
|
|
|
# 第四欄:任務參數
|
|
|
# ============================
|
|
|
param_col = QVBoxLayout()
|
|
|
param_col.setSpacing(2)
|
|
|
|
|
|
param_title = QLabel("任務參數")
|
|
|
param_title.setStyleSheet(TITLE)
|
|
|
param_col.addWidget(param_title)
|
|
|
|
|
|
INPUT = ("QLineEdit { background-color: #333; color: #DDD; "
|
|
|
"border: 1px solid #555; border-radius: 3px; "
|
|
|
"padding: 1px 4px; font-size: 11px; }")
|
|
|
|
|
|
# 每種任務類型的參數定義: (key, label, default_value)
|
|
|
self._param_defs = {
|
|
|
'M_FORMATION': [
|
|
|
('spacing', '間距 (m)', '5.0'),
|
|
|
('base_altitude', '基準高度 (m)', '10.0'),
|
|
|
('altitude_diff', '高低差 (m)', '2.0'),
|
|
|
],
|
|
|
'CIRCLE_FORMATION': [
|
|
|
('radius', '半徑 (m)', '10.0'),
|
|
|
('altitude', '高度 (m)', '10.0'),
|
|
|
('start_angle', '起始角 (°)', '0'),
|
|
|
],
|
|
|
'LEADER_FOLLOWER': [
|
|
|
('lateral_offset', '橫向偏移 (m)', '3.0'),
|
|
|
('longitudinal_spacing', '縱向間距 (m)', '5.0'),
|
|
|
('altitude', '高度 (m)', '10.0'),
|
|
|
],
|
|
|
'GRID_SWEEP': [
|
|
|
('line_spacing', '掃描線距 (m)', '5.0'),
|
|
|
('altitude', '高度 (m)', '10.0'),
|
|
|
],
|
|
|
}
|
|
|
|
|
|
# 建立所有參數列的 widget(先全部建好,切換時顯示/隱藏)
|
|
|
self._param_widgets = {} # key → (label_widget, input_widget)
|
|
|
self._param_rows = [] # 所有 row layout 對應的 container widget
|
|
|
|
|
|
for mission_type, defs in self._param_defs.items():
|
|
|
for key, label_text, default in defs:
|
|
|
if key in self._param_widgets:
|
|
|
continue # 同名參數只建一次
|
|
|
row_w = QWidget()
|
|
|
row_l = QHBoxLayout(row_w)
|
|
|
row_l.setContentsMargins(0, 0, 0, 0)
|
|
|
row_l.setSpacing(3)
|
|
|
lbl = QLabel(label_text)
|
|
|
lbl.setStyleSheet(LBL)
|
|
|
inp = QLineEdit(default)
|
|
|
inp.setStyleSheet(INPUT)
|
|
|
inp.setFixedWidth(50)
|
|
|
row_l.addWidget(lbl, 1)
|
|
|
row_l.addWidget(inp)
|
|
|
param_col.addWidget(row_w)
|
|
|
self._param_widgets[key] = (row_w, inp)
|
|
|
self._param_rows.append(row_w)
|
|
|
|
|
|
param_col.addStretch()
|
|
|
|
|
|
# 初始顯示對應的參數
|
|
|
self._update_param_visibility()
|
|
|
|
|
|
# 當任務類型切換時更新參數顯示
|
|
|
self.type_combo.currentTextChanged.connect(self._update_param_visibility)
|
|
|
|
|
|
# ── 組裝四欄:控制指令 > 任務規劃 > 任務參數 > 選取與分組 ──
|
|
|
cols.addLayout(left, 1)
|
|
|
cols.addWidget(self._make_sep())
|
|
|
cols.addLayout(mid, 1)
|
|
|
cols.addWidget(self._make_sep())
|
|
|
cols.addLayout(param_col, 1)
|
|
|
cols.addWidget(self._make_sep())
|
|
|
cols.addLayout(right, 1)
|
|
|
|
|
|
layout.addLayout(cols)
|
|
|
|
|
|
def update_drone_list(self):
|
|
|
"""更新無人機列表顯示"""
|
|
|
if not self.group.drone_ids:
|
|
|
self.drone_list_label.setText("尚未分配")
|
|
|
self.drone_list_label.setStyleSheet("color: #888; font-size: 11px;")
|
|
|
else:
|
|
|
sorted_ids = sorted(self.group.drone_ids,
|
|
|
key=lambda x: (x.split('_')[0], int(x.split('_')[1])))
|
|
|
self.drone_list_label.setText("、".join(sorted_ids))
|
|
|
self.drone_list_label.setStyleSheet(
|
|
|
f"color: {self.group.color}; font-size: 11px; font-weight: bold;")
|
|
|
|
|
|
def update_status(self):
|
|
|
"""更新任務狀態顯示"""
|
|
|
state = self.group.state
|
|
|
if self.group.planned_waypoints is None:
|
|
|
self.status_label.setText("○ 未規劃")
|
|
|
self.status_label.setStyleSheet("color: #888; font-size: 11px;")
|
|
|
elif state == MissionState.RUNNING:
|
|
|
self.status_label.setText("▶ 執行中")
|
|
|
self.status_label.setStyleSheet("color: #4CAF50; font-size: 11px; font-weight: bold;")
|
|
|
elif state == MissionState.PAUSED:
|
|
|
self.status_label.setText("⏸ 已暫停")
|
|
|
self.status_label.setStyleSheet("color: #FFA000; font-size: 11px; font-weight: bold;")
|
|
|
else:
|
|
|
n = len(self.group.drone_ids)
|
|
|
total_wps = sum(len(wps) for wps in self.group.planned_waypoints['waypoints'])
|
|
|
self.status_label.setText(f"● 已規劃 ({n}架/{total_wps}點)")
|
|
|
self.status_label.setStyleSheet(
|
|
|
f"color: {self.group.color}; font-size: 11px; font-weight: bold;")
|
|
|
|
|
|
def _update_param_visibility(self, _=None):
|
|
|
"""根據當前任務類型,顯示/隱藏對應的參數列"""
|
|
|
mission_type = self.type_combo.currentText()
|
|
|
visible_keys = {d[0] for d in self._param_defs.get(mission_type, [])}
|
|
|
for key, (row_w, _inp) in self._param_widgets.items():
|
|
|
row_w.setVisible(key in visible_keys)
|
|
|
|
|
|
def get_mission_params(self):
|
|
|
"""讀取當前顯示的參數值,回傳 dict"""
|
|
|
mission_type = self.type_combo.currentText()
|
|
|
params = {}
|
|
|
for key, _label, default in self._param_defs.get(mission_type, []):
|
|
|
if key in self._param_widgets:
|
|
|
_row_w, inp = self._param_widgets[key]
|
|
|
try:
|
|
|
params[key] = float(inp.text())
|
|
|
except ValueError:
|
|
|
params[key] = float(default)
|
|
|
return params
|
|
|
|
|
|
def update_mission_info(self, center_lat, center_lon, target_lat, target_lon):
|
|
|
"""更新中心點 / 目標點顯示"""
|
|
|
info_style = f"color: {self.group.color}; font-size: 11px; font-weight: bold;"
|
|
|
self.center_label.setText(f"中心: {center_lat:.6f}°, {center_lon:.6f}°")
|
|
|
self.center_label.setStyleSheet(info_style)
|
|
|
self.target_label.setText(f"目標: {target_lat:.6f}°, {target_lon:.6f}°")
|
|
|
self.target_label.setStyleSheet(info_style)
|
|
|
|
|
|
def clear_mission_info(self):
|
|
|
"""清除中心點 / 目標點顯示"""
|
|
|
self.center_label.setText("中心: --")
|
|
|
self.center_label.setStyleSheet("color: #AAA; font-size: 11px;")
|
|
|
self.target_label.setText("目標: --")
|
|
|
self.target_label.setStyleSheet("color: #AAA; font-size: 11px;")
|
|
|
|
|
|
def _on_takeoff(self):
|
|
|
try:
|
|
|
alt = float(self.alt_input.currentText())
|
|
|
except ValueError:
|
|
|
alt = 10.0
|
|
|
self.takeoff_requested.emit(self.group.group_id, alt)
|
|
|
|