diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..35410cac
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 00000000..105ce2da
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..05adead2
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..a0733a5c
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/project.iml b/.idea/project.iml
new file mode 100644
index 00000000..d0876a78
--- /dev/null
+++ b/.idea/project.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Doc/~$人机后勤输送路径规划系统“软件设计规格说明书.doc b/Doc/~$人机后勤输送路径规划系统“软件设计规格说明书.doc
deleted file mode 100644
index 3a1b11ea..00000000
Binary files a/Doc/~$人机后勤输送路径规划系统“软件设计规格说明书.doc and /dev/null differ
diff --git a/Src/command_center/__init__.py b/Src/command_center/__init__.py
index 122acb7e..43cacfc2 100644
--- a/Src/command_center/__init__.py
+++ b/Src/command_center/__init__.py
@@ -1 +1,5 @@
+# -*- coding: utf-8 -*-
+# File: __init__.py (in command_center)
+# Purpose: 将 command_center 目录标记为 Python 包。
+
# 指挥控制中心包
\ No newline at end of file
diff --git a/Src/command_center/communication/__init__.py b/Src/command_center/communication/__init__.py
index 9acca399..a7f61463 100644
--- a/Src/command_center/communication/__init__.py
+++ b/Src/command_center/communication/__init__.py
@@ -1,3 +1,7 @@
+# -*- coding: utf-8 -*-
+# File: __init__.py (in communication)
+# Purpose: 将 communication 目录标记为 Python 包,并可能导出其中的类。
+
from .mavlink_handler import MavlinkHandler
from .database_handler import DatabaseHandler
from .message_queue import MessageQueue
diff --git a/Src/command_center/communication/__pycache__/__init__.cpython-312.pyc b/Src/command_center/communication/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..a5c8ad02
Binary files /dev/null and b/Src/command_center/communication/__pycache__/__init__.cpython-312.pyc differ
diff --git a/Src/command_center/communication/__pycache__/communication_manager.cpython-312.pyc b/Src/command_center/communication/__pycache__/communication_manager.cpython-312.pyc
new file mode 100644
index 00000000..341b0a21
Binary files /dev/null and b/Src/command_center/communication/__pycache__/communication_manager.cpython-312.pyc differ
diff --git a/Src/command_center/communication/__pycache__/database_handler.cpython-312.pyc b/Src/command_center/communication/__pycache__/database_handler.cpython-312.pyc
new file mode 100644
index 00000000..3141d59a
Binary files /dev/null and b/Src/command_center/communication/__pycache__/database_handler.cpython-312.pyc differ
diff --git a/Src/command_center/communication/__pycache__/drone_connection_manager.cpython-312.pyc b/Src/command_center/communication/__pycache__/drone_connection_manager.cpython-312.pyc
new file mode 100644
index 00000000..821f230e
Binary files /dev/null and b/Src/command_center/communication/__pycache__/drone_connection_manager.cpython-312.pyc differ
diff --git a/Src/command_center/communication/__pycache__/mavlink_handler.cpython-312.pyc b/Src/command_center/communication/__pycache__/mavlink_handler.cpython-312.pyc
new file mode 100644
index 00000000..3a799178
Binary files /dev/null and b/Src/command_center/communication/__pycache__/mavlink_handler.cpython-312.pyc differ
diff --git a/Src/command_center/communication/__pycache__/message_queue.cpython-312.pyc b/Src/command_center/communication/__pycache__/message_queue.cpython-312.pyc
new file mode 100644
index 00000000..8856595a
Binary files /dev/null and b/Src/command_center/communication/__pycache__/message_queue.cpython-312.pyc differ
diff --git a/Src/command_center/communication/communication_manager.py b/Src/command_center/communication/communication_manager.py
index 5ffe8dfb..1d42d723 100644
--- a/Src/command_center/communication/communication_manager.py
+++ b/Src/command_center/communication/communication_manager.py
@@ -1,6 +1,12 @@
-import threading
+# -*- coding: utf-8 -*-
+# File: communication_manager.py
+# Purpose: 核心通信管理器,处理与后端、数据库和无人机的通信。
+# 管理连接、消息分发和回调注册。
+
+import json
import time
-from typing import Dict, Any, Optional, List
+from threading import Thread, Lock
+from typing import Dict, List, Callable, Optional, Any
from .mavlink_handler import MavlinkHandler
from .database_handler import DatabaseHandler
from .message_queue import MessageQueue
@@ -16,8 +22,11 @@ class CommunicationManager:
database=db_config['database']
)
self.drone_manager = DroneConnectionManager(self.database)
+ self.drones: Dict[str, dict] = {} # 存储无人机信息
+ self.callbacks: Dict[str, List[Callable]] = {} # 存储回调函数
self.running = False
- self.thread = None
+ self.lock = Lock()
+ self.update_thread = None
self.message_queue = MessageQueue()
def start(self) -> bool:
@@ -45,74 +54,92 @@ class CommunicationManager:
# 启动消息处理线程
self.running = True
- self.thread = threading.Thread(target=self._process_messages)
- self.thread.daemon = True
- self.thread.start()
+ self.update_thread = Thread(target=self._update_loop)
+ self.update_thread.daemon = True
+ self.update_thread.start()
return True
def stop(self):
"""停止通信管理器"""
self.running = False
- if self.thread:
- self.thread.join()
+ if self.update_thread:
+ self.update_thread.join()
self.mavlink.stop()
self.drone_manager.stop()
self.database.disconnect()
- def add_drone(self, drone_id: str, ip_address: str, port: int,
- connection_type: str = 'udp') -> bool:
- """添加新的无人机连接"""
- return self.database.add_drone_connection(drone_id, ip_address, port, connection_type)
-
- def connect_drone(self, drone_id: str) -> bool:
- """连接指定的无人机"""
- return self.drone_manager.connect_drone(drone_id)
-
- def disconnect_drone(self, drone_id: str):
- """断开指定无人机的连接"""
- self.drone_manager.disconnect_drone(drone_id)
-
- def get_connected_drones(self) -> List[str]:
- """获取所有已连接的无人机ID"""
- return self.drone_manager.get_connected_drones()
+ def register_callback(self, event: str, callback: Callable):
+ """注册回调函数"""
+ if event not in self.callbacks:
+ self.callbacks[event] = []
+ self.callbacks[event].append(callback)
- def is_drone_connected(self, drone_id: str) -> bool:
- """检查指定无人机是否已连接"""
- return self.drone_manager.is_drone_connected(drone_id)
-
- def send_command(self, drone_id: str, command_type: str, **kwargs) -> bool:
- """向指定无人机发送命令"""
- return self.drone_manager.send_command(drone_id, command_type, **kwargs)
-
- def _process_messages(self):
- """处理消息队列中的消息"""
+ def _update_loop(self):
+ """更新循环"""
while self.running:
try:
- msg = self.mavlink.get_message_queue().get(timeout=1.0)
- if msg:
- self._handle_message(msg)
+ self._update_drone_status()
+ time.sleep(0.1) # 100ms更新间隔
except Exception as e:
- print(f"消息处理错误: {str(e)}")
- time.sleep(1)
-
- def _handle_message(self, msg: Dict[str, Any]):
- """处理接收到的消息"""
- try:
- msg_type = msg['message'].get_type()
- msg_data = msg['message'].to_dict()
-
- # 保存消息到数据库
- self.database.save_mavlink_message(msg_type, msg_data)
-
- # 根据消息类型更新无人机状态
- if msg_type == 'GLOBAL_POSITION_INT':
- self._update_drone_position(msg_data)
- elif msg_type == 'SYS_STATUS':
- self._update_drone_status(msg_data)
-
- except Exception as e:
- print(f"消息处理失败: {str(e)}")
+ print(f"更新循环错误: {str(e)}")
+
+ def _update_drone_status(self):
+ """更新无人机状态"""
+ # TODO: 实现实际的通信逻辑
+ # 这里模拟一些数据更新
+ with self.lock:
+ for drone_id in self.drones:
+ self.drones[drone_id].update({
+ "battery": max(0, self.drones[drone_id].get("battery", 100) - 0.1),
+ "signal_strength": max(0, self.drones[drone_id].get("signal_strength", 100) - 0.2),
+ "latency": self.drones[drone_id].get("latency", 0) + 1
+ })
+
+ def add_drone(self, drone_id: str, info: dict):
+ """添加无人机"""
+ with self.lock:
+ self.drones[drone_id] = info
+ self._notify("drone_added", drone_id)
+
+ def remove_drone(self, drone_id: str):
+ """移除无人机"""
+ with self.lock:
+ if drone_id in self.drones:
+ del self.drones[drone_id]
+ self._notify("drone_removed", drone_id)
+
+ def update_drone(self, drone_id: str, info: dict):
+ """更新无人机信息"""
+ with self.lock:
+ if drone_id in self.drones:
+ self.drones[drone_id].update(info)
+ self._notify("drone_updated", drone_id)
+
+ def get_drone(self, drone_id: str) -> dict:
+ """获取无人机信息"""
+ with self.lock:
+ return self.drones.get(drone_id, {})
+
+ def get_all_drones(self) -> List[dict]:
+ """获取所有无人机信息"""
+ with self.lock:
+ return list(self.drones.values())
+
+ def send_command(self, drone_id: str, command: str, params: dict = None):
+ """发送控制命令"""
+ # TODO: 实现实际的命令发送逻辑
+ print(f"发送命令到无人机 {drone_id}: {command} {params}")
+ self._notify("command_sent", drone_id, {"command": command, "params": params})
+
+ def _notify(self, event: str, drone_id: str, data: dict = None):
+ """通知事件"""
+ if event in self.callbacks:
+ for callback in self.callbacks[event]:
+ try:
+ callback(drone_id, data)
+ except Exception as e:
+ print(f"回调函数执行错误: {str(e)}")
def _handle_heartbeat(self, msg):
"""处理心跳消息"""
@@ -135,37 +162,6 @@ class CommunicationManager:
'data': msg.to_dict()
})
- def _update_drone_position(self, position_data: Dict[str, Any]):
- """更新无人机位置信息"""
- query = """
- INSERT INTO drone_status
- (drone_id, latitude, longitude, altitude, timestamp)
- VALUES (%s, %s, %s, %s, %s)
- """
- params = (
- position_data.get('sysid', 'unknown'),
- position_data.get('lat', 0) / 1e7, # 转换为度
- position_data.get('lon', 0) / 1e7, # 转换为度
- position_data.get('alt', 0) / 1000, # 转换为米
- datetime.now()
- )
- self.database.execute_update(query, params)
-
- def _update_drone_status(self, status_data: Dict[str, Any]):
- """更新无人机状态信息"""
- query = """
- UPDATE drone_status
- SET battery_level = %s, status = %s
- WHERE drone_id = %s
- ORDER BY timestamp DESC LIMIT 1
- """
- params = (
- status_data.get('battery_remaining', 0),
- status_data.get('system_status', 'UNKNOWN'),
- status_data.get('sysid', 'unknown')
- )
- self.database.execute_update(query, params)
-
def get_drone_status(self, drone_id: str) -> Optional[Dict[str, Any]]:
"""获取指定无人机的状态"""
query = """
diff --git a/Src/command_center/communication/database_handler.py b/Src/command_center/communication/database_handler.py
index 8186f7bc..45f656f7 100644
--- a/Src/command_center/communication/database_handler.py
+++ b/Src/command_center/communication/database_handler.py
@@ -1,3 +1,7 @@
+# -*- coding: utf-8 -*-
+# File: database_handler.py
+# Purpose: 处理与数据库的交互,包括连接、数据读写(如用户信息、无人机数据、任务记录等)。
+
import mysql.connector
from mysql.connector import Error
from typing import Dict, List, Any, Optional
diff --git a/Src/command_center/communication/drone_connection_manager.py b/Src/command_center/communication/drone_connection_manager.py
index 42ec3c86..ca4d5043 100644
--- a/Src/command_center/communication/drone_connection_manager.py
+++ b/Src/command_center/communication/drone_connection_manager.py
@@ -1,9 +1,15 @@
+# -*- coding: utf-8 -*-
+# File: drone_connection_manager.py
+# Purpose: 管理与单个或多个无人机的直接连接(可能通过 MAVLink 或其他协议)。
+# 处理心跳、连接状态和底层数据收发。
+
from typing import Dict, List, Optional
from .mavlink_handler import MavlinkHandler
from .database_handler import DatabaseHandler
import threading
import time
from datetime import datetime, timedelta
+import socket
class DroneConnectionManager:
def __init__(self, db_handler: DatabaseHandler):
diff --git a/Src/command_center/communication/mavlink_handler.py b/Src/command_center/communication/mavlink_handler.py
index 2674e095..1472c25f 100644
--- a/Src/command_center/communication/mavlink_handler.py
+++ b/Src/command_center/communication/mavlink_handler.py
@@ -1,3 +1,7 @@
+# -*- coding: utf-8 -*-
+# File: mavlink_handler.py
+# Purpose: 处理 MAVLink 协议的编码、解码和消息处理。
+
import pymavlink.mavutil as mavutil
import threading
import time
diff --git a/Src/command_center/communication/message_queue.py b/Src/command_center/communication/message_queue.py
index 3e3db6cc..cd069032 100644
--- a/Src/command_center/communication/message_queue.py
+++ b/Src/command_center/communication/message_queue.py
@@ -1,3 +1,7 @@
+# -*- coding: utf-8 -*-
+# File: message_queue.py
+# Purpose: 提供线程安全的消息队列,用于模块间的异步通信。
+
import queue
import threading
from typing import Any, Optional
diff --git a/Src/command_center/main.py b/Src/command_center/main.py
index a81c28ef..77fe26ea 100644
--- a/Src/command_center/main.py
+++ b/Src/command_center/main.py
@@ -1,13 +1,17 @@
+# -*- coding: utf-8 -*-
+# File: main.py
+# Purpose: 主应用程序入口点,定义和启动指挥控制中心主窗口 (CommandCenterApp)。
+# 负责初始化UI、处理登录、创建数据模型和协调各个功能模块。
+
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTabWidget, QPushButton, QLabel,
QGroupBox, QComboBox, QSpinBox, QDoubleSpinBox,
- QProgressBar, QCheckBox, QMessageBox)
-from PyQt5.QtCore import Qt, pyqtSignal
-from PyQt5.QtGui import QFont
+ QProgressBar, QCheckBox, QMessageBox, QFileDialog)
+from PyQt5.QtCore import Qt, pyqtSignal, QThread, QObject
+from PyQt5.QtGui import QFont, QPixmap, QPainter, QPen, QColor
from ui.login_view import LoginView
-from ui.main_view import MainView
-from ui.map_view import MapView
+from ui.simple_map_view import SimpleMapView
from ui.threat_layer_view import ThreatLayerView
from ui.path_layer_view import PathLayerView
from ui.drone_list_view import DroneListView
@@ -15,8 +19,42 @@ from ui.drone_detail_view import DroneDetailView
from ui.status_dashboard import StatusDashboard
from ui.path_planning_view import PathPlanningView
from ui.algorithm_config_view import AlgorithmConfigView
-from ui.path_simulation_view import PathSimulationView
from communication.communication_manager import CommunicationManager
+from ui.base_map_view import BaseMapView
+from ui.map_data_model import MapDataModel
+
+class PathPlanningThread(QThread):
+ path_planned = pyqtSignal(list) # 规划完成后发出新路径
+
+ def __init__(self, simulator, drone_id, goal):
+ super().__init__()
+ self.simulator = simulator
+ self.drone_id = drone_id
+ self.goal = goal
+ self._running = True
+ self._replan_requested = False
+
+ def run(self):
+ while self._running:
+ if self._replan_requested:
+ self._replan_requested = False
+ path = self.simulator.path_planner.plan_path(
+ self.drone_id,
+ self.simulator.drones[self.drone_id]['position'],
+ self.goal
+ )
+ if path:
+ self.path_planned.emit(path)
+ self.msleep(100) # 避免CPU占用过高
+
+ def request_replan(self, goal=None):
+ if goal is not None:
+ self.goal = goal
+ self._replan_requested = True
+
+ def stop(self):
+ self._running = False
+ self.wait()
class CommandCenterApp(QMainWindow):
# 定义信号
@@ -42,16 +80,18 @@ class CommandCenterApp(QMainWindow):
# 创建标签页
self.tab_widget = QTabWidget()
+ # 创建地图数据模型
+ self.map_data_model = MapDataModel()
+
# 添加各个功能标签页
- self.tab_widget.addTab(MapView(), "地图视图")
- self.tab_widget.addTab(ThreatLayerView(), "威胁层")
- self.tab_widget.addTab(PathLayerView(), "路径层")
+ self.tab_widget.addTab(SimpleMapView(self.map_data_model), "地图视图")
+ self.tab_widget.addTab(ThreatLayerView(self.map_data_model), "威胁层")
+ self.tab_widget.addTab(PathLayerView(self.map_data_model), "路径层")
self.tab_widget.addTab(DroneListView(), "无人机列表")
self.tab_widget.addTab(DroneDetailView(), "无人机详情")
self.tab_widget.addTab(StatusDashboard(), "状态仪表板")
self.tab_widget.addTab(PathPlanningView(), "路径规划")
self.tab_widget.addTab(AlgorithmConfigView(), "算法配置")
- self.tab_widget.addTab(PathSimulationView(), "路径模拟")
main_layout.addWidget(self.tab_widget)
diff --git a/Src/command_center/path_planning/__pycache__/battlefield_simulator.cpython-312.pyc b/Src/command_center/path_planning/__pycache__/battlefield_simulator.cpython-312.pyc
new file mode 100644
index 00000000..b4264754
Binary files /dev/null and b/Src/command_center/path_planning/__pycache__/battlefield_simulator.cpython-312.pyc differ
diff --git a/Src/command_center/path_planning/__pycache__/hybrid_astar.cpython-312.pyc b/Src/command_center/path_planning/__pycache__/hybrid_astar.cpython-312.pyc
new file mode 100644
index 00000000..1c34980b
Binary files /dev/null and b/Src/command_center/path_planning/__pycache__/hybrid_astar.cpython-312.pyc differ
diff --git a/Src/command_center/path_planning/__pycache__/path_planner.cpython-312.pyc b/Src/command_center/path_planning/__pycache__/path_planner.cpython-312.pyc
new file mode 100644
index 00000000..83b6599a
Binary files /dev/null and b/Src/command_center/path_planning/__pycache__/path_planner.cpython-312.pyc differ
diff --git a/Src/command_center/path_planning/battlefield_simulator.py b/Src/command_center/path_planning/battlefield_simulator.py
new file mode 100644
index 00000000..e8f660a7
--- /dev/null
+++ b/Src/command_center/path_planning/battlefield_simulator.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+# File: battlefield_simulator.py
+# Purpose: 定义战场环境模拟器,可能包含地图、无人机状态、威胁等信息。
+
+import numpy as np
+import matplotlib.pyplot as plt
+from typing import List, Tuple, Dict
+import random
+from .path_planner import PathPlanner
+
+class BattlefieldSimulator:
+ def __init__(self, width: float = 100.0, height: float = 100.0):
+ self.width = width
+ self.height = height
+ self.obstacles: List[Tuple[float, float]] = []
+ self.drones: Dict[str, Dict] = {}
+ self.path_planner = PathPlanner()
+
+ def generate_battlefield(self,
+ num_buildings: int = 10,
+ num_barriers: int = 15,
+ num_vehicles: int = 5):
+ """生成战场环境"""
+ # 清空现有障碍物
+ self.obstacles.clear()
+
+ # 生成建筑物(矩形障碍物)
+ for _ in range(num_buildings):
+ x = random.uniform(0, self.width)
+ y = random.uniform(0, self.height)
+ width = random.uniform(5, 15)
+ height = random.uniform(5, 15)
+ self._add_rectangular_obstacle(x, y, width, height)
+
+ # 生成路障(点状障碍物)
+ for _ in range(num_barriers):
+ x = random.uniform(0, self.width)
+ y = random.uniform(0, self.height)
+ self.obstacles.append((x, y))
+
+ # 生成废弃车辆(点状障碍物)
+ for _ in range(num_vehicles):
+ x = random.uniform(0, self.width)
+ y = random.uniform(0, self.height)
+ self.obstacles.append((x, y))
+
+ # 更新路径规划器的障碍物
+ self.path_planner.update_obstacles(self.obstacles)
+
+ def _add_rectangular_obstacle(self, x: float, y: float, width: float, height: float):
+ """添加矩形障碍物"""
+ # 在矩形区域内生成多个点作为障碍物
+ for i in np.arange(x - width/2, x + width/2, 1.0):
+ for j in np.arange(y - height/2, y + height/2, 1.0):
+ if 0 <= i <= self.width and 0 <= j <= self.height:
+ self.obstacles.append((i, j))
+
+ def add_drone(self, drone_id: str, start_pos: Tuple[float, float, float]):
+ """添加无人机"""
+ self.drones[drone_id] = {
+ 'position': start_pos,
+ 'path': None,
+ 'status': 'standby'
+ }
+
+ def plan_path(self, drone_id: str, goal: Tuple[float, float, float]) -> bool:
+ """为无人机规划路径"""
+ if drone_id not in self.drones:
+ return False
+
+ start = self.drones[drone_id]['position']
+ path = self.path_planner.plan_path(drone_id, start, goal)
+
+ if path:
+ self.drones[drone_id]['path'] = path
+ return True
+ return False
+
+ def visualize(self, show_paths: bool = True):
+ """可视化战场环境"""
+ plt.figure(figsize=(10, 10))
+
+ # 绘制障碍物
+ x_obs = [obs[0] for obs in self.obstacles]
+ y_obs = [obs[1] for obs in self.obstacles]
+ plt.scatter(x_obs, y_obs, c='red', s=10, label='Obstacles')
+
+ # 绘制无人机和路径
+ for drone_id, drone in self.drones.items():
+ # 绘制无人机位置
+ pos = drone['position']
+ plt.scatter(pos[0], pos[1], c='blue', s=100, label=f'Drone {drone_id}')
+
+ # 绘制航向
+ dx = 2 * np.cos(pos[2])
+ dy = 2 * np.sin(pos[2])
+ plt.arrow(pos[0], pos[1], dx, dy, head_width=1, head_length=1, fc='blue', ec='blue')
+
+ # 绘制路径
+ if show_paths and drone['path']:
+ path = drone['path']
+ x_path = [p[0] for p in path]
+ y_path = [p[1] for p in path]
+ plt.plot(x_path, y_path, 'g--', label=f'Path {drone_id}')
+
+ plt.grid(True)
+ plt.legend()
+ plt.title('Battlefield Simulation')
+ plt.xlabel('X (m)')
+ plt.ylabel('Y (m)')
+ plt.axis('equal')
+ plt.show()
+
+ def simulate_movement(self, drone_id: str, step_size: float = 1.0):
+ """模拟无人机沿路径移动"""
+ if drone_id not in self.drones or not self.drones[drone_id]['path']:
+ return False
+
+ drone = self.drones[drone_id]
+ path = drone['path']
+ current_pos = drone['position']
+
+ # 找到当前位置在路径上的最近点
+ min_dist = float('inf')
+ next_idx = 0
+
+ for i, point in enumerate(path):
+ dist = np.sqrt((point[0] - current_pos[0])**2 +
+ (point[1] - current_pos[1])**2)
+ if dist < min_dist:
+ min_dist = dist
+ next_idx = i
+
+ # 移动到下一个点
+ if next_idx < len(path) - 1:
+ next_point = path[next_idx + 1]
+ self.drones[drone_id]['position'] = next_point
+ return True
+
+ return False
\ No newline at end of file
diff --git a/Src/command_center/path_planning/hybrid_astar.py b/Src/command_center/path_planning/hybrid_astar.py
new file mode 100644
index 00000000..072dd3cf
--- /dev/null
+++ b/Src/command_center/path_planning/hybrid_astar.py
@@ -0,0 +1,229 @@
+# -*- coding: utf-8 -*-
+# File: hybrid_astar.py
+# Purpose: 实现混合 A* 路径规划算法的具体逻辑。
+
+import numpy as np
+from typing import List, Tuple, Dict, Optional
+from dataclasses import dataclass
+import math
+import heapq
+
+@dataclass
+class Node:
+ x: float
+ y: float
+ theta: float # 航向角
+ g_cost: float # 从起点到当前节点的代价
+ h_cost: float # 从当前节点到终点的估计代价
+ parent: Optional['Node'] = None
+
+ @property
+ def f_cost(self) -> float:
+ return self.g_cost + self.h_cost
+
+class HybridAStar:
+ def __init__(self,
+ grid_size: float = 0.5,
+ max_steering_angle: float = math.pi/4,
+ steering_step: float = math.pi/12,
+ speed_step: float = 1.0,
+ max_speed: float = 5.0,
+ min_speed: float = -2.0):
+ self.grid_size = grid_size
+ self.max_steering_angle = max_steering_angle
+ self.steering_step = steering_step
+ self.speed_step = speed_step
+ self.max_speed = max_speed
+ self.min_speed = min_speed
+
+ def plan(self,
+ start: Tuple[float, float, float],
+ goal: Tuple[float, float, float],
+ obstacles: List[Tuple[float, float]],
+ vehicle_length: float = 4.0,
+ vehicle_width: float = 2.0) -> Optional[List[Tuple[float, float, float]]]:
+ """
+ 使用混合A*算法规划路径
+
+ 参数:
+ start: (x, y, theta) 起点位置和航向
+ goal: (x, y, theta) 终点位置和航向
+ obstacles: [(x, y), ...] 障碍物位置列表
+ vehicle_length: 车辆长度
+ vehicle_width: 车辆宽度
+
+ 返回:
+ 路径点列表 [(x, y, theta), ...] 或 None(如果找不到路径)
+ """
+ # 初始化开放列表和关闭列表
+ open_list: List[Node] = []
+ closed_set = set()
+
+ # 创建起点节点
+ start_node = Node(
+ x=start[0],
+ y=start[1],
+ theta=start[2],
+ g_cost=0,
+ h_cost=self._heuristic(start, goal)
+ )
+ open_list.append(start_node)
+
+ while open_list:
+ # 获取f值最小的节点
+ current = min(open_list, key=lambda n: n.f_cost)
+ open_list.remove(current)
+
+ # 检查是否到达目标
+ if self._is_goal_reached(current, goal):
+ return self._reconstruct_path(current)
+
+ # 将当前节点加入关闭列表
+ closed_set.add(self._get_grid_position(current))
+
+ # 扩展当前节点
+ for next_node in self._get_successors(current, goal, obstacles, vehicle_length, vehicle_width):
+ grid_pos = self._get_grid_position(next_node)
+
+ # 如果节点在关闭列表中,跳过
+ if grid_pos in closed_set:
+ continue
+
+ # 如果节点不在开放列表中,添加它
+ if not any(self._is_same_node(next_node, n) for n in open_list):
+ open_list.append(next_node)
+
+ return None
+
+ def _heuristic(self, current: Tuple[float, float, float], goal: Tuple[float, float, float]) -> float:
+ """计算启发式函数值"""
+ dx = goal[0] - current[0]
+ dy = goal[1] - current[1]
+ dtheta = abs(goal[2] - current[2])
+ return math.sqrt(dx*dx + dy*dy) + 0.5 * dtheta
+
+ def _is_goal_reached(self, current: Node, goal: Tuple[float, float, float]) -> bool:
+ """检查是否到达目标"""
+ dx = goal[0] - current.x
+ dy = goal[1] - current.y
+ dtheta = abs(goal[2] - current.theta)
+ return (math.sqrt(dx*dx + dy*dy) < self.grid_size and
+ dtheta < math.pi/6)
+
+ def _get_grid_position(self, node: Node) -> Tuple[int, int, int]:
+ """获取节点在网格中的位置"""
+ x = int(node.x / self.grid_size)
+ y = int(node.y / self.grid_size)
+ theta = int(node.theta / self.steering_step)
+ return (x, y, theta)
+
+ def _is_same_node(self, node1: Node, node2: Node) -> bool:
+ """检查两个节点是否相同"""
+ grid_pos1 = self._get_grid_position(node1)
+ grid_pos2 = self._get_grid_position(node2)
+ return grid_pos1 == grid_pos2
+
+ def _get_successors(self,
+ current: Node,
+ goal: Tuple[float, float, float],
+ obstacles: List[Tuple[float, float]],
+ vehicle_length: float,
+ vehicle_width: float) -> List[Node]:
+ """获取当前节点的后继节点"""
+ successors = []
+
+ # 尝试不同的转向角和速度
+ for steering in np.arange(-self.max_steering_angle,
+ self.max_steering_angle + self.steering_step,
+ self.steering_step):
+ for speed in np.arange(self.min_speed,
+ self.max_speed + self.speed_step,
+ self.speed_step):
+ # 计算下一个位置
+ next_x = current.x + speed * math.cos(current.theta)
+ next_y = current.y + speed * math.sin(current.theta)
+ next_theta = current.theta + speed * math.tan(steering) / vehicle_length
+
+ # 检查是否与障碍物碰撞
+ if self._check_collision((next_x, next_y, next_theta),
+ obstacles,
+ vehicle_length,
+ vehicle_width):
+ continue
+
+ # 创建新节点
+ next_node = Node(
+ x=next_x,
+ y=next_y,
+ theta=next_theta,
+ g_cost=current.g_cost + self._calculate_cost(current, (next_x, next_y, next_theta)),
+ h_cost=self._heuristic((next_x, next_y, next_theta), goal),
+ parent=current
+ )
+ successors.append(next_node)
+
+ return successors
+
+ def _check_collision(self,
+ pose: Tuple[float, float, float],
+ obstacles: List[Tuple[float, float]],
+ vehicle_length: float,
+ vehicle_width: float) -> bool:
+ """检查给定位置是否与障碍物碰撞"""
+ # 计算车辆四个角的位置
+ corners = self._get_vehicle_corners(pose, vehicle_length, vehicle_width)
+
+ # 检查每个角是否与障碍物碰撞
+ for corner in corners:
+ for obstacle in obstacles:
+ if math.sqrt((corner[0] - obstacle[0])**2 +
+ (corner[1] - obstacle[1])**2) < self.grid_size:
+ return True
+ return False
+
+ def _get_vehicle_corners(self,
+ pose: Tuple[float, float, float],
+ vehicle_length: float,
+ vehicle_width: float) -> List[Tuple[float, float]]:
+ """计算车辆四个角的位置"""
+ x, y, theta = pose
+ half_length = vehicle_length / 2
+ half_width = vehicle_width / 2
+
+ corners = [
+ (x + half_length * math.cos(theta) - half_width * math.sin(theta),
+ y + half_length * math.sin(theta) + half_width * math.cos(theta)),
+ (x + half_length * math.cos(theta) + half_width * math.sin(theta),
+ y + half_length * math.sin(theta) - half_width * math.cos(theta)),
+ (x - half_length * math.cos(theta) - half_width * math.sin(theta),
+ y - half_length * math.sin(theta) + half_width * math.cos(theta)),
+ (x - half_length * math.cos(theta) + half_width * math.sin(theta),
+ y - half_length * math.sin(theta) - half_width * math.cos(theta))
+ ]
+ return corners
+
+ def _calculate_cost(self,
+ current: Node,
+ next_pose: Tuple[float, float, float]) -> float:
+ """计算从当前节点到下一个节点的代价"""
+ # 距离代价
+ dx = next_pose[0] - current.x
+ dy = next_pose[1] - current.y
+ distance = math.sqrt(dx*dx + dy*dy)
+
+ # 转向代价
+ dtheta = abs(next_pose[2] - current.theta)
+
+ # 总代价
+ return distance + 0.5 * dtheta
+
+ def _reconstruct_path(self, goal_node: Node) -> List[Tuple[float, float, float]]:
+ """重建路径"""
+ path = []
+ current = goal_node
+
+ while current is not None:
+ path.append((current.x, current.y, current.theta))
+ current = current.parent
+
+ return list(reversed(path))
\ No newline at end of file
diff --git a/Src/command_center/path_planning/path_planner.py b/Src/command_center/path_planning/path_planner.py
new file mode 100644
index 00000000..26e6dd13
--- /dev/null
+++ b/Src/command_center/path_planning/path_planner.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+# File: path_planner.py
+# Purpose: 路径规划的核心逻辑,可能包含调用不同路径规划算法的接口。
+
+from typing import List, Tuple, Optional, Dict
+from .hybrid_astar import HybridAStar
+import numpy as np
+import json
+import time
+
+class PathPlanner:
+ def __init__(self):
+ self.planner = HybridAStar()
+ self.current_paths: Dict[str, List[Tuple[float, float, float]]] = {}
+ self.obstacles: List[Tuple[float, float]] = []
+
+ def plan_path(self,
+ drone_id: str,
+ start: Tuple[float, float, float],
+ goal: Tuple[float, float, float],
+ vehicle_length: float = 4.0,
+ vehicle_width: float = 2.0) -> Optional[List[Tuple[float, float, float]]]:
+ """
+ 为指定无人机规划路径
+
+ 参数:
+ drone_id: 无人机ID
+ start: (x, y, theta) 起点位置和航向
+ goal: (x, y, theta) 终点位置和航向
+ vehicle_length: 车辆长度
+ vehicle_width: 车辆宽度
+
+ 返回:
+ 路径点列表 [(x, y, theta), ...] 或 None(如果找不到路径)
+ """
+ # 规划路径
+ path = self.planner.plan(
+ start=start,
+ goal=goal,
+ obstacles=self.obstacles,
+ vehicle_length=vehicle_length,
+ vehicle_width=vehicle_width
+ )
+
+ if path:
+ self.current_paths[drone_id] = path
+ return path
+ return None
+
+ def update_obstacles(self, obstacles: List[Tuple[float, float]]):
+ """更新障碍物列表"""
+ self.obstacles = obstacles
+
+ def get_current_path(self, drone_id: str) -> Optional[List[Tuple[float, float, float]]]:
+ """获取指定无人机的当前路径"""
+ return self.current_paths.get(drone_id)
+
+ def clear_path(self, drone_id: str):
+ """清除指定无人机的路径"""
+ if drone_id in self.current_paths:
+ del self.current_paths[drone_id]
+
+ def smooth_path(self, path: List[Tuple[float, float, float]], weight_data: float = 0.5,
+ weight_smooth: float = 0.3, tolerance: float = 0.000001) -> List[Tuple[float, float, float]]:
+ """
+ 使用平滑算法优化路径
+
+ 参数:
+ path: 原始路径
+ weight_data: 数据项权重
+ weight_smooth: 平滑项权重
+ tolerance: 收敛阈值
+
+ 返回:
+ 平滑后的路径
+ """
+ if len(path) <= 2:
+ return path
+
+ # 将路径转换为numpy数组
+ path_array = np.array(path)
+
+ # 初始化平滑后的路径
+ smooth_path = path_array.copy()
+
+ # 迭代优化
+ change = tolerance
+ while change >= tolerance:
+ change = 0.0
+
+ # 对每个点进行优化(除了起点和终点)
+ for i in range(1, len(path) - 1):
+ for j in range(3): # x, y, theta
+ aux = smooth_path[i][j]
+ smooth_path[i][j] += weight_data * (path_array[i][j] - smooth_path[i][j])
+ smooth_path[i][j] += weight_smooth * (smooth_path[i-1][j] + smooth_path[i+1][j] - 2.0 * smooth_path[i][j])
+ change += abs(aux - smooth_path[i][j])
+
+ return [(x, y, theta) for x, y, theta in smooth_path]
+
+ def save_path(self, drone_id: str, filename: str):
+ """保存路径到文件"""
+ if drone_id in self.current_paths:
+ path_data = {
+ 'drone_id': drone_id,
+ 'timestamp': time.time(),
+ 'path': self.current_paths[drone_id]
+ }
+ with open(filename, 'w') as f:
+ json.dump(path_data, f)
+
+ def load_path(self, filename: str) -> Optional[str]:
+ """从文件加载路径"""
+ try:
+ with open(filename, 'r') as f:
+ path_data = json.load(f)
+ self.current_paths[path_data['drone_id']] = path_data['path']
+ return path_data['drone_id']
+ except:
+ return None
\ No newline at end of file
diff --git a/Src/command_center/path_planning/test_battlefield.py b/Src/command_center/path_planning/test_battlefield.py
new file mode 100644
index 00000000..5883af21
--- /dev/null
+++ b/Src/command_center/path_planning/test_battlefield.py
@@ -0,0 +1,138 @@
+import numpy as np
+from battlefield_simulator import BattlefieldSimulator
+import time
+
+def test_single_drone():
+ """测试单个无人机的路径规划"""
+ # 创建战场模拟器
+ simulator = BattlefieldSimulator(width=100, height=100)
+
+ # 生成战场环境
+ simulator.generate_battlefield(
+ num_buildings=8,
+ num_barriers=12,
+ num_vehicles=3
+ )
+
+ # 添加无人机
+ start_pos = (10, 10, 0) # (x, y, theta)
+ simulator.add_drone("drone1", start_pos)
+
+ # 设置目标点
+ goal = (80, 80, np.pi/2)
+
+ # 规划路径
+ success = simulator.plan_path("drone1", goal)
+ print(f"Path planning {'successful' if success else 'failed'}")
+
+ # 可视化结果
+ simulator.visualize()
+
+ # 模拟移动
+ if success:
+ print("Simulating movement...")
+ for _ in range(10): # 模拟10步移动
+ simulator.simulate_movement("drone1")
+ simulator.visualize()
+ time.sleep(0.5) # 暂停0.5秒以便观察
+
+def test_multiple_drones():
+ """测试多个无人机的协同路径规划"""
+ # 创建战场模拟器
+ simulator = BattlefieldSimulator(width=100, height=100)
+
+ # 生成战场环境
+ simulator.generate_battlefield(
+ num_buildings=10,
+ num_barriers=15,
+ num_vehicles=5
+ )
+
+ # 添加多个无人机
+ drones = {
+ "drone1": (10, 10, 0),
+ "drone2": (20, 10, np.pi/4),
+ "drone3": (10, 20, np.pi/2)
+ }
+
+ goals = {
+ "drone1": (80, 80, np.pi/2),
+ "drone2": (70, 70, np.pi/4),
+ "drone3": (90, 90, 0)
+ }
+
+ # 添加无人机并规划路径
+ for drone_id, start_pos in drones.items():
+ simulator.add_drone(drone_id, start_pos)
+ success = simulator.plan_path(drone_id, goals[drone_id])
+ print(f"Path planning for {drone_id}: {'successful' if success else 'failed'}")
+
+ # 可视化初始状态
+ simulator.visualize()
+
+ # 模拟移动
+ print("Simulating movement...")
+ for _ in range(15): # 模拟15步移动
+ for drone_id in drones.keys():
+ simulator.simulate_movement(drone_id)
+ simulator.visualize()
+ time.sleep(0.5) # 暂停0.5秒以便观察
+
+def test_dynamic_obstacles():
+ """测试动态障碍物情况下的路径规划"""
+ # 创建战场模拟器
+ simulator = BattlefieldSimulator(width=100, height=100)
+
+ # 生成初始战场环境
+ simulator.generate_battlefield(
+ num_buildings=5,
+ num_barriers=8,
+ num_vehicles=3
+ )
+
+ # 添加无人机
+ start_pos = (10, 10, 0)
+ simulator.add_drone("drone1", start_pos)
+
+ # 设置目标点
+ goal = (80, 80, np.pi/2)
+
+ # 规划初始路径
+ success = simulator.plan_path("drone1", goal)
+ print(f"Initial path planning: {'successful' if success else 'failed'}")
+
+ # 可视化初始状态
+ simulator.visualize()
+
+ # 模拟移动并动态添加障碍物
+ print("Simulating movement with dynamic obstacles...")
+ for i in range(10):
+ # 移动无人机
+ simulator.simulate_movement("drone1")
+
+ # 每3步添加新的障碍物
+ if i % 3 == 0:
+ # 在无人机当前位置附近添加障碍物
+ pos = simulator.drones["drone1"]["position"]
+ new_obstacle = (
+ pos[0] + np.random.uniform(-10, 10),
+ pos[1] + np.random.uniform(-10, 10)
+ )
+ simulator.obstacles.append(new_obstacle)
+ simulator.path_planner.update_obstacles(simulator.obstacles)
+
+ # 重新规划路径
+ simulator.plan_path("drone1", goal)
+
+ simulator.visualize()
+ time.sleep(0.5)
+
+if __name__ == "__main__":
+ print("Testing single drone scenario...")
+ test_single_drone()
+
+ print("\nTesting multiple drones scenario...")
+ test_multiple_drones()
+
+ print("\nTesting dynamic obstacles scenario...")
+ test_dynamic_obstacles()
\ No newline at end of file
diff --git a/Src/command_center/ui/__init__.py b/Src/command_center/ui/__init__.py
index db1f1c7a..94f9d52d 100644
--- a/Src/command_center/ui/__init__.py
+++ b/Src/command_center/ui/__init__.py
@@ -1 +1,7 @@
-# UI组件包
\ No newline at end of file
+# -*- coding: utf-8 -*-
+# File: __init__.py (in ui)
+# Purpose: 将 ui 目录标记为 Python 包。
+
+# UI组件包
+
+# UI package initialization
\ No newline at end of file
diff --git a/Src/command_center/ui/__pycache__/__init__.cpython-312.pyc b/Src/command_center/ui/__pycache__/__init__.cpython-312.pyc
index d489b6e5..c30c3323 100644
Binary files a/Src/command_center/ui/__pycache__/__init__.cpython-312.pyc and b/Src/command_center/ui/__pycache__/__init__.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/algorithm_config_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/algorithm_config_view.cpython-312.pyc
index cc912ff5..f6809f9d 100644
Binary files a/Src/command_center/ui/__pycache__/algorithm_config_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/algorithm_config_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/base_map_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/base_map_view.cpython-312.pyc
new file mode 100644
index 00000000..0200506d
Binary files /dev/null and b/Src/command_center/ui/__pycache__/base_map_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/battlefield_map_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/battlefield_map_view.cpython-312.pyc
new file mode 100644
index 00000000..82d15880
Binary files /dev/null and b/Src/command_center/ui/__pycache__/battlefield_map_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/drone_detail_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/drone_detail_view.cpython-312.pyc
index ccde15eb..285dc9ce 100644
Binary files a/Src/command_center/ui/__pycache__/drone_detail_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/drone_detail_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/drone_list_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/drone_list_view.cpython-312.pyc
index 48140a8a..d00e5229 100644
Binary files a/Src/command_center/ui/__pycache__/drone_list_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/drone_list_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/login_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/login_view.cpython-312.pyc
index e840d68c..4a511513 100644
Binary files a/Src/command_center/ui/__pycache__/login_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/login_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/main_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/main_view.cpython-312.pyc
index b781628c..adcd8af8 100644
Binary files a/Src/command_center/ui/__pycache__/main_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/main_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/map_data_model.cpython-312.pyc b/Src/command_center/ui/__pycache__/map_data_model.cpython-312.pyc
new file mode 100644
index 00000000..d8cad968
Binary files /dev/null and b/Src/command_center/ui/__pycache__/map_data_model.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/map_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/map_view.cpython-312.pyc
index 4813c46c..009cdeb3 100644
Binary files a/Src/command_center/ui/__pycache__/map_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/map_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/matplotlib_canvas.cpython-312.pyc b/Src/command_center/ui/__pycache__/matplotlib_canvas.cpython-312.pyc
new file mode 100644
index 00000000..19422226
Binary files /dev/null and b/Src/command_center/ui/__pycache__/matplotlib_canvas.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/path_layer_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/path_layer_view.cpython-312.pyc
index c36f3922..929b15bc 100644
Binary files a/Src/command_center/ui/__pycache__/path_layer_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/path_layer_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/path_planning_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/path_planning_view.cpython-312.pyc
index ad35ab26..e7285e89 100644
Binary files a/Src/command_center/ui/__pycache__/path_planning_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/path_planning_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/path_simulation_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/path_simulation_view.cpython-312.pyc
index 300a9620..405ff3ca 100644
Binary files a/Src/command_center/ui/__pycache__/path_simulation_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/path_simulation_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/simple_map_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/simple_map_view.cpython-312.pyc
new file mode 100644
index 00000000..8acb7a20
Binary files /dev/null and b/Src/command_center/ui/__pycache__/simple_map_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/status_dashboard.cpython-312.pyc b/Src/command_center/ui/__pycache__/status_dashboard.cpython-312.pyc
index 02dc1706..3e03fa8c 100644
Binary files a/Src/command_center/ui/__pycache__/status_dashboard.cpython-312.pyc and b/Src/command_center/ui/__pycache__/status_dashboard.cpython-312.pyc differ
diff --git a/Src/command_center/ui/__pycache__/threat_layer_view.cpython-312.pyc b/Src/command_center/ui/__pycache__/threat_layer_view.cpython-312.pyc
index a5fefa0f..7d269f89 100644
Binary files a/Src/command_center/ui/__pycache__/threat_layer_view.cpython-312.pyc and b/Src/command_center/ui/__pycache__/threat_layer_view.cpython-312.pyc differ
diff --git a/Src/command_center/ui/algorithm_config_view.py b/Src/command_center/ui/algorithm_config_view.py
index 265299c4..1d04657d 100644
--- a/Src/command_center/ui/algorithm_config_view.py
+++ b/Src/command_center/ui/algorithm_config_view.py
@@ -1,6 +1,6 @@
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QGroupBox, QFormLayout, QComboBox,
- QSpinBox, QDoubleSpinBox, QCheckBox)
+ QSpinBox, QDoubleSpinBox, QCheckBox, QMessageBox)
from PyQt5.QtCore import Qt
class AlgorithmConfigView(QWidget):
@@ -95,9 +95,11 @@ class AlgorithmConfigView(QWidget):
self.reset_config_btn.clicked.connect(self.reset_config)
def save_config(self):
- # TODO: 实现保存配置功能
+ QMessageBox.information(self, "功能确认", "保存配置 (功能待实现)")
+ # TODO: Implement save config logic
pass
def reset_config(self):
- # TODO: 实现重置配置功能
+ QMessageBox.information(self, "功能确认", "重置配置 (功能待实现)")
+ # TODO: Implement reset config logic (reset UI fields to default)
pass
\ No newline at end of file
diff --git a/Src/command_center/ui/base_map_view.py b/Src/command_center/ui/base_map_view.py
new file mode 100644
index 00000000..a72685b8
--- /dev/null
+++ b/Src/command_center/ui/base_map_view.py
@@ -0,0 +1,296 @@
+# -*- coding: utf-8 -*-
+# File: base_map_view.py
+# Purpose: 定义基础地图视图类,提供地图加载、显示、缩放、平移和点击处理等通用功能。
+# 作为 SimpleMapView, ThreatLayerView, PathLayerView 的父类。
+
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QPushButton, QToolBar, QAction, QFileDialog)
+from PyQt5.QtCore import Qt, pyqtSignal, QPointF, QRect, QRectF
+from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor, QTransform, QCursor, QMouseEvent
+
+class BaseMapView(QWidget):
+ # 定义信号
+ map_loaded = pyqtSignal()
+ threat_points_changed = pyqtSignal(list)
+
+ def __init__(self, map_data_model):
+ super().__init__()
+ self.map_data_model = map_data_model
+ self.scale_factor = 1.0
+ # Panning state
+ self.panning = False
+ self.last_pan_point = QPointF()
+ self.map_offset = QPointF(0, 0) # Top-left corner offset of the map relative to the widget
+ self.init_ui()
+
+ def init_ui(self):
+ # 创建主布局
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(0, 0, 0, 0) # Remove margins
+
+ # 创建工具栏
+ toolbar = QToolBar()
+ toolbar.setMovable(False)
+
+ # 添加工具栏按钮
+ self.load_map_action = QAction("加载地图", self)
+ self.zoom_in_action = QAction("放大", self)
+ self.zoom_out_action = QAction("缩小", self)
+ self.pan_action = QAction("平移", self)
+ self.pan_action.setCheckable(True) # Make pan action checkable
+
+ toolbar.addAction(self.load_map_action)
+ toolbar.addAction(self.zoom_in_action)
+ toolbar.addAction(self.zoom_out_action)
+ toolbar.addAction(self.pan_action)
+
+ main_layout.addWidget(toolbar)
+
+ # 创建地图显示区域
+ self.map_label = QLabel("请加载地图图片")
+ self.map_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) # Align top-left for panning
+ self.map_label.setStyleSheet("background-color: #333; border: none;") # Darker background, no border
+ main_layout.addWidget(self.map_label)
+
+ # 设置布局
+ self.setLayout(main_layout)
+
+ # 连接信号
+ print("BaseMapView: Connecting load_map_action to load_map_image") # Debug print
+ self.load_map_action.triggered.connect(self.load_map_image)
+ print("BaseMapView: Connecting zoom_in_action to zoom_in") # Debug print
+ self.zoom_in_action.triggered.connect(self.zoom_in)
+ print("BaseMapView: Connecting zoom_out_action to zoom_out") # Debug print
+ self.zoom_out_action.triggered.connect(self.zoom_out)
+ print("BaseMapView: Connecting pan_action to toggle_pan_mode") # Debug print
+ self.pan_action.triggered.connect(self.toggle_pan_mode) # Connect pan toggle
+ print("BaseMapView: Connecting map_data_model.data_changed to update_map") # Debug print
+ self.map_data_model.data_changed.connect(self.update_map)
+
+ def load_map_image(self):
+ print("BaseMapView: load_map_image called") # Debug print: function entry
+ file_name, _ = QFileDialog.getOpenFileName(self, "选择地图图片", "", "Images (*.png *.jpg *.bmp)")
+ if file_name:
+ pixmap = QPixmap(file_name)
+ if pixmap.isNull():
+ print("BaseMapView: Error - Loaded pixmap is null!")
+ self.map_data_model.set_map(None) # Ensure model knows it's invalid
+ else:
+ print(f"BaseMapView: Map loaded successfully, size: {pixmap.width()}x{pixmap.height()}")
+ self.map_data_model.set_map(pixmap)
+ self.reset_view() # Reset zoom/pan on new map load
+ self.map_loaded.emit()
+
+ def reset_view(self):
+ print("BaseMapView: reset_view called") # Debug print
+ if not self.map_data_model.map_pixmap or self.map_data_model.map_pixmap.isNull():
+ print("BaseMapView: reset_view - No valid map pixmap found.")
+ self.scale_factor = 1.0
+ self.map_offset = QPointF(0, 0)
+ self.update_map()
+ return
+
+ # Calculate scale factor to fit the map inside the label
+ label_size = self.map_label.size()
+ map_size = self.map_data_model.map_pixmap.size()
+ print(f"BaseMapView: reset_view - Label size: {label_size.width()}x{label_size.height()}, Map size: {map_size.width()}x{map_size.height()}")
+
+ if label_size.isEmpty() or map_size.isEmpty() or map_size.width() == 0 or map_size.height() == 0:
+ print("BaseMapView: reset_view - Warning: Invalid label or map size for scaling.")
+ self.scale_factor = 1.0 # Default if sizes are invalid
+ else:
+ scale_x = label_size.width() / map_size.width()
+ scale_y = label_size.height() / map_size.height()
+ self.scale_factor = min(scale_x, scale_y) # Use min to fit entirely (KeepAspectRatio)
+ print(f"BaseMapView: reset_view - Calculated initial scale factor: {self.scale_factor}")
+
+ # Calculate offset to center the scaled map
+ scaled_map_width = map_size.width() * self.scale_factor
+ scaled_map_height = map_size.height() * self.scale_factor
+ offset_x = (label_size.width() - scaled_map_width) / 2
+ offset_y = (label_size.height() - scaled_map_height) / 2
+ self.map_offset = QPointF(offset_x, offset_y)
+ print(f"BaseMapView: reset_view - Calculated initial offset: ({offset_x}, {offset_y})")
+
+ self.update_map()
+
+ def update_map(self):
+ # print("BaseMapView: update_map called") # Can be noisy, enable if needed
+ if self.map_data_model.map_pixmap and not self.map_data_model.map_pixmap.isNull():
+ original_pixmap = self.map_data_model.map_pixmap
+ label_size = self.map_label.size()
+
+ # Calculate scaled size
+ scaled_width = int(original_pixmap.width() * self.scale_factor)
+ scaled_height = int(original_pixmap.height() * self.scale_factor)
+
+ if scaled_width <= 0 or scaled_height <= 0:
+ print(f"BaseMapView: update_map - Warning: Invalid scaled size ({scaled_width}x{scaled_height}), skipping draw.")
+ return
+
+ # Create a new pixmap for drawing
+ # Ensure display_pixmap is at least the size of the label to avoid issues
+ display_pixmap_width = max(scaled_width + int(abs(self.map_offset.x())) + 1, label_size.width())
+ display_pixmap_height = max(scaled_height + int(abs(self.map_offset.y())) + 1, label_size.height())
+ display_pixmap = QPixmap(display_pixmap_width, display_pixmap_height)
+ display_pixmap.fill(Qt.transparent) # Use transparent background
+
+ # print(f"BaseMapView: update_map - Scale: {self.scale_factor:.2f}, Offset: ({self.map_offset.x():.1f}, {self.map_offset.y():.1f})")
+ # print(f"BaseMapView: update_map - Scaled map size: {scaled_width}x{scaled_height}")
+ # print(f"BaseMapView: update_map - Display pixmap size: {display_pixmap_width}x{display_pixmap_height}")
+
+ painter = QPainter(display_pixmap)
+ painter.setRenderHint(QPainter.SmoothPixmapTransform) # Improve scaling quality
+
+ # Define the target rectangle for the scaled map within the display_pixmap
+ # Ensure offset is integer for QRect constructor
+ target_rect = QRect(int(self.map_offset.x()), int(self.map_offset.y()),
+ scaled_width, scaled_height)
+
+ # Draw the scaled original map
+ painter.drawPixmap(target_rect, original_pixmap, original_pixmap.rect())
+
+ # --- Draw overlays ---
+ # (Code for drawing overlays remains the same)
+ # ... (threats, start, goal, paths) ...
+ # Apply scale and offset transform to the painter for drawing overlays
+ transform = QTransform()
+ transform.translate(self.map_offset.x(), self.map_offset.y())
+ transform.scale(self.scale_factor, self.scale_factor)
+ painter.setTransform(transform)
+
+ # Draw threats
+ if self.map_data_model.threat_points:
+ point_size = max(1.0, 5.0 / self.scale_factor) # Ensure point size is at least 1
+ pen = QPen(QColor(255, 0, 0), point_size) # Keep pen size visually constant
+ painter.setPen(pen)
+ for point in self.map_data_model.threat_points:
+ painter.drawPoint(QPointF(point[0], point[1])) # Draw at original coords (transformed)
+
+ # Draw start point
+ if self.map_data_model.start_point:
+ point_size = max(1.0, 5.0 / self.scale_factor)
+ pen = QPen(QColor(0, 255, 0), point_size)
+ painter.setPen(pen)
+ painter.drawPoint(QPointF(self.map_data_model.start_point[0], self.map_data_model.start_point[1]))
+
+ # Draw goal point
+ if self.map_data_model.goal_point:
+ point_size = max(1.0, 5.0 / self.scale_factor)
+ pen = QPen(QColor(0, 0, 255), point_size)
+ painter.setPen(pen)
+ painter.drawPoint(QPointF(self.map_data_model.goal_point[0], self.map_data_model.goal_point[1]))
+
+ # Draw paths
+ if self.map_data_model.paths:
+ line_width = max(0.5, 2.0 / self.scale_factor) # Use 0 for cosmetic pen if needed, ensure > 0
+ pen = QPen(QColor(0, 255, 255), line_width)
+ painter.setPen(pen)
+ for path in self.map_data_model.paths:
+ if len(path) > 1:
+ points = [QPointF(p[0], p[1]) for p in path]
+ painter.drawPolyline(*points) # Draw lines between original coords
+
+ painter.end()
+
+ # Set the potentially larger pixmap on the label
+ self.map_label.setPixmap(display_pixmap)
+ # print("BaseMapView: update_map - Pixmap set on label")
+ else:
+ print("BaseMapView: update_map - No map pixmap to draw.")
+ self.map_label.setText("请加载地图图片")
+ self.map_label.setPixmap(QPixmap()) # Clear pixmap if none loaded
+
+ def resizeEvent(self, event):
+ super().resizeEvent(event)
+ # We might need to call update_map if label size affects how map is displayed
+ # For now, assume update_map is called when needed by zoom/pan
+ self.update_map() # Update map on resize
+
+ def zoom(self, factor):
+ # Store previous scale and mouse position (relative to widget)
+ prev_scale = self.scale_factor
+ mouse_pos = self.map_label.mapFromGlobal(QCursor.pos())
+
+ # Calculate map point under mouse before zoom
+ map_point_before_zoom = (mouse_pos - self.map_offset) / prev_scale
+
+ # Update scale factor
+ self.scale_factor *= factor
+ # Add limits to scaling if needed
+ # self.scale_factor = max(0.1, min(self.scale_factor, 10.0))
+
+ # Calculate map point under mouse after zoom (if offset remained the same)
+ map_point_after_zoom_same_offset = map_point_before_zoom * self.scale_factor
+
+ # Adjust offset to keep the point under the mouse cursor stationary
+ self.map_offset = mouse_pos - map_point_after_zoom_same_offset
+
+ self.update_map()
+
+ def zoom_in(self):
+ print("BaseMapView: zoom_in called") # Debug print
+ self.zoom(1.2) # Zoom in by 20%
+
+ def zoom_out(self):
+ print("BaseMapView: zoom_out called") # Debug print
+ self.zoom(1 / 1.2) # Zoom out by 20%
+
+ def toggle_pan_mode(self, checked):
+ print(f"BaseMapView: toggle_pan_mode called, checked: {checked}") # Debug print
+ self.panning = checked
+ if self.panning:
+ self.map_label.setCursor(Qt.OpenHandCursor)
+ else:
+ self.map_label.setCursor(Qt.ArrowCursor)
+
+ def mousePressEvent(self, event):
+ if self.panning and event.button() == Qt.LeftButton:
+ self.last_pan_point = event.pos()
+ self.map_label.setCursor(Qt.ClosedHandCursor)
+ event.accept()
+ elif not self.panning and event.button() == Qt.LeftButton:
+ label_pos = self.map_label.mapFromParent(event.pos())
+ if self.map_label.pixmap() and not self.map_label.pixmap().isNull() and self.map_label.rect().contains(label_pos):
+ # Calculate map point under mouse in original map coordinates
+ map_point_f = (label_pos - self.map_offset) / self.scale_factor
+
+ # Check if click is within the actual map boundaries (optional but good practice)
+ original_map_rect = QRectF(0, 0, self.map_data_model.map_pixmap.width(), self.map_data_model.map_pixmap.height())
+ if original_map_rect.contains(map_point_f):
+ # Call the handler method (implemented by subclasses)
+ self.handle_map_click(map_point_f)
+ event.accept() # Indicate event was handled
+ return # Don't ignore if handled
+
+ # If click wasn't on map or not handled, ignore
+ event.ignore()
+ else:
+ # Ignore other buttons or if panning is off but not LeftButton
+ event.ignore()
+
+ def mouseMoveEvent(self, event):
+ if self.panning and event.buttons() & Qt.LeftButton:
+ delta = event.pos() - self.last_pan_point
+ self.map_offset += delta # Update offset
+ self.last_pan_point = event.pos()
+ self.update_map() # Redraw map at new offset
+ event.accept()
+ else:
+ event.ignore()
+
+ def mouseReleaseEvent(self, event):
+ if self.panning and event.button() == Qt.LeftButton:
+ self.map_label.setCursor(Qt.OpenHandCursor) # Change back to open hand
+ event.accept()
+ else:
+ event.ignore()
+
+ def handle_map_click(self, map_point):
+ """Subclasses should override this to handle clicks on the map."""
+ # Default implementation does nothing
+ pass
+
+# Need to import QRect, QPointF, QTransform, QCursor, QMouseEvent at the top
+from PyQt5.QtCore import QRect, QRectF
+from PyQt5.QtGui import QCursor, QMouseEvent
\ No newline at end of file
diff --git a/Src/command_center/ui/drone_detail_view.py b/Src/command_center/ui/drone_detail_view.py
index cc0d6731..a68e80e8 100644
--- a/Src/command_center/ui/drone_detail_view.py
+++ b/Src/command_center/ui/drone_detail_view.py
@@ -1,12 +1,188 @@
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QGroupBox, QFormLayout)
-from PyQt5.QtCore import Qt
+ QPushButton, QGroupBox, QFormLayout, QProgressBar,
+ QComboBox, QSpinBox, QDoubleSpinBox, QDialog,
+ QMessageBox)
+from PyQt5.QtCore import Qt, pyqtSignal
+from PyQt5.QtGui import QColor, QPalette
+
+class ParameterDialog(QDialog):
+ def __init__(self, title, min_value, max_value, unit, parent=None):
+ super().__init__(parent)
+ self.title = title
+ self.min_value = min_value
+ self.max_value = max_value
+ self.unit = unit
+ self.init_ui()
+
+ def init_ui(self):
+ self.setWindowTitle(self.title)
+ layout = QFormLayout()
+
+ self.value_spin = QDoubleSpinBox()
+ self.value_spin.setRange(self.min_value, self.max_value)
+ self.value_spin.setSuffix(f" {self.unit}")
+ self.value_spin.setDecimals(1)
+
+ layout.addRow("值:", self.value_spin)
+
+ buttons = QHBoxLayout()
+ ok_button = QPushButton("确定")
+ cancel_button = QPushButton("取消")
+
+ ok_button.clicked.connect(self.accept)
+ cancel_button.clicked.connect(self.reject)
+
+ buttons.addWidget(ok_button)
+ buttons.addWidget(cancel_button)
+ layout.addRow(buttons)
+
+ self.setLayout(layout)
+
+ def get_value(self):
+ return self.value_spin.value()
+
+class PayloadDialog(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.init_ui()
+
+ def init_ui(self):
+ self.setWindowTitle("设置负载")
+ layout = QFormLayout()
+
+ self.type_combo = QComboBox()
+ self.type_combo.addItems(["无", "货物", "医疗物资", "救援设备", "监控设备"])
+
+ self.weight_spin = QDoubleSpinBox()
+ self.weight_spin.setRange(0, 100)
+ self.weight_spin.setSuffix(" kg")
+ self.weight_spin.setDecimals(1)
+
+ layout.addRow("负载类型:", self.type_combo)
+ layout.addRow("负载重量:", self.weight_spin)
+
+ buttons = QHBoxLayout()
+ ok_button = QPushButton("确定")
+ cancel_button = QPushButton("取消")
+
+ ok_button.clicked.connect(self.accept)
+ cancel_button.clicked.connect(self.reject)
+
+ buttons.addWidget(ok_button)
+ buttons.addWidget(cancel_button)
+ layout.addRow(buttons)
+
+ self.setLayout(layout)
+
+ def get_payload_info(self):
+ return {
+ "type": self.type_combo.currentText(),
+ "weight": self.weight_spin.value()
+ }
class DroneDetailView(QWidget):
- def __init__(self):
+ # 定义信号
+ control_command = pyqtSignal(str, str) # 命令类型, 参数
+
+ def __init__(self, comm_manager=None):
super().__init__()
+ self.comm_manager = comm_manager
+ self.current_drone_id = None
self.init_ui()
+ self.setup_communication()
+ def setup_communication(self):
+ """设置通信回调"""
+ if self.comm_manager:
+ self.comm_manager.register_callback("drone_updated", self.on_drone_updated)
+ self.comm_manager.register_callback("command_sent", self.on_command_sent)
+
+ def on_drone_updated(self, drone_id, data):
+ """处理无人机更新事件"""
+ if drone_id == self.current_drone_id:
+ self.update_drone_info(drone_id, self.comm_manager.get_drone(drone_id))
+
+ def on_command_sent(self, drone_id, data):
+ """处理命令发送事件"""
+ if drone_id == self.current_drone_id:
+ command = data.get("command", "")
+ params = data.get("params", {})
+ QMessageBox.information(self, "命令发送", f"命令 {command} 已发送到无人机 {drone_id}")
+
+ def set_drone(self, drone_id):
+ """设置当前显示的无人机"""
+ self.current_drone_id = drone_id
+ if self.comm_manager:
+ info = self.comm_manager.get_drone(drone_id)
+ self.update_drone_info(drone_id, info)
+
+ def send_control_command(self, command_type, params=None):
+ """发送控制命令"""
+ if self.current_drone_id and self.comm_manager:
+ self.comm_manager.send_command(self.current_drone_id, command_type, params)
+
+ def set_altitude(self):
+ """设置高度"""
+ dialog = ParameterDialog("设置高度", 0, 1000, "m", self)
+ if dialog.exec_() == QDialog.Accepted:
+ value = dialog.get_value()
+ self.send_control_command("set_altitude", {"value": value})
+
+ def set_speed(self):
+ """设置速度"""
+ dialog = ParameterDialog("设置速度", 0, 50, "m/s", self)
+ if dialog.exec_() == QDialog.Accepted:
+ value = dialog.get_value()
+ self.send_control_command("set_speed", {"value": value})
+
+ def set_heading(self):
+ """设置航向"""
+ dialog = ParameterDialog("设置航向", 0, 360, "°", self)
+ if dialog.exec_() == QDialog.Accepted:
+ value = dialog.get_value()
+ self.send_control_command("set_heading", {"value": value})
+
+ def set_payload(self):
+ """设置负载"""
+ dialog = PayloadDialog(self)
+ if dialog.exec_() == QDialog.Accepted:
+ info = dialog.get_payload_info()
+ self.send_control_command("set_payload", info)
+
+ def update_drone_info(self, drone_id, info):
+ """更新无人机信息"""
+ self.current_drone_id = drone_id
+
+ # 更新基本信息
+ self.id_label.setText(info.get("id", ""))
+ self.model_label.setText(info.get("model", ""))
+ self.status_label.setText(info.get("status", ""))
+ self.battery_label.setText(f"{info.get('battery', 0)}%")
+ self.group_label.setText(info.get("group", ""))
+ self.color_label.setText(info.get("color", ""))
+
+ # 更新位置信息
+ self.latitude_label.setText(f"{info.get('latitude', 0):.6f}")
+ self.longitude_label.setText(f"{info.get('longitude', 0):.6f}")
+ self.altitude_label.setText(f"{info.get('altitude', 0):.1f}m")
+ self.speed_label.setText(f"{info.get('speed', 0):.1f}m/s")
+ self.heading_label.setText(f"{info.get('heading', 0):.1f}°")
+
+ # 更新负载信息
+ self.payload_type_label.setText(info.get("payload_type", ""))
+ self.payload_weight_label.setText(f"{info.get('payload_weight', 0):.1f}kg")
+ self.payload_status_label.setText(info.get("payload_status", ""))
+
+ # 更新通信状态
+ self.signal_strength.setValue(info.get("signal_strength", 0))
+ self.latency_label.setText(f"{info.get('latency', 0)}ms")
+ self.packet_loss_label.setText(f"{info.get('packet_loss', 0):.1f}%")
+
+ # 更新任务信息
+ self.task_type_label.setText(info.get("task_type", ""))
+ self.task_status_label.setText(info.get("task_status", ""))
+ self.task_progress.setValue(info.get("task_progress", 0))
+
def init_ui(self):
# 创建主布局
main_layout = QVBoxLayout()
@@ -19,11 +195,15 @@ class DroneDetailView(QWidget):
self.model_label = QLabel("")
self.status_label = QLabel("")
self.battery_label = QLabel("")
+ self.group_label = QLabel("")
+ self.color_label = QLabel("")
basic_layout.addRow("ID:", self.id_label)
basic_layout.addRow("型号:", self.model_label)
basic_layout.addRow("状态:", self.status_label)
basic_layout.addRow("电量:", self.battery_label)
+ basic_layout.addRow("分组:", self.group_label)
+ basic_layout.addRow("颜色标识:", self.color_label)
basic_info_group.setLayout(basic_layout)
main_layout.addWidget(basic_info_group)
@@ -36,49 +216,129 @@ class DroneDetailView(QWidget):
self.longitude_label = QLabel("")
self.altitude_label = QLabel("")
self.speed_label = QLabel("")
+ self.heading_label = QLabel("")
position_layout.addRow("纬度:", self.latitude_label)
position_layout.addRow("经度:", self.longitude_label)
position_layout.addRow("高度:", self.altitude_label)
position_layout.addRow("速度:", self.speed_label)
+ position_layout.addRow("航向:", self.heading_label)
position_group.setLayout(position_layout)
main_layout.addWidget(position_group)
+ # 创建负载信息组
+ payload_group = QGroupBox("负载信息")
+ payload_layout = QFormLayout()
+
+ self.payload_type_label = QLabel("")
+ self.payload_weight_label = QLabel("")
+ self.payload_status_label = QLabel("")
+
+ payload_layout.addRow("负载类型:", self.payload_type_label)
+ payload_layout.addRow("负载重量:", self.payload_weight_label)
+ payload_layout.addRow("负载状态:", self.payload_status_label)
+
+ payload_group.setLayout(payload_layout)
+ main_layout.addWidget(payload_group)
+
+ # 创建通信状态组
+ comm_group = QGroupBox("通信状态")
+ comm_layout = QFormLayout()
+
+ self.signal_strength = QProgressBar()
+ self.signal_strength.setRange(0, 100)
+ self.signal_strength.setValue(0)
+
+ self.latency_label = QLabel("")
+ self.packet_loss_label = QLabel("")
+
+ comm_layout.addRow("信号强度:", self.signal_strength)
+ comm_layout.addRow("通信延迟:", self.latency_label)
+ comm_layout.addRow("丢包率:", self.packet_loss_label)
+
+ comm_group.setLayout(comm_layout)
+ main_layout.addWidget(comm_group)
+
+ # 创建任务信息组
+ task_group = QGroupBox("任务信息")
+ task_layout = QFormLayout()
+
+ self.task_type_label = QLabel("")
+ self.task_status_label = QLabel("")
+ self.task_progress = QProgressBar()
+ self.task_progress.setRange(0, 100)
+ self.task_progress.setValue(0)
+
+ task_layout.addRow("任务类型:", self.task_type_label)
+ task_layout.addRow("任务状态:", self.task_status_label)
+ task_layout.addRow("任务进度:", self.task_progress)
+
+ task_group.setLayout(task_layout)
+ main_layout.addWidget(task_group)
+
# 创建控制按钮
- button_layout = QHBoxLayout()
+ control_group = QGroupBox("控制面板")
+ control_layout = QVBoxLayout()
+
+ # 基本控制按钮
+ basic_control_layout = QHBoxLayout()
self.takeoff_btn = QPushButton("起飞")
self.land_btn = QPushButton("降落")
self.return_btn = QPushButton("返航")
self.emergency_btn = QPushButton("紧急停止")
- button_layout.addWidget(self.takeoff_btn)
- button_layout.addWidget(self.land_btn)
- button_layout.addWidget(self.return_btn)
- button_layout.addWidget(self.emergency_btn)
+ basic_control_layout.addWidget(self.takeoff_btn)
+ basic_control_layout.addWidget(self.land_btn)
+ basic_control_layout.addWidget(self.return_btn)
+ basic_control_layout.addWidget(self.emergency_btn)
+
+ # 高级控制
+ advanced_control_layout = QHBoxLayout()
+ self.set_altitude_btn = QPushButton("设置高度")
+ self.set_speed_btn = QPushButton("设置速度")
+ self.set_heading_btn = QPushButton("设置航向")
+ self.set_payload_btn = QPushButton("设置负载")
- main_layout.addLayout(button_layout)
+ advanced_control_layout.addWidget(self.set_altitude_btn)
+ advanced_control_layout.addWidget(self.set_speed_btn)
+ advanced_control_layout.addWidget(self.set_heading_btn)
+ advanced_control_layout.addWidget(self.set_payload_btn)
+
+ control_layout.addLayout(basic_control_layout)
+ control_layout.addLayout(advanced_control_layout)
+ control_group.setLayout(control_layout)
+ main_layout.addWidget(control_group)
self.setLayout(main_layout)
- # 连接信号
+ # Connect control buttons
+ self.set_altitude_btn.clicked.connect(self.set_altitude)
+ self.set_speed_btn.clicked.connect(self.set_speed)
+ self.set_heading_btn.clicked.connect(self.set_heading)
+ self.set_payload_btn.clicked.connect(self.set_payload)
self.takeoff_btn.clicked.connect(self.takeoff)
self.land_btn.clicked.connect(self.land)
self.return_btn.clicked.connect(self.return_home)
self.emergency_btn.clicked.connect(self.emergency_stop)
-
+
def takeoff(self):
- # TODO: 实现起飞功能
- pass
-
+ QMessageBox.information(self, "功能确认", "起飞命令 (功能待实现)")
+ # self.send_control_command("takeoff")
+
def land(self):
- # TODO: 实现降落功能
- pass
-
+ QMessageBox.information(self, "功能确认", "降落命令 (功能待实现)")
+ # self.send_control_command("land")
+
def return_home(self):
- # TODO: 实现返航功能
- pass
-
+ QMessageBox.information(self, "功能确认", "返航命令 (功能待实现)")
+ # self.send_control_command("return_home")
+
def emergency_stop(self):
- # TODO: 实现紧急停止功能
- pass
\ No newline at end of file
+ reply = QMessageBox.warning(self, "紧急停止确认",
+ "确定要发送紧急停止命令吗?这将停止所有电机!",
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ QMessageBox.information(self, "功能确认", "紧急停止命令 (功能待实现)")
+ # self.send_control_command("emergency_stop")
+
\ No newline at end of file
diff --git a/Src/command_center/ui/drone_list_view.py b/Src/command_center/ui/drone_list_view.py
index 5ce4fa4d..69b4200a 100644
--- a/Src/command_center/ui/drone_list_view.py
+++ b/Src/command_center/ui/drone_list_view.py
@@ -1,21 +1,177 @@
+# -*- coding: utf-8 -*-
+# File: drone_list_view.py
+# Purpose: 定义无人机列表标签页的 UI 和逻辑,显示无人机信息并提供管理操作。
+
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem)
-from PyQt5.QtCore import Qt
+ QPushButton, QTableWidget, QTableWidgetItem,
+ QComboBox, QColorDialog, QMenu, QAction,
+ QDialog, QLineEdit, QFormLayout, QMessageBox)
+from PyQt5.QtCore import Qt, pyqtSignal
+from PyQt5.QtGui import QColor, QBrush
+
+class AddDroneDialog(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.init_ui()
+
+ def init_ui(self):
+ self.setWindowTitle("添加无人机")
+ layout = QFormLayout()
+
+ self.id_edit = QLineEdit()
+ self.model_edit = QLineEdit()
+ self.group_combo = QComboBox()
+ self.group_combo.addItems(["运输组", "监控组", "救援组"])
+
+ layout.addRow("ID:", self.id_edit)
+ layout.addRow("型号:", self.model_edit)
+ layout.addRow("分组:", self.group_combo)
+
+ buttons = QHBoxLayout()
+ ok_button = QPushButton("确定")
+ cancel_button = QPushButton("取消")
+
+ ok_button.clicked.connect(self.accept)
+ cancel_button.clicked.connect(self.reject)
+
+ buttons.addWidget(ok_button)
+ buttons.addWidget(cancel_button)
+ layout.addRow(buttons)
+
+ self.setLayout(layout)
+
+ def get_drone_info(self):
+ return {
+ "id": self.id_edit.text(),
+ "model": self.model_edit.text(),
+ "group": self.group_combo.currentText(),
+ "status": "待命",
+ "battery": 100,
+ "signal_strength": 100,
+ "latency": 0,
+ "packet_loss": 0,
+ "payload_type": "无",
+ "payload_weight": 0,
+ "payload_status": "未装载",
+ "task_type": "无",
+ "task_status": "空闲",
+ "task_progress": 0
+ }
class DroneListView(QWidget):
- def __init__(self):
+ # 定义信号
+ drone_selected = pyqtSignal(str) # 无人机被选中时发出信号
+
+ def __init__(self, comm_manager=None):
super().__init__()
+ self.comm_manager = comm_manager
+ self.drone_colors = {} # 存储无人机颜色
+ self.drone_groups = {} # 存储无人机分组
self.init_ui()
+ self.setup_communication()
+
+ def setup_communication(self):
+ """设置通信回调"""
+ if self.comm_manager:
+ self.comm_manager.register_callback("drone_added", self.on_drone_added)
+ self.comm_manager.register_callback("drone_removed", self.on_drone_removed)
+ self.comm_manager.register_callback("drone_updated", self.on_drone_updated)
+
+ def on_drone_added(self, drone_id, data):
+ """处理无人机添加事件"""
+ self.add_drone_to_table(drone_id, self.comm_manager.get_drone(drone_id))
+
+ def on_drone_removed(self, drone_id, data):
+ """处理无人机移除事件"""
+ self.remove_drone_from_table(drone_id)
+
+ def on_drone_updated(self, drone_id, data):
+ """处理无人机更新事件"""
+ self.update_drone_in_table(drone_id, self.comm_manager.get_drone(drone_id))
+
+ def add_drone_to_table(self, drone_id, info):
+ """添加无人机到表格"""
+ row = self.drone_table.rowCount()
+ self.drone_table.insertRow(row)
+
+ # 设置各列数据
+ self.drone_table.setItem(row, 0, QTableWidgetItem(info.get("id", "")))
+ self.drone_table.setItem(row, 1, QTableWidgetItem(info.get("model", "")))
+ self.drone_table.setItem(row, 2, QTableWidgetItem(info.get("status", "")))
+ self.drone_table.setItem(row, 3, QTableWidgetItem(f"{info.get('latitude', 0):.6f}, {info.get('longitude', 0):.6f}"))
+ self.drone_table.setItem(row, 4, QTableWidgetItem(f"{info.get('battery', 0)}%"))
+ self.drone_table.setItem(row, 5, QTableWidgetItem(f"{info.get('payload_type', '')} ({info.get('payload_weight', 0)}kg)"))
+ self.drone_table.setItem(row, 6, QTableWidgetItem(info.get("group", "")))
+ self.drone_table.setItem(row, 7, QTableWidgetItem(f"{info.get('signal_strength', 0)}%"))
+
+ # 应用颜色
+ if drone_id in self.drone_colors:
+ self.update_drone_color(drone_id)
+
+ def remove_drone_from_table(self, drone_id):
+ """从表格中移除无人机"""
+ for row in range(self.drone_table.rowCount()):
+ if self.drone_table.item(row, 0).text() == drone_id:
+ self.drone_table.removeRow(row)
+ break
+
+ def update_drone_in_table(self, drone_id, info):
+ """更新表格中的无人机信息"""
+ for row in range(self.drone_table.rowCount()):
+ if self.drone_table.item(row, 0).text() == drone_id:
+ self.drone_table.setItem(row, 1, QTableWidgetItem(info.get("model", "")))
+ self.drone_table.setItem(row, 2, QTableWidgetItem(info.get("status", "")))
+ self.drone_table.setItem(row, 3, QTableWidgetItem(f"{info.get('latitude', 0):.6f}, {info.get('longitude', 0):.6f}"))
+ self.drone_table.setItem(row, 4, QTableWidgetItem(f"{info.get('battery', 0)}%"))
+ self.drone_table.setItem(row, 5, QTableWidgetItem(f"{info.get('payload_type', '')} ({info.get('payload_weight', 0)}kg)"))
+ self.drone_table.setItem(row, 6, QTableWidgetItem(info.get("group", "")))
+ self.drone_table.setItem(row, 7, QTableWidgetItem(f"{info.get('signal_strength', 0)}%"))
+ break
def init_ui(self):
# 创建主布局
main_layout = QVBoxLayout()
+ # 创建过滤和分组控制
+ filter_layout = QHBoxLayout()
+
+ # 状态过滤
+ self.status_filter = QComboBox()
+ self.status_filter.addItems(["全部状态", "飞行中", "待命", "充电中", "故障"])
+ self.status_filter.currentTextChanged.connect(self.apply_filters)
+
+ # 分组过滤
+ self.group_filter = QComboBox()
+ self.group_filter.addItems(["全部分组", "运输组", "监控组", "救援组"])
+ self.group_filter.currentTextChanged.connect(self.apply_filters)
+
+ filter_layout.addWidget(QLabel("状态过滤:"))
+ filter_layout.addWidget(self.status_filter)
+ filter_layout.addWidget(QLabel("分组过滤:"))
+ filter_layout.addWidget(self.group_filter)
+ filter_layout.addStretch()
+
+ main_layout.addLayout(filter_layout)
+
# 创建无人机列表
self.drone_table = QTableWidget()
- self.drone_table.setColumnCount(5)
- self.drone_table.setHorizontalHeaderLabels(["ID", "型号", "状态", "位置", "电量"])
- self.drone_table.setStyleSheet("QTableWidget { border: 1px solid #ccc; }")
+ self.drone_table.setColumnCount(8) # 增加列数
+ self.drone_table.setHorizontalHeaderLabels([
+ "ID", "型号", "状态", "位置", "电量", "负载", "分组", "通信状态"
+ ])
+ self.drone_table.setStyleSheet("""
+ QTableWidget {
+ border: 1px solid #ccc;
+ gridline-color: #ddd;
+ }
+ QTableWidget::item:selected {
+ background-color: #e0e0e0;
+ }
+ """)
+ self.drone_table.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.drone_table.customContextMenuRequested.connect(self.show_context_menu)
+ self.drone_table.itemSelectionChanged.connect(self.on_selection_changed)
+
main_layout.addWidget(self.drone_table)
# 创建控制按钮
@@ -24,11 +180,15 @@ class DroneListView(QWidget):
self.edit_drone_btn = QPushButton("编辑信息")
self.delete_drone_btn = QPushButton("删除无人机")
self.refresh_btn = QPushButton("刷新状态")
+ self.set_color_btn = QPushButton("设置颜色")
+ self.set_group_btn = QPushButton("设置分组")
button_layout.addWidget(self.add_drone_btn)
button_layout.addWidget(self.edit_drone_btn)
button_layout.addWidget(self.delete_drone_btn)
button_layout.addWidget(self.refresh_btn)
+ button_layout.addWidget(self.set_color_btn)
+ button_layout.addWidget(self.set_group_btn)
main_layout.addLayout(button_layout)
@@ -39,19 +199,224 @@ class DroneListView(QWidget):
self.edit_drone_btn.clicked.connect(self.edit_drone)
self.delete_drone_btn.clicked.connect(self.delete_drone)
self.refresh_btn.clicked.connect(self.refresh_status)
+ self.set_color_btn.clicked.connect(self.set_drone_color)
+ self.set_group_btn.clicked.connect(self.set_drone_group)
- def add_drone(self):
- # TODO: 实现添加无人机功能
+ def show_context_menu(self, position):
+ menu = QMenu()
+ view_details = QAction("查看详情", self)
+ set_color = QAction("设置颜色", self)
+ set_group = QAction("设置分组", self)
+ show_trajectory = QAction("显示轨迹", self)
+
+ menu.addAction(view_details)
+ menu.addAction(set_color)
+ menu.addAction(set_group)
+ menu.addAction(show_trajectory)
+
+ action = menu.exec_(self.drone_table.mapToGlobal(position))
+ if action == view_details:
+ self.view_drone_details()
+ elif action == set_color:
+ self.set_drone_color()
+ elif action == set_group:
+ self.set_drone_group()
+ elif action == show_trajectory:
+ self.show_drone_trajectory()
+
+ def on_selection_changed(self):
+ selected_items = self.drone_table.selectedItems()
+ if selected_items:
+ drone_id = self.drone_table.item(selected_items[0].row(), 0).text()
+ self.drone_selected.emit(drone_id)
+
+ def apply_filters(self):
+ status_filter = self.status_filter.currentText()
+ group_filter = self.group_filter.currentText()
+
+ for row in range(self.drone_table.rowCount()):
+ show_row = True
+
+ if status_filter != "全部状态":
+ status = self.drone_table.item(row, 2).text()
+ if status != status_filter:
+ show_row = False
+
+ if group_filter != "全部分组":
+ group = self.drone_table.item(row, 6).text()
+ if group != group_filter:
+ show_row = False
+
+ self.drone_table.setRowHidden(row, not show_row)
+
+ def set_drone_color(self):
+ """设置无人机颜色"""
+ selected_items = self.drone_table.selectedItems()
+ if not selected_items:
+ QMessageBox.warning(self, "警告", "请先选择要设置颜色的无人机")
+ return
+
+ drone_id = self.drone_table.item(selected_items[0].row(), 0).text()
+ color = QColorDialog.getColor()
+ if color.isValid():
+ self.drone_colors[drone_id] = color
+ self.update_drone_color(drone_id)
+ QMessageBox.information(self, "功能确认", f"无人机 {drone_id} 颜色已设置")
+
+ def set_drone_group(self):
+ """设置无人机分组"""
+ selected_items = self.drone_table.selectedItems()
+ if not selected_items:
+ QMessageBox.warning(self, "警告", "请先选择要设置分组的无人机")
+ return
+
+ drone_id = self.drone_table.item(selected_items[0].row(), 0).text()
+ group = self.group_filter.currentText()
+ if group != "全部分组":
+ self.drone_groups[drone_id] = group
+ self.update_drone_group(drone_id)
+
+ if self.comm_manager:
+ self.comm_manager.update_drone(drone_id, {"group": group})
+
+ QMessageBox.information(self, "功能确认", f"设置无人机 {drone_id} 分组 (功能待实现)")
+
+ def update_drone_color(self, drone_id):
+ for row in range(self.drone_table.rowCount()):
+ if self.drone_table.item(row, 0).text() == drone_id:
+ color = self.drone_colors.get(drone_id, QColor(255, 255, 255))
+ for col in range(self.drone_table.columnCount()):
+ item = self.drone_table.item(row, col)
+ if item:
+ item.setBackground(QBrush(color))
+ break
+
+ def update_drone_group(self, drone_id):
+ for row in range(self.drone_table.rowCount()):
+ if self.drone_table.item(row, 0).text() == drone_id:
+ group = self.drone_groups.get(drone_id, "未分组")
+ self.drone_table.setItem(row, 6, QTableWidgetItem(group))
+ break
+
+ def view_drone_details(self):
+ QMessageBox.information(self, "功能确认", "查看详情按钮已连接")
+ # TODO: 实现查看无人机详情功能
+ pass
+
+ def show_drone_trajectory(self):
+ QMessageBox.information(self, "功能确认", "显示轨迹按钮已连接")
+ # TODO: 实现显示无人机轨迹功能
pass
+
+ def add_drone(self):
+ dialog = AddDroneDialog(self)
+ if dialog.exec_() == QDialog.Accepted:
+ drone_info = dialog.get_drone_info()
+ # TODO: Add drone to actual data source (e.g., comm_manager)
+ self.add_drone_to_table(drone_info['id'], drone_info) # Add to table for now
+ QMessageBox.information(self, "功能确认", f"添加无人机: {drone_info['id']}")
def edit_drone(self):
- # TODO: 实现编辑无人机信息功能
- pass
+ selected_items = self.drone_table.selectedItems()
+ if not selected_items:
+ QMessageBox.warning(self, "提示", "请先选择要编辑的无人机")
+ return
+ # For simplicity, we'll just show a message. Actual editing would require a dialog.
+ drone_id = self.drone_table.item(selected_items[0].row(), 0).text()
+ QMessageBox.information(self, "功能确认", f"编辑无人机 {drone_id} (功能待实现)")
+ # TODO: Implement edit drone functionality
def delete_drone(self):
- # TODO: 实现删除无人机功能
+ selected_items = self.drone_table.selectedItems()
+ if not selected_items:
+ QMessageBox.warning(self, "提示", "请先选择要删除的无人机")
+ return
+ drone_id = self.drone_table.item(selected_items[0].row(), 0).text()
+ reply = QMessageBox.question(self, "确认删除", f"确定要删除无人机 {drone_id} 吗?",
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ # TODO: Remove drone from actual data source (e.g., comm_manager)
+ self.remove_drone_from_table(drone_id) # Remove from table for now
+ QMessageBox.information(self, "功能确认", f"删除无人机 {drone_id}")
+
+ def refresh_status(self):
+ # TODO: Implement actual status refresh logic from comm_manager or data source
+ QMessageBox.information(self, "功能确认", "刷新状态 (功能待实现)")
+ # Example: self.update_drone_list(self.comm_manager.get_all_drones() if self.comm_manager else [])
+
+ def update_drone_list(self, drone_list):
+ # 这里可以实现刷新表格的逻辑,当前先占位
pass
+
+ def add_drone_to_table(self, drone_id, info):
+ """添加无人机到表格"""
+ row = self.drone_table.rowCount()
+ self.drone_table.insertRow(row)
- def refresh_status(self):
- # TODO: 实现刷新状态功能
- pass
\ No newline at end of file
+ # 设置各列数据
+ self.drone_table.setItem(row, 0, QTableWidgetItem(info.get("id", "")))
+ self.drone_table.setItem(row, 1, QTableWidgetItem(info.get("model", "")))
+ self.drone_table.setItem(row, 2, QTableWidgetItem(info.get("status", "")))
+ self.drone_table.setItem(row, 3, QTableWidgetItem(f"{info.get('latitude', 0):.6f}, {info.get('longitude', 0):.6f}"))
+ self.drone_table.setItem(row, 4, QTableWidgetItem(f"{info.get('battery', 0)}%"))
+ self.drone_table.setItem(row, 5, QTableWidgetItem(f"{info.get('payload_type', '')} ({info.get('payload_weight', 0)}kg)"))
+ self.drone_table.setItem(row, 6, QTableWidgetItem(info.get("group", "")))
+ self.drone_table.setItem(row, 7, QTableWidgetItem(f"{info.get('signal_strength', 0)}%"))
+
+ # 应用颜色
+ if drone_id in self.drone_colors:
+ self.update_drone_color(drone_id)
+
+ def remove_drone_from_table(self, drone_id):
+ """从表格中移除无人机"""
+ for row in range(self.drone_table.rowCount()):
+ if self.drone_table.item(row, 0).text() == drone_id:
+ self.drone_table.removeRow(row)
+ break
+
+ def update_drone_in_table(self, drone_id, info):
+ """更新表格中的无人机信息"""
+ for row in range(self.drone_table.rowCount()):
+ if self.drone_table.item(row, 0).text() == drone_id:
+ self.drone_table.setItem(row, 1, QTableWidgetItem(info.get("model", "")))
+ self.drone_table.setItem(row, 2, QTableWidgetItem(info.get("status", "")))
+ self.drone_table.setItem(row, 3, QTableWidgetItem(f"{info.get('latitude', 0):.6f}, {info.get('longitude', 0):.6f}"))
+ self.drone_table.setItem(row, 4, QTableWidgetItem(f"{info.get('battery', 0)}%"))
+ self.drone_table.setItem(row, 5, QTableWidgetItem(f"{info.get('payload_type', '')} ({info.get('payload_weight', 0)}kg)"))
+ self.drone_table.setItem(row, 6, QTableWidgetItem(info.get("group", "")))
+ self.drone_table.setItem(row, 7, QTableWidgetItem(f"{info.get('signal_strength', 0)}%"))
+ break
+
+ def update_drone_color(self, drone_id):
+ for row in range(self.drone_table.rowCount()):
+ if self.drone_table.item(row, 0).text() == drone_id:
+ color = self.drone_colors.get(drone_id, QColor(255, 255, 255))
+ for col in range(self.drone_table.columnCount()):
+ item = self.drone_table.item(row, col)
+ if item:
+ item.setBackground(QBrush(color))
+ break
+
+ def update_drone_group(self, drone_id):
+ for row in range(self.drone_table.rowCount()):
+ if self.drone_table.item(row, 0).text() == drone_id:
+ group = self.drone_groups.get(drone_id, "未分组")
+ self.drone_table.setItem(row, 6, QTableWidgetItem(group))
+ break
+
+ def view_drone_details(self):
+ selected_items = self.drone_table.selectedItems()
+ if selected_items:
+ drone_id = self.drone_table.item(selected_items[0].row(), 0).text()
+ self.drone_selected.emit(drone_id)
+
+ def show_drone_trajectory(self):
+ """显示无人机轨迹"""
+ selected_items = self.drone_table.selectedItems()
+ if not selected_items:
+ QMessageBox.warning(self, "警告", "请先选择要查看轨迹的无人机")
+ return
+
+ drone_id = self.drone_table.item(selected_items[0].row(), 0).text()
+ # TODO: 实现轨迹显示功能
+ QMessageBox.information(self, "提示", f"显示无人机 {drone_id} 的轨迹")
\ No newline at end of file
diff --git a/Src/command_center/ui/login_view.py b/Src/command_center/ui/login_view.py
index efbad863..c88e2429 100644
--- a/Src/command_center/ui/login_view.py
+++ b/Src/command_center/ui/login_view.py
@@ -1,3 +1,7 @@
+# -*- coding: utf-8 -*-
+# File: login_view.py
+# Purpose: 定义登录界面的 UI 和逻辑。
+
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QMessageBox)
from PyQt5.QtCore import Qt, pyqtSignal
diff --git a/Src/command_center/ui/main_view.py b/Src/command_center/ui/main_view.py
deleted file mode 100644
index 36ffd5f0..00000000
--- a/Src/command_center/ui/main_view.py
+++ /dev/null
@@ -1,87 +0,0 @@
-from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
- QLabel, QPushButton, QTabWidget, QMessageBox)
-from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QFont
-
-class MainView(QMainWindow):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("无人机后勤输送系统 - 指挥控制中心")
- self.setMinimumSize(800, 600)
- self.init_ui()
-
- def init_ui(self):
- # 创建主窗口部件
- central_widget = QWidget()
- self.setCentralWidget(central_widget)
- main_layout = QVBoxLayout(central_widget)
-
- # 顶部工具栏
- toolbar = QHBoxLayout()
- self.status_label = QLabel("系统状态: 正常")
- self.status_label.setFont(QFont('Arial', 10))
- logout_button = QPushButton("退出登录")
- logout_button.clicked.connect(self.handle_logout)
-
- toolbar.addWidget(self.status_label)
- toolbar.addStretch()
- toolbar.addWidget(logout_button)
- main_layout.addLayout(toolbar)
-
- # 主内容区域
- self.tab_widget = QTabWidget()
-
- # 创建各个功能标签页
- self.tab_widget.addTab(self.create_drone_management_tab(), "无人机管理")
- self.tab_widget.addTab(self.create_task_management_tab(), "任务管理")
- self.tab_widget.addTab(self.create_monitoring_tab(), "实时监控")
- self.tab_widget.addTab(self.create_system_settings_tab(), "系统设置")
-
- main_layout.addWidget(self.tab_widget)
-
- def create_drone_management_tab(self):
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # 添加无人机管理相关的控件
- layout.addWidget(QLabel("无人机管理功能待实现"))
-
- return tab
-
- def create_task_management_tab(self):
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # 添加任务管理相关的控件
- layout.addWidget(QLabel("任务管理功能待实现"))
-
- return tab
-
- def create_monitoring_tab(self):
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # 添加实时监控相关的控件
- layout.addWidget(QLabel("实时监控功能待实现"))
-
- return tab
-
- def create_system_settings_tab(self):
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # 添加系统设置相关的控件
- layout.addWidget(QLabel("系统设置功能待实现"))
-
- return tab
-
- def handle_logout(self):
- reply = QMessageBox.question(self, '确认退出',
- '确定要退出登录吗?',
- QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No)
-
- if reply == QMessageBox.Yes:
- # TODO: 实现实际的登出逻辑
- self.close()
- # 这里应该触发登录窗口的显示
\ No newline at end of file
diff --git a/Src/command_center/ui/map_data_model.py b/Src/command_center/ui/map_data_model.py
new file mode 100644
index 00000000..8a5b009a
--- /dev/null
+++ b/Src/command_center/ui/map_data_model.py
@@ -0,0 +1,39 @@
+from PyQt5.QtCore import QObject, pyqtSignal
+
+class MapDataModel(QObject):
+ data_changed = pyqtSignal()
+
+ def __init__(self):
+ super().__init__()
+ self.map_pixmap = None
+ self.threat_points = []
+ self.start_point = None
+ self.goal_point = None
+ self.paths = []
+
+ def set_map(self, pixmap):
+ self.map_pixmap = pixmap
+ self.data_changed.emit()
+
+ def add_threat_point(self, point):
+ self.threat_points.append(point)
+ self.data_changed.emit()
+
+ def set_start_point(self, point):
+ self.start_point = point
+ self.data_changed.emit()
+
+ def set_goal_point(self, point):
+ self.goal_point = point
+ self.data_changed.emit()
+
+ def clear_all(self):
+ self.threat_points.clear()
+ self.start_point = None
+ self.goal_point = None
+ self.paths = []
+ self.data_changed.emit()
+
+ def set_paths(self, paths):
+ self.paths = paths
+ self.data_changed.emit()
\ No newline at end of file
diff --git a/Src/command_center/ui/path_layer_view.py b/Src/command_center/ui/path_layer_view.py
index c4e40da4..15b928d4 100644
--- a/Src/command_center/ui/path_layer_view.py
+++ b/Src/command_center/ui/path_layer_view.py
@@ -1,63 +1,80 @@
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem)
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QPointF
+from .base_map_view import BaseMapView
-class PathLayerView(QWidget):
- def __init__(self):
- super().__init__()
- self.init_ui()
+class PathLayerView(BaseMapView):
+ def __init__(self, map_data_model):
+ super().__init__(map_data_model)
+ self.add_path_mode = False
+ self.current_path = []
+ self.init_additional_ui()
- def init_ui(self):
- # 创建主布局
- main_layout = QVBoxLayout()
+ def init_additional_ui(self):
+ # 创建路径控制按钮
+ path_control_layout = QHBoxLayout()
- # 创建路径列表
- self.path_table = QTableWidget()
- self.path_table.setColumnCount(4)
- self.path_table.setHorizontalHeaderLabels(["ID", "起点", "终点", "状态"])
- self.path_table.setStyleSheet("QTableWidget { border: 1px solid #ccc; }")
- main_layout.addWidget(self.path_table)
+ self.add_path_btn = QPushButton("添加路径点")
+ self.add_path_btn.setCheckable(True)
+ self.add_path_btn.clicked.connect(self.toggle_add_path_mode)
- # 创建控制按钮
- button_layout = QHBoxLayout()
- self.add_path_btn = QPushButton("添加路径")
- self.edit_path_btn = QPushButton("编辑路径")
- self.delete_path_btn = QPushButton("删除路径")
- self.simulate_path_btn = QPushButton("模拟路径")
+ self.complete_path_btn = QPushButton("完成路径")
+ self.complete_path_btn.clicked.connect(self.complete_path)
+ self.complete_path_btn.setEnabled(False)
- button_layout.addWidget(self.add_path_btn)
- button_layout.addWidget(self.edit_path_btn)
- button_layout.addWidget(self.delete_path_btn)
- button_layout.addWidget(self.simulate_path_btn)
+ self.clear_paths_btn = QPushButton("清除所有路径")
+ self.clear_paths_btn.clicked.connect(self.clear_all_paths)
- main_layout.addLayout(button_layout)
+ path_control_layout.addWidget(self.add_path_btn)
+ path_control_layout.addWidget(self.complete_path_btn)
+ path_control_layout.addWidget(self.clear_paths_btn)
- # 创建路径详情区域
- self.path_detail = QLabel("路径详情")
- self.path_detail.setAlignment(Qt.AlignCenter)
- self.path_detail.setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;")
- main_layout.addWidget(self.path_detail)
+ # 将按钮添加到主布局
+ layout = self.layout()
+ if layout:
+ layout.addLayout(path_control_layout)
+ else:
+ print("Error: Layout not found in PathLayerView")
- self.setLayout(main_layout)
+ def toggle_add_path_mode(self):
+ self.add_path_mode = self.add_path_btn.isChecked()
+ if self.add_path_mode:
+ self.add_path_btn.setText("取消添加点")
+ self.complete_path_btn.setEnabled(bool(self.current_path))
+ else:
+ self.add_path_btn.setText("添加路径点")
+ self.complete_path_btn.setEnabled(False)
+
+ def complete_path(self):
+ if len(self.current_path) > 1:
+ # Add a copy to the main data model
+ self.map_data_model.paths.append(list(self.current_path))
+ self.map_data_model.data_changed.emit()
+ self.current_path = []
+ self.add_path_mode = False
+ self.add_path_btn.setChecked(False)
+ self.add_path_btn.setText("添加路径点")
+ self.complete_path_btn.setEnabled(False)
+ self.update_map()
+
+ def clear_all_paths(self):
+ self.current_path = []
+ self.map_data_model.paths = []
+ self.map_data_model.data_changed.emit()
+ self.update_map()
- # 连接信号
- self.add_path_btn.clicked.connect(self.add_path)
- self.edit_path_btn.clicked.connect(self.edit_path)
- self.delete_path_btn.clicked.connect(self.delete_path)
- self.simulate_path_btn.clicked.connect(self.simulate_path)
-
- def add_path(self):
- # TODO: 实现添加路径功能
- pass
-
- def edit_path(self):
- # TODO: 实现编辑路径功能
- pass
-
- def delete_path(self):
- # TODO: 实现删除路径功能
- pass
-
- def simulate_path(self):
- # TODO: 实现路径模拟功能
- pass
\ No newline at end of file
+ def handle_map_click(self, map_point: QPointF):
+ """Handles clicks forwarded from BaseMapView."""
+ if self.add_path_mode:
+ img_x = int(map_point.x())
+ img_y = int(map_point.y())
+ # 添加路径点到当前路径
+ self.current_path.append((img_x, img_y))
+ self.complete_path_btn.setEnabled(True)
+
+ print(f"Added path point: {img_x}, {img_y}")
+ # self.update_map()
+
+ # Remove the old mousePressEvent
+ # def mousePressEvent(self, event):
+ # pass
\ No newline at end of file
diff --git a/Src/command_center/ui/path_planning_view.py b/Src/command_center/ui/path_planning_view.py
index e2d84cf3..74cb42bf 100644
--- a/Src/command_center/ui/path_planning_view.py
+++ b/Src/command_center/ui/path_planning_view.py
@@ -1,6 +1,10 @@
+# -*- coding: utf-8 -*-
+# File: path_planning_view.py
+# Purpose: 定义路径规划标签页的 UI,提供设置路径规划参数和触发规划操作的界面。
+
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QGroupBox, QFormLayout, QComboBox,
- QSpinBox, QDoubleSpinBox)
+ QSpinBox, QDoubleSpinBox, QMessageBox)
from PyQt5.QtCore import Qt
class PathPlanningView(QWidget):
@@ -76,13 +80,16 @@ class PathPlanningView(QWidget):
self.export_path_btn.clicked.connect(self.export_path)
def plan_path(self):
- # TODO: 实现路径规划功能
+ QMessageBox.information(self, "功能确认", "规划路径 (功能待实现)")
+ # TODO: Implement path planning logic
pass
def clear_path(self):
- # TODO: 实现清除路径功能
+ QMessageBox.information(self, "功能确认", "清除路径 (功能待实现)")
+ # TODO: Clear path from MapDataModel or internal state
pass
def export_path(self):
- # TODO: 实现导出路径功能
+ QMessageBox.information(self, "功能确认", "导出路径 (功能待实现)")
+ # TODO: Implement path export functionality
pass
\ No newline at end of file
diff --git a/Src/command_center/ui/path_simulation_view.py b/Src/command_center/ui/path_simulation_view.py
deleted file mode 100644
index 2f21a1b1..00000000
--- a/Src/command_center/ui/path_simulation_view.py
+++ /dev/null
@@ -1,109 +0,0 @@
-from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QGroupBox, QFormLayout, QProgressBar,
- QSpinBox, QDoubleSpinBox)
-from PyQt5.QtCore import Qt, QTimer
-
-class PathSimulationView(QWidget):
- def __init__(self):
- super().__init__()
- self.init_ui()
- self.simulation_timer = QTimer()
- self.simulation_timer.timeout.connect(self.update_simulation)
- self.simulation_progress = 0
-
- def init_ui(self):
- # 创建主布局
- main_layout = QVBoxLayout()
-
- # 创建模拟控制组
- control_group = QGroupBox("模拟控制")
- control_layout = QFormLayout()
-
- self.speed_spinbox = QDoubleSpinBox()
- self.speed_spinbox.setRange(0.1, 10.0)
- self.speed_spinbox.setValue(1.0)
- self.speed_spinbox.setSingleStep(0.1)
-
- self.interval_spinbox = QSpinBox()
- self.interval_spinbox.setRange(10, 1000)
- self.interval_spinbox.setValue(100)
- self.interval_spinbox.setSingleStep(10)
-
- control_layout.addRow("模拟速度:", self.speed_spinbox)
- control_layout.addRow("更新间隔(ms):", self.interval_spinbox)
-
- control_group.setLayout(control_layout)
- main_layout.addWidget(control_group)
-
- # 创建模拟进度条
- self.progress_bar = QProgressBar()
- self.progress_bar.setRange(0, 100)
- self.progress_bar.setValue(0)
- main_layout.addWidget(self.progress_bar)
-
- # 创建控制按钮
- button_layout = QHBoxLayout()
- self.start_btn = QPushButton("开始模拟")
- self.pause_btn = QPushButton("暂停")
- self.stop_btn = QPushButton("停止")
- self.reset_btn = QPushButton("重置")
-
- button_layout.addWidget(self.start_btn)
- button_layout.addWidget(self.pause_btn)
- button_layout.addWidget(self.stop_btn)
- button_layout.addWidget(self.reset_btn)
-
- main_layout.addLayout(button_layout)
-
- # 创建状态显示组
- status_group = QGroupBox("模拟状态")
- status_layout = QFormLayout()
-
- self.time_label = QLabel("0.0s")
- self.distance_label = QLabel("0.0m")
- self.altitude_label = QLabel("0.0m")
- self.speed_label = QLabel("0.0m/s")
-
- status_layout.addRow("已用时间:", self.time_label)
- status_layout.addRow("飞行距离:", self.distance_label)
- status_layout.addRow("当前高度:", self.altitude_label)
- status_layout.addRow("当前速度:", self.speed_label)
-
- status_group.setLayout(status_layout)
- main_layout.addWidget(status_group)
-
- self.setLayout(main_layout)
-
- # 连接信号
- self.start_btn.clicked.connect(self.start_simulation)
- self.pause_btn.clicked.connect(self.pause_simulation)
- self.stop_btn.clicked.connect(self.stop_simulation)
- self.reset_btn.clicked.connect(self.reset_simulation)
-
- def start_simulation(self):
- self.simulation_timer.start(self.interval_spinbox.value())
-
- def pause_simulation(self):
- self.simulation_timer.stop()
-
- def stop_simulation(self):
- self.simulation_timer.stop()
- self.simulation_progress = 0
- self.progress_bar.setValue(0)
-
- def reset_simulation(self):
- self.simulation_progress = 0
- self.progress_bar.setValue(0)
- self.time_label.setText("0.0s")
- self.distance_label.setText("0.0m")
- self.altitude_label.setText("0.0m")
- self.speed_label.setText("0.0m/s")
-
- def update_simulation(self):
- # TODO: 实现模拟更新逻辑
- self.simulation_progress += self.speed_spinbox.value()
- if self.simulation_progress > 100:
- self.simulation_progress = 100
- self.simulation_timer.stop()
-
- self.progress_bar.setValue(int(self.simulation_progress))
\ No newline at end of file
diff --git a/Src/command_center/ui/simple_map_view.py b/Src/command_center/ui/simple_map_view.py
new file mode 100644
index 00000000..f8ef21d1
--- /dev/null
+++ b/Src/command_center/ui/simple_map_view.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# File: simple_map_view.py
+# Purpose: 定义地图视图标签页,继承自 BaseMapView,主要负责威胁点的交互。
+
+from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QFileDialog
+from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor
+from PyQt5.QtCore import Qt, pyqtSignal, QPointF
+from .base_map_view import BaseMapView
+
+class SimpleMapView(BaseMapView):
+ # 当威胁点变化时发出信号,便于主程序联动路径规划
+ threat_points_changed = pyqtSignal(list)
+
+ def __init__(self, map_data_model):
+ super().__init__(map_data_model)
+ self.add_threat_point_mode = False
+ self.init_additional_ui()
+ # Force re-connection of load map action in case it was overwritten
+ if hasattr(self, 'load_map_action'):
+ print("SimpleMapView: Forcing re-connection of load_map_action")
+ try:
+ self.load_map_action.triggered.disconnect()
+ except TypeError: # No connections exist
+ pass
+ self.load_map_action.triggered.connect(self.load_map_image)
+ else:
+ print("SimpleMapView: Warning - load_map_action not found in base class?")
+
+ def init_additional_ui(self):
+ # 添加威胁点标记按钮
+ self.threat_point_btn = QPushButton("标记威胁点")
+ self.threat_point_btn.setCheckable(True)
+ self.threat_point_btn.clicked.connect(self.toggle_threat_point_mode)
+
+ # 将按钮添加到布局中
+ layout = self.layout()
+ if layout:
+ layout.addWidget(self.threat_point_btn)
+ else:
+ print("Error: Layout not found in SimpleMapView") # Error handling
+
+ def toggle_threat_point_mode(self):
+ print("SimpleMapView: toggle_threat_point_mode called") # Debug print
+ is_checked = self.threat_point_btn.isChecked()
+ print(f"SimpleMapView: threat_point_btn isChecked: {is_checked}")
+ self.add_threat_point_mode = is_checked
+ print(f"SimpleMapView: self.add_threat_point_mode set to: {self.add_threat_point_mode}")
+ if self.add_threat_point_mode:
+ self.threat_point_btn.setText("取消标记")
+ else:
+ self.threat_point_btn.setText("标记威胁点")
+
+ def handle_map_click(self, map_point: QPointF):
+ """Handles clicks forwarded from BaseMapView."""
+ print(f"SimpleMapView: handle_map_click called, mode: {self.add_threat_point_mode}") # Debug print
+ if self.add_threat_point_mode:
+ img_x = int(map_point.x())
+ img_y = int(map_point.y())
+ print(f"SimpleMapView: Adding threat point at ({img_x}, {img_y})") # Debug print
+ # 添加威胁点到数据模型
+ self.map_data_model.add_threat_point((img_x, img_y))
+ # Emit signal (optional, BaseMapView already emits on data change)
+ # self.threat_points_changed.emit(self.map_data_model.threat_points)
+
+ # Remove the old mousePressEvent if it exists
+ # def mousePressEvent(self, event): # <--- REMOVE THIS METHOD
+ # pass
+
+ # load_map_image and update_map are now handled by BaseMapView
+ # We might need specific update logic here in the future if SimpleMapView
+ # needs drawing beyond what BaseMapView provides.
\ No newline at end of file
diff --git a/Src/command_center/ui/status_dashboard.py b/Src/command_center/ui/status_dashboard.py
index 6e13ecdc..af2feed6 100644
--- a/Src/command_center/ui/status_dashboard.py
+++ b/Src/command_center/ui/status_dashboard.py
@@ -1,3 +1,7 @@
+# -*- coding: utf-8 -*-
+# File: status_dashboard.py
+# Purpose: 定义状态仪表板标签页的 UI,显示系统整体状态和性能指标。
+
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QProgressBar, QGroupBox)
from PyQt5.QtCore import Qt
diff --git a/Src/command_center/ui/threat_layer_view.py b/Src/command_center/ui/threat_layer_view.py
index e77b9cb6..9b8b2823 100644
--- a/Src/command_center/ui/threat_layer_view.py
+++ b/Src/command_center/ui/threat_layer_view.py
@@ -1,56 +1,71 @@
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem)
-from PyQt5.QtCore import Qt
+ QPushButton, QTableWidget, QTableWidgetItem, QSpinBox)
+from PyQt5.QtCore import Qt, QPointF
+from .base_map_view import BaseMapView
-class ThreatLayerView(QWidget):
- def __init__(self):
- super().__init__()
- self.init_ui()
+class ThreatLayerView(BaseMapView):
+ def __init__(self, map_data_model):
+ super().__init__(map_data_model)
+ self.add_threat_mode = False
+ self.threat_radius = 50 # 默认威胁半径
+ self.init_additional_ui()
- def init_ui(self):
- # 创建主布局
- main_layout = QVBoxLayout()
+ def init_additional_ui(self):
+ # 创建威胁控制面板
+ threat_control_layout = QHBoxLayout()
- # 创建威胁列表
- self.threat_table = QTableWidget()
- self.threat_table.setColumnCount(4)
- self.threat_table.setHorizontalHeaderLabels(["ID", "类型", "位置", "威胁等级"])
- self.threat_table.setStyleSheet("QTableWidget { border: 1px solid #ccc; }")
- main_layout.addWidget(self.threat_table)
+ # 威胁点标记按钮
+ self.add_threat_btn = QPushButton("添加威胁区域")
+ self.add_threat_btn.setCheckable(True)
+ self.add_threat_btn.clicked.connect(self.toggle_add_threat_mode)
- # 创建控制按钮
- button_layout = QHBoxLayout()
- self.add_threat_btn = QPushButton("添加威胁")
- self.edit_threat_btn = QPushButton("编辑威胁")
- self.delete_threat_btn = QPushButton("删除威胁")
+ # 威胁半径控制
+ radius_label = QLabel("威胁半径:")
+ self.radius_spinbox = QSpinBox()
+ self.radius_spinbox.setRange(10, 200)
+ self.radius_spinbox.setValue(self.threat_radius)
+ self.radius_spinbox.valueChanged.connect(self.set_threat_radius)
- button_layout.addWidget(self.add_threat_btn)
- button_layout.addWidget(self.edit_threat_btn)
- button_layout.addWidget(self.delete_threat_btn)
+ # 清除威胁按钮
+ self.clear_threat_btn = QPushButton("清除威胁")
+ self.clear_threat_btn.clicked.connect(self.clear_threats)
- main_layout.addLayout(button_layout)
+ # 添加控件到布局
+ threat_control_layout.addWidget(self.add_threat_btn)
+ threat_control_layout.addWidget(self.clear_threat_btn)
- # 创建威胁详情区域
- self.threat_detail = QLabel("威胁详情")
- self.threat_detail.setAlignment(Qt.AlignCenter)
- self.threat_detail.setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;")
- main_layout.addWidget(self.threat_detail)
-
- self.setLayout(main_layout)
-
- # 连接信号
- self.add_threat_btn.clicked.connect(self.add_threat)
- self.edit_threat_btn.clicked.connect(self.edit_threat)
- self.delete_threat_btn.clicked.connect(self.delete_threat)
-
- def add_threat(self):
- # TODO: 实现添加威胁功能
- pass
+ # 将控制面板添加到主布局
+ layout = self.layout()
+ if layout:
+ layout.addLayout(threat_control_layout)
+ else:
+ print("Error: Layout not found in ThreatLayerView")
+
+ def toggle_add_threat_mode(self):
+ self.add_threat_mode = self.add_threat_btn.isChecked()
+ if self.add_threat_mode:
+ self.add_threat_btn.setText("取消添加")
+ else:
+ self.add_threat_btn.setText("添加威胁区域")
+
+ def set_threat_radius(self, value):
+ self.threat_radius = value
- def edit_threat(self):
- # TODO: 实现编辑威胁功能
- pass
+ def clear_threats(self):
+ self.map_data_model.threat_points = []
+ self.map_data_model.data_changed.emit()
+ self.update_map() # Force redraw
- def delete_threat(self):
- # TODO: 实现删除威胁功能
- pass
\ No newline at end of file
+ def handle_map_click(self, map_point: QPointF):
+ """Handles clicks forwarded from BaseMapView."""
+ if self.add_threat_mode:
+ img_x = int(map_point.x())
+ img_y = int(map_point.y())
+ # 添加威胁点到数据模型
+ # Note: Threat radius is stored but not visualized yet
+ self.map_data_model.add_threat_point((img_x, img_y))
+ # BaseMapView will redraw automatically due to data_changed signal
+
+ # Remove the old mousePressEvent
+ # def mousePressEvent(self, event):
+ # pass
\ No newline at end of file