#!/usr/bin/env python3 from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtCore import QTimer, pyqtSignal, QObject, pyqtSlot from PyQt6.QtWebChannel import QWebChannel def _log(level, message): print(f"[{level}] {message}") class DroneMap: """無人機地圖類別 - 負責管理 Leaflet 地圖顯示""" def __init__(self): """初始化地圖""" self.map_view = QWebEngineView() self.map_loaded = False self.pending_map_updates = {} # 創建橋接對象 self.bridge = MapBridge() # 設置 QWebChannel self.channel = QWebChannel() self.channel.registerObject('bridge', self.bridge) self.map_view.page().setWebChannel(self.channel) # 設置地圖 HTML inline_html = '''
''' self.map_view.setHtml(inline_html) self.map_view.loadFinished.connect(self._on_map_loaded) # 設置地圖更新計時器 self.map_update_timer = QTimer() self.map_update_timer.timeout.connect(self.update_map_positions) self.map_update_timer.start(200) # 每 200ms 更新一次 def _on_map_loaded(self, ok: bool): """地圖加載完成回調""" if ok: self.map_loaded = True else: _log("ERROR", "地圖載入失敗") def update_drone_position(self, drone_id, lat, lon, heading): """更新無人機位置(加入待處理隊列)""" self.pending_map_updates[drone_id] = (lat, lon, heading) def update_map_positions(self): """批量更新地圖上的無人機位置""" if not self.map_loaded or not self.pending_map_updates: return js_commands = [] for drone_id, (lat, lon, heading) in self.pending_map_updates.items(): js_commands.append(f"updateDrone({lat:.6f}, {lon:.6f}, '{drone_id}', {heading:.1f});") if js_commands: combined_js = "\n".join(js_commands) self.map_view.page().runJavaScript(combined_js) self.pending_map_updates.clear() def clear_trajectories(self): """清除所有軌跡""" if self.map_loaded: self.map_view.page().runJavaScript("clearAllTrajectories();") def focus_on_drone(self, drone_id): """聚焦到指定無人機""" if self.map_loaded: self.map_view.page().runJavaScript(f"focusOn('{drone_id}');") # ================================================================================ # 任務規劃視覺化方法 # ================================================================================ def draw_mission_plan(self, center_lat, center_lon, target_lat, target_lon): """在地圖上繪製任務規劃(舊介面,相容用)""" self.draw_mission_plan_for_group('_default', '#FF4444', center_lat, center_lon, target_lat, target_lon) def draw_mission_plan_for_group(self, group_id, color, center_lat, center_lon, target_lat, target_lon): """在地圖上繪製指定群組的任務規劃(帶顏色區分)""" if self.map_loaded: js_code = ( f"drawMissionPlanForGroup(" f"'{group_id}', '{color}', " f"{center_lat:.6f}, {center_lon:.6f}, " f"{target_lat:.6f}, {target_lon:.6f});" ) self.map_view.page().runJavaScript(js_code) _log( "INFO", f"地圖已繪製 Group {group_id} 任務規劃: " f"C({center_lat:.6f}, {center_lon:.6f}) -> " f"T({target_lat:.6f}, {target_lon:.6f})", ) def clear_mission_plan(self): """清除地圖上所有任務規劃標記""" if self.map_loaded: self.map_view.page().runJavaScript("clearAllMissionPlans();") _log("INFO", "地圖已清除所有任務規劃") def clear_mission_plan_for_group(self, group_id): """清除指定群組的任務規劃標記""" if self.map_loaded: self.map_view.page().runJavaScript(f"clearMissionPlanForGroup('{group_id}');") _log("INFO", f"地圖已清除 Group {group_id} 任務規劃") def set_mission_mode(self, mode): """從 Python 端切換地圖的任務模式(觸發框選/路徑標記等)""" if self.map_loaded: self.map_view.page().runJavaScript(f"onMissionModeChanged('{mode}');") def toggle_drone_box_select(self): """切換地圖上的無人機框選模式""" if self.map_loaded: self.map_view.page().runJavaScript("toggleDroneSelection();") # ================================================================================ def get_widget(self): """獲取地圖 widget""" return self.map_view def get_gps_signal(self): """獲取 GPS 信號""" return self.bridge.gps_signal def get_drone_clicked_signal(self): """獲取無人機點擊信號""" return self.bridge.drone_clicked def get_clear_all_drone_selection_signal(self): """獲取清除所有無人機選擇信號""" return self.bridge.clear_all_drone_selection def get_toggle_select_all_drones_signal(self): """獲取切換全選所有無人機信號""" return self.bridge.select_all_drones def get_select_all_drones_signal(self): """獲取全選所有無人機信號""" return self.bridge.select_all_drones def get_start_mission_signal(self): """獲取開始任務信號""" return self.bridge.start_mission_signal def get_pause_mission_signal(self): """獲取暫停任務信號""" return self.bridge.pause_mission_signal def get_rectangle_selected_signal(self): """獲取矩形選擇信號""" return self.bridge.rectangle_selected def get_polygon_selected_signal(self): """獲取多邊形選擇信號""" return self.bridge.polygon_selected def get_mission_mode_changed_signal(self): """獲取任務模式切換信號""" return self.bridge.mission_mode_changed def get_route_confirmed_signal(self): """獲取路徑確認信號""" return self.bridge.route_confirmed def get_drone_box_selected_signal(self): """獲取框選無人機完成信號""" return self.bridge.drone_box_selected class MapBridge(QObject): """JavaScript 和 Python 之間的橋接類""" gps_signal = pyqtSignal(float, float) drone_clicked = pyqtSignal(str) clear_all_drone_selection = pyqtSignal() select_all_drones = pyqtSignal() start_mission_signal = pyqtSignal(float, float, float, float) pause_mission_signal = pyqtSignal() rectangle_selected = pyqtSignal(str) polygon_selected = pyqtSignal(str) mission_mode_changed = pyqtSignal(str) route_confirmed = pyqtSignal(str) # 路徑確認 (JSON 字串) drone_box_selected = pyqtSignal(str) # 框選無人機完成 (JSON 字串) def __init__(self): super().__init__() @pyqtSlot(float, float) def emitGpsSignal(self, lat, lon): """供 JavaScript 調用的方法""" self.gps_signal.emit(lat, lon) @pyqtSlot(str) def emitDroneClicked(self, drone_id): """供 JavaScript 調用的方法 - 當無人機被點擊時""" self.drone_clicked.emit(drone_id) @pyqtSlot() def clearAllDroneSelection(self): """供 JavaScript 調用的方法 - 清除所有無人機選擇""" self.clear_all_drone_selection.emit() _log("INFO", "已清除所有無人機選擇") @pyqtSlot() def toggleSelectAllDrones(self): """供 JavaScript 調用的方法 - 切換全選/取消全選所有無人機""" self.select_all_drones.emit() _log("INFO", "已切換全選無人機") @pyqtSlot(float, float, float, float) def startMissionSignal(self, center_lat, center_lon, target_lat, target_lon): """供 JavaScript 調用的方法 - 開始任務""" self.start_mission_signal.emit(center_lat, center_lon, target_lat, target_lon) _log( "INFO", f"已發出開始任務信號: " f"C({center_lat}, {center_lon}) -> T({target_lat}, {target_lon})", ) @pyqtSlot() def pauseMissionSignal(self): """供 JavaScript 調用的方法 - 暫停任務""" self.pause_mission_signal.emit() _log("INFO", "已發出暫停任務信號") @pyqtSlot(str) def rectangleSelected(self, points_json): """供 JavaScript 調用的方法 - 矩形選擇完成""" self.rectangle_selected.emit(points_json) _log("INFO", f"矩形區域已選擇: {points_json}") @pyqtSlot(str) def polygonSelected(self, points_json): """供 JavaScript 調用的方法 - 多邊形選擇完成""" self.polygon_selected.emit(points_json) _log("INFO", f"多邊形區域已選擇: {points_json}") @pyqtSlot(str) def missionModeChanged(self, mode): """供 JavaScript 調用的方法 - 任務模式切換""" self.mission_mode_changed.emit(mode) _log("INFO", f"任務模式已切換: {mode}") @pyqtSlot(str) def routeConfirmed(self, points_json): """供 JavaScript 調用的方法 - 路徑確認""" self.route_confirmed.emit(points_json) _log("INFO", f"路徑已確認: {points_json}") @pyqtSlot(str) def droneBoxSelected(self, drone_ids_json): """供 JavaScript 調用的方法 - 框選無人機完成""" self.drone_box_selected.emit(drone_ids_json) _log("INFO", f"框選無人機完成: {drone_ids_json}")