You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AirTrapMine/src/GUI/map_layout.py

1098 lines
45 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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 = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-rotatedmarker/leaflet.rotatedMarker.js"></script>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<style>
html, body, #map { height: 100%; margin: 0; }
#map {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
background-color: rgba(255, 255, 255, 0.95);
padding: 10px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
gap: 5px;
min-width: 150px;
}
.map-controls-bottom {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 1000;
}
.control-button {
padding: 8px 12px;
background-color: #EF5350;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.control-button:hover {
background-color: #E53935;
}
.mission-info-panel {
position: absolute;
bottom: 10px;
right: 10px;
z-index: 1000;
background-color: rgba(255, 255, 255, 0.95);
padding: 12px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
min-width: 200px;
}
.mission-info-row {
margin-bottom: 8px;
font-size: 12px;
color: #333;
}
.mission-info-label {
font-weight: bold;
color: #555;
}
.mission-info-value {
color: #2196F3;
font-family: monospace;
}
.mission-start-button {
width: 100%;
padding: 10px;
background-color: #7FBA82;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
margin-top: 8px;
}
.mission-start-button:hover {
background-color: #6FA872;
}
.mission-start-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.selection-buttons {
display: flex;
flex-direction: column;
gap: 5px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
}
.selection-button-blue {
width: 100%;
padding: 8px;
background-color: #64B5F6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
transition: background-color 0.2s;
}
.selection-button-blue:hover {
background-color: #42A5F5;
}
.selection-button-blue.active {
background-color: #1976D2;
}
.selection-button {
width: 100%;
padding: 8px;
background-color: #F06A61;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
transition: background-color 0.2s;
}
.selection-button:hover {
background-color: #E53935;
}
.selection-button.active {
background-color: #D32F2F;
}
.confirm-route-button {
width: 100%;
padding: 8px;
background-color: #66BB6A;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
transition: background-color 0.2s;
}
.confirm-route-button:hover {
background-color: #4CAF50;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="map-controls" id="map-controls-panel" style="display: none;">
<button class="confirm-route-button" id="confirm-route-btn" onclick="confirmRoute()">確認路徑</button>
</div>
<script>
var bridge;
new QWebChannel(qt.webChannelTransport, function(channel) {
bridge = channel.objects.bridge;
});
var map = L.map('map').setView([0, 0], 19);
// 創建不同的地圖圖層
var streetLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
});
var satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19,
attribution: 'Tiles © Esri'
});
// 默認使用衛星圖
var currentLayer = satelliteLayer;
currentLayer.addTo(map);
var isSatellite = true;
// 地圖點擊事件
map.on('click', function(e) {
if (selectionMode === 'polygon') {
addPolygonPoint(e.latlng.lat, e.latlng.lng);
} else if (selectionMode === 'route') {
// 跟隨模式:添加路徑點
addRoutePoint(e.latlng.lat, e.latlng.lng);
} else if (!selectionMode) {
// 正常模式發送GPS信號
if (bridge) {
bridge.emitGpsSignal(e.latlng.lat, e.latlng.lng);
console.log('點擊位置:', e.latlng.lat, e.latlng.lng);
}
}
});
// 地圖拖曳事件(用於矩形選擇)
map.on('mousedown', function(e) {
mapDragStarted = false;
clickStartPos = {x: e.originalEvent.clientX, y: e.originalEvent.clientY};
if (selectionMode === 'rect' || selectionMode === 'drones') {
isDrawing = true;
drawStartPoint = e.latlng;
L.DomEvent.preventDefault(e.originalEvent);
L.DomEvent.stopPropagation(e);
}
});
map.on('mousemove', function(e) {
// 檢測地圖拖移
if (clickStartPos && !selectionMode) {
var dx = Math.abs(e.originalEvent.clientX - clickStartPos.x);
var dy = Math.abs(e.originalEvent.clientY - clickStartPos.y);
if (dx > 5 || dy > 5) {
mapDragStarted = true;
}
}
if (isDrawing && (selectionMode === 'rect' || selectionMode === 'drones') && drawStartPoint) {
if (tempRectangle) {
selectionLayer.removeLayer(tempRectangle);
}
var bounds = L.latLngBounds(drawStartPoint, e.latlng);
tempRectangle = L.rectangle(bounds, {
color: selectionMode === 'drones' ? '#9C27B0' : '#FF6B6B',
weight: 2,
fillOpacity: selectionMode === 'drones' ? 0 : 0.2,
dashArray: selectionMode === 'drones' ? '5, 5' : null
}).addTo(selectionLayer);
}
});
map.on('mouseup', function(e) {
if (isDrawing && (selectionMode === 'rect' || selectionMode === 'drones') && drawStartPoint) {
isDrawing = false;
var bounds = L.latLngBounds(drawStartPoint, e.latlng);
finishRectSelection(bounds);
drawStartPoint = null;
}
clickStartPos = null;
setTimeout(function() {
mapDragStarted = false;
}, 100);
});
function createArrowIcon(color) {
return L.divIcon({
className: 'drone-arrow',
html: `
<div style="
width: 30px; height: 30px;
display: flex;
align-items: center;
justify-content: center;
">
<svg width="30" height="30" viewBox="0 0 30 30">
<polygon points="15,0 30,30 15,25 0,30" fill="${color || '#FF0000'}" />
</svg>
</div>
`,
iconSize: [30, 30],
iconAnchor: [15, 15]
});
}
function getColorBySocketId(id) {
if (id.startsWith("s0_")) return "#00BFFF";
if (id.startsWith("s1_")) return "#FFD700";
if (id.startsWith("s2_")) return "#FF6969";
if (id.startsWith("s3_")) return "#FF69B4";
if (id.startsWith("s4_")) return "#00FA9A";
if (id.startsWith("s5_")) return "#9370DB";
if (id.startsWith("s6_")) return "#FFA500";
if (id.startsWith("s7_")) return "#20B2AA";
if (id.startsWith("s8_")) return "#7CFC00";
if (id.startsWith("s9_")) return "#FF8C00";
return "#AAAAAA";
}
function createIdIcon(id) {
const sysid = id.split('_')[1];
return L.divIcon({
className: 'drone-id',
html: `<div style="
background-color: transparent;
color: black;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 12px;
text-shadow: 1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8);
">${sysid}</div>`,
iconSize: [16, 16],
iconAnchor: [8, 6]
});
}
var markers = {};
var idLabels = {};
var trajectories = {};
var trajectoryLines = {};
var focusedId = null;
var initialized = false;
var trajectoryGroup = L.layerGroup().addTo(map);
// ================================================================================
// 任務規劃視覺化變數
// ================================================================================
var missionPlanGroup = L.layerGroup().addTo(map);
var centerMarker = null;
var targetMarker = null;
var missionLine = null;
var centerPosition = null;
var targetPosition = null;
// 多群組任務規劃 — 每個 group 各自的 layer group
var groupMissionLayers = {}; // group_id → L.layerGroup
// 選擇工具變量
var selectionMode = null; // 'drones', 'rect', 'polygon', 'route', null
var selectionLayer = L.layerGroup().addTo(map);
var polygonPoints = [];
var polygonMarkers = [];
var tempRectangle = null;
var isDrawing = false;
var drawStartPoint = null;
var mapDragStarted = false;
var clickStartPos = null;
// 路徑標記變量 (跟隨模式用)
var routePoints = [];
var routeMarkers = [];
var routePolyline = null;
var routeLayer = L.layerGroup().addTo(map);
// ================================================================================
// 任務信息已移至 GroupPanel 顯示
function updateMissionInfo() {}
// ================================================================================
// 選擇工具函數
// ================================================================================
function toggleSelectAllDrones() {
if (bridge) {
bridge.toggleSelectAllDrones();
console.log('切換全選無人機');
}
}
function toggleDroneSelection() {
if (selectionMode === 'drones') {
selectionMode = null;
selectionLayer.clearLayers();
tempRectangle = null;
map.dragging.enable();
} else {
clearSelectionMode();
selectionMode = 'drones';
map.dragging.disable();
}
}
function togglePolygonSelection() {
if (selectionMode === 'polygon') {
if (polygonPoints.length >= 3) {
finishPolygonSelection();
} else {
alert('至少需要3個點來形成區域');
clearSelectionMode();
clearPolygonPoints();
selectionMode = null;
// select-polygon-btn removed from map overlay
}
} else {
clearSelectionMode();
clearPolygonPoints();
selectionMode = 'polygon';
document.getElementById('select-polygon-btn').classList.add('active');
map.dragging.disable();
}
}
function clearSelectionMode() {
selectionLayer.clearLayers();
tempRectangle = null;
// 不清除 routeLayer由 clearRoutePoints 單獨管理)
map.dragging.enable();
if (selectionMode !== 'route') {
selectionMode = null;
}
}
function addPolygonPoint(lat, lng) {
polygonPoints.push([lat, lng]);
var marker = L.circleMarker([lat, lng], {
radius: 5,
color: '#FF6B6B',
fillColor: '#FF6B6B',
fillOpacity: 0.8
}).addTo(selectionLayer);
polygonMarkers.push(marker);
if (polygonPoints.length > 1) {
L.polyline(polygonPoints, {
color: '#FF6B6B',
weight: 2,
dashArray: '5, 5'
}).addTo(selectionLayer);
}
console.log('添加邊界點:', lat, lng, '總共:', polygonPoints.length);
}
function clearPolygonPoints() {
polygonPoints = [];
polygonMarkers.forEach(m => selectionLayer.removeLayer(m));
polygonMarkers = [];
selectionLayer.clearLayers();
}
function finishPolygonSelection() {
if (polygonPoints.length < 3) {
alert('至少需要3個點來形成區域');
return;
}
var polygon = L.polygon(polygonPoints, {
color: '#FF6B6B',
fillColor: '#FF6B6B',
fillOpacity: 0.2,
weight: 2
}).addTo(selectionLayer);
if (bridge) {
var pointsStr = JSON.stringify(polygonPoints);
bridge.polygonSelected(pointsStr);
console.log('多邊形選擇完成:', polygonPoints);
}
selectionMode = null;
// select-polygon-btn removed from map overlay
map.dragging.enable();
}
function finishRectSelection(bounds) {
var selectedDrones = [];
if (tempRectangle) {
selectionLayer.removeLayer(tempRectangle);
tempRectangle = null;
}
if (selectionMode === 'drones') {
if (bridge) {
bridge.clearAllDroneSelection();
}
Object.keys(markers).forEach(droneId => {
var pos = markers[droneId].getLatLng();
if (bounds.contains(pos)) {
selectedDrones.push(droneId);
if (bridge) {
bridge.emitDroneClicked(droneId);
}
}
});
console.log('框選無人機:', selectedDrones);
// 通知 Python 完整的框選結果
if (bridge && selectedDrones.length > 0) {
bridge.droneBoxSelected(JSON.stringify(selectedDrones));
}
} else if (selectionMode === 'rect') {
var rectPoints = [
[bounds.getNorth(), bounds.getWest()],
[bounds.getNorth(), bounds.getEast()],
[bounds.getSouth(), bounds.getEast()],
[bounds.getSouth(), bounds.getWest()]
];
L.rectangle(bounds, {
color: '#FF6B6B',
fillColor: '#FF6B6B',
fillOpacity: 0.2,
weight: 2
}).addTo(selectionLayer);
rectPoints.forEach(point => {
L.circleMarker(point, {
radius: 5,
color: '#FF6B6B',
fillColor: '#FF6B6B',
fillOpacity: 0.8
}).addTo(selectionLayer);
});
if (bridge) {
var pointsStr = JSON.stringify(rectPoints);
bridge.rectangleSelected(pointsStr);
console.log('矩形選擇完成:', rectPoints);
}
}
// 重置狀態
selectionMode = null;
map.dragging.enable();
}
// ================================================================================
// 路徑標記函數 (跟隨模式用)
// ================================================================================
function addRoutePoint(lat, lng) {
routePoints.push([lat, lng]);
var idx = routePoints.length;
// 添加編號標記
var icon = L.divIcon({
className: 'route-point',
html: '<div style="' +
'background-color: #FF5722;' +
'color: white;' +
'width: 22px;' +
'height: 22px;' +
'border-radius: 50%;' +
'display: flex;' +
'align-items: center;' +
'justify-content: center;' +
'font-weight: bold;' +
'font-size: 11px;' +
'border: 2px solid white;' +
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
'">' + idx + '</div>',
iconSize: [22, 22],
iconAnchor: [11, 11]
});
var marker = L.marker([lat, lng], {
icon: icon,
zIndexOffset: 3000
}).addTo(routeLayer);
routeMarkers.push(marker);
// 更新連線
if (routePolyline) {
routeLayer.removeLayer(routePolyline);
}
if (routePoints.length > 1) {
routePolyline = L.polyline(routePoints, {
color: '#FF5722',
weight: 2.5,
opacity: 0.8,
dashArray: '8, 6'
}).addTo(routeLayer);
}
console.log('添加路徑點 #' + idx + ':', lat, lng);
}
function clearRoutePoints() {
routePoints = [];
routeMarkers = [];
if (routePolyline) {
routeLayer.removeLayer(routePolyline);
routePolyline = null;
}
routeLayer.clearLayers();
}
function confirmRoute() {
if (routePoints.length < 2) {
alert('至少需要 2 個路徑點');
return;
}
if (bridge) {
var pointsStr = JSON.stringify(routePoints);
bridge.routeConfirmed(pointsStr);
console.log('路徑確認:', routePoints.length, '個點');
}
}
// ================================================================================
// 開始任務
function startMission() {
if (!centerPosition || !targetPosition) {
alert('請先設定中心點和目標點');
return;
}
if (bridge) {
bridge.startMissionSignal(
centerPosition.lat, centerPosition.lng,
targetPosition.lat, targetPosition.lng
);
console.log('開始任務:', centerPosition, targetPosition);
}
}
// 暫停任務
function pauseMission() {
if (bridge) {
bridge.pauseMissionSignal();
console.log('暫停任務');
}
}
// ================================================================================
// 任務模式切換
// ================================================================================
function onMissionModeChanged(mode) {
// 先清除選擇狀態
selectionMode = null;
clearSelectionMode();
clearRoutePoints();
// 預設隱藏控制面板
document.getElementById('map-controls-panel').style.display = 'none';
if (mode === 'GRID_SWEEP') {
// 自動進入框選模式
selectionMode = 'rect';
map.dragging.disable();
console.log('Grid Sweep: 進入框選模式');
} else if (mode === 'LEADER_FOLLOWER') {
// 進入路徑標記模式 + 顯示確認路徑按鈕
selectionMode = 'route';
document.getElementById('map-controls-panel').style.display = 'flex';
console.log('跟隨模式: 進入路徑標記模式,點擊地圖添加路徑點');
}
if (bridge) {
bridge.missionModeChanged(mode);
console.log('任務模式切換:', mode);
}
}
// ================================================================================
function initTrajectory(id) {
if (!trajectories[id]) {
trajectories[id] = [];
const color = getColorBySocketId(id);
trajectoryLines[id] = L.polyline([], {
color: color,
weight: 3,
opacity: 0.7,
smoothFactor: 1
}).addTo(trajectoryGroup);
}
}
function addTrajectoryPoint(id, lat, lon) {
initTrajectory(id);
const point = [lat, lon];
trajectories[id].push(point);
if (trajectories[id].length > 1000) {
trajectories[id].shift();
}
trajectoryLines[id].setLatLngs([...trajectories[id]]);
}
function focusOn(id) {
if (!markers[id]) return;
focusedId = id;
var latlng = markers[id].getLatLng();
map.flyTo(latlng, map.getZoom());
}
setInterval(() => {
if (focusedId && markers[focusedId]) {
var latlng = markers[focusedId].getLatLng();
map.panTo(latlng);
}
}, 1000);
function updateDrone(lat, lon, id, heading) {
if (markers[id]) {
const lastPos = markers[id].getLatLng();
const distance = lastPos.distanceTo([lat, lon]);
if (distance > 1) {
addTrajectoryPoint(id, lat, lon);
}
markers[id]
.setLatLng([lat, lon])
.setRotationAngle(heading);
idLabels[id].setLatLng([lat, lon]);
} else {
initTrajectory(id);
addTrajectoryPoint(id, lat, lon);
const color = getColorBySocketId(id);
markers[id] = L.marker([lat, lon], {
icon: createArrowIcon(color),
rotationAngle: heading,
rotationOrigin: 'center'
})
.on('click', function () {
if (mapDragStarted) {
return;
}
if (bridge) {
bridge.emitDroneClicked(id);
}
focusOn(id);
})
.addTo(map);
idLabels[id] = L.marker([lat, lon], {
icon: createIdIcon(id),
zIndexOffset: 1000
})
.on('click', function() {
if (mapDragStarted) {
return;
}
if (bridge) {
bridge.emitDroneClicked(id);
}
focusOn(id);
})
.addTo(map);
if (!initialized || id < focusedId) {
focusedId = id;
map.setView([lat, lon], 19);
initialized = true;
}
}
}
function clearAllTrajectories() {
trajectories = {};
Object.values(trajectoryLines).forEach(line => {
trajectoryGroup.removeLayer(line);
});
trajectoryLines = {};
console.log('所有軌跡已清除');
}
// ================================================================================
// 任務規劃視覺化函式
// ================================================================================
function drawMissionPlan(centerLat, centerLon, targetLat, targetLon) {
drawMissionPlanForGroup('_default', '#FF4444', centerLat, centerLon, targetLat, targetLon);
}
function drawMissionPlanForGroup(groupId, color, centerLat, centerLon, targetLat, targetLon) {
clearMissionPlanForGroup(groupId);
centerPosition = {lat: centerLat, lng: centerLon};
targetPosition = {lat: targetLat, lng: targetLon};
updateMissionInfo();
var layerGroup = L.layerGroup().addTo(map);
groupMissionLayers[groupId] = layerGroup;
var centerIcon = L.divIcon({
className: 'mission-center',
html: '<div style="' +
'background-color: ' + color + ';' +
'color: white;' +
'width: 22px; height: 22px;' +
'border-radius: 50%;' +
'display: flex; align-items: center; justify-content: center;' +
'font-weight: bold; font-size: 10px;' +
'border: 2px solid white;' +
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
'">' + groupId + '</div>',
iconSize: [22, 22],
iconAnchor: [11, 11]
});
L.marker([centerLat, centerLon], {
icon: centerIcon, zIndexOffset: 2000
}).addTo(layerGroup);
var targetIcon = L.divIcon({
className: 'mission-target',
html: '<div style="' +
'background-color: #FFD700;' +
'color: ' + color + ';' +
'width: 22px; height: 22px;' +
'border-radius: 50%;' +
'display: flex; align-items: center; justify-content: center;' +
'font-weight: bold; font-size: 14px;' +
'border: 2px solid white;' +
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
'">★</div>',
iconSize: [22, 22],
iconAnchor: [11, 11]
});
L.marker([targetLat, targetLon], {
icon: targetIcon, zIndexOffset: 2000
}).addTo(layerGroup);
console.log('Group ' + groupId + ' 任務規劃已繪製');
}
function clearMissionPlanForGroup(groupId) {
if (groupMissionLayers[groupId]) {
map.removeLayer(groupMissionLayers[groupId]);
delete groupMissionLayers[groupId];
}
}
function clearAllMissionPlans() {
for (var gid in groupMissionLayers) {
map.removeLayer(groupMissionLayers[gid]);
}
groupMissionLayers = {};
centerPosition = null;
targetPosition = null;
updateMissionInfo();
console.log('任務規劃已清除');
}
// ================================================================================
</script>
</body>
</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}")