feat: 新增任務規劃系統 (M-Formation, Circle, Leader-Follower, Grid Sweep)
- mission_planner: 四種任務模式,Leader-Follower 支援 Bezier 曲線轉彎 - mission_executor: QTimer 驅動的多航點執行器 - command_sender: MAVLink setpoint 發送抽象層 - gui: 地圖任務模式選單 + 框選/路徑標記觸發 - map_layout: 任務模式下拉選單 + 確認路徑按鈕 + route bridge 信號 - verify_waypoints: 靜態航點圖 + 動畫模擬驗證 (Play/Pause/Reset)
This commit is contained in:
+221
-118
@@ -122,24 +122,8 @@ class DroneMap:
|
||||
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-blue {
|
||||
.selection-button-blue {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background-color: #64B5F6;
|
||||
@@ -175,14 +159,38 @@ class DroneMap:
|
||||
.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">
|
||||
<button class="selection-button" onclick="clearAllTrajectories()">清除軌跡</button>
|
||||
<button class="selection-button" id="select-rect-btn" onclick="toggleRectSelection()">框選方形區域</button>
|
||||
<button class="selection-button" id="select-polygon-btn" onclick="togglePolygonSelection()">多點選擇區域</button>
|
||||
<div style="border-top: 1px solid #ddd; padding-top: 5px; margin-top: 5px;">
|
||||
<label style="font-size: 12px; color: #555; font-weight: bold;">任務模式</label>
|
||||
<select id="mission-mode-select" onchange="onMissionModeChanged(this.value)" style="width: 100%; padding: 6px; border-radius: 4px; border: 1px solid #ccc; font-size: 12px; margin-top: 3px;">
|
||||
<option value="M_FORMATION">列隊飛行</option>
|
||||
<option value="CIRCLE_FORMATION">環狀包圍</option>
|
||||
<option value="LEADER_FOLLOWER">跟隨模式</option>
|
||||
<option value="GRID_SWEEP">柵狀偵查</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="confirm-route-button" id="confirm-route-btn" onclick="confirmRoute()" style="display: none;">確認路徑</button>
|
||||
<button class="selection-button" id="select-polygon-btn" onclick="togglePolygonSelection()">多點選擇區域 (開發中)</button>
|
||||
</div>
|
||||
<div class="mission-info-panel">
|
||||
<div class="selection-buttons">
|
||||
@@ -229,8 +237,10 @@ class DroneMap:
|
||||
// 地圖點擊事件
|
||||
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) {
|
||||
@@ -264,7 +274,6 @@ class DroneMap:
|
||||
}
|
||||
|
||||
if (isDrawing && (selectionMode === 'rect' || selectionMode === 'drones') && drawStartPoint) {
|
||||
// 更新臨時矩形
|
||||
if (tempRectangle) {
|
||||
selectionLayer.removeLayer(tempRectangle);
|
||||
}
|
||||
@@ -286,7 +295,6 @@ class DroneMap:
|
||||
drawStartPoint = null;
|
||||
}
|
||||
|
||||
// 重置拖移狀態
|
||||
clickStartPos = null;
|
||||
setTimeout(function() {
|
||||
mapDragStarted = false;
|
||||
@@ -309,22 +317,22 @@ class DroneMap:
|
||||
</div>
|
||||
`,
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 15] // ★ 箭頭往左上移,使 ID 顯示在右下
|
||||
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"; // 灰色 (預設)
|
||||
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) {
|
||||
@@ -345,7 +353,7 @@ class DroneMap:
|
||||
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] // ★ icon 中心 = 經緯度點
|
||||
iconAnchor: [8, 6]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -358,25 +366,31 @@ class DroneMap:
|
||||
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; // 目標點經緯度
|
||||
var missionPlanGroup = L.layerGroup().addTo(map);
|
||||
var centerMarker = null;
|
||||
var targetMarker = null;
|
||||
var missionLine = null;
|
||||
var centerPosition = null;
|
||||
var targetPosition = null;
|
||||
|
||||
// 選擇工具變量
|
||||
var selectionMode = null; // 'drones', 'rect', 'polygon', null
|
||||
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 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);
|
||||
// ================================================================================
|
||||
|
||||
// 更新任務信息面板
|
||||
@@ -397,7 +411,6 @@ class DroneMap:
|
||||
targetElem.textContent = '未設定';
|
||||
}
|
||||
|
||||
// 只有當中心點和目標點都設定時才啟用按鈕
|
||||
if (centerPosition && targetPosition) {
|
||||
startBtn.disabled = false;
|
||||
} else {
|
||||
@@ -409,7 +422,6 @@ class DroneMap:
|
||||
// 選擇工具函數
|
||||
// ================================================================================
|
||||
function toggleSelectAllDrones() {
|
||||
// 切換全選/取消全選無人機
|
||||
if (bridge) {
|
||||
bridge.toggleSelectAllDrones();
|
||||
console.log('切換全選無人機');
|
||||
@@ -429,34 +441,18 @@ class DroneMap:
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRectSelection() {
|
||||
clearSelectionMode();
|
||||
if (selectionMode === 'rect') {
|
||||
selectionMode = null;
|
||||
document.getElementById('select-rect-btn').classList.remove('active');
|
||||
map.dragging.enable();
|
||||
} else {
|
||||
selectionMode = 'rect';
|
||||
document.getElementById('select-rect-btn').classList.add('active');
|
||||
map.dragging.disable();
|
||||
}
|
||||
}
|
||||
|
||||
function togglePolygonSelection() {
|
||||
if (selectionMode === 'polygon') {
|
||||
// 第二次點擊:完成選擇
|
||||
if (polygonPoints.length >= 3) {
|
||||
finishPolygonSelection();
|
||||
} else {
|
||||
alert('至少需要3個點來形成區域');
|
||||
// 清除並重置
|
||||
clearSelectionMode();
|
||||
clearPolygonPoints();
|
||||
selectionMode = null;
|
||||
document.getElementById('select-polygon-btn').classList.remove('active');
|
||||
}
|
||||
} else {
|
||||
// 第一次點擊:清除上次的點位並啟動模式
|
||||
clearSelectionMode();
|
||||
clearPolygonPoints();
|
||||
selectionMode = 'polygon';
|
||||
@@ -466,23 +462,25 @@ class DroneMap:
|
||||
}
|
||||
|
||||
function clearSelectionMode() {
|
||||
// 清除所有按鈕的激活狀態
|
||||
document.getElementById('select-drones-btn').classList.remove('active');
|
||||
document.getElementById('select-rect-btn').classList.remove('active');
|
||||
document.getElementById('select-polygon-btn').classList.remove('active');
|
||||
|
||||
// 清除選擇圖層
|
||||
selectionLayer.clearLayers();
|
||||
tempRectangle = null;
|
||||
|
||||
// 重新啟用地圖拖曳
|
||||
// 不清除 routeLayer(由 clearRoutePoints 單獨管理)
|
||||
|
||||
map.dragging.enable();
|
||||
|
||||
// 如果離開 route 模式,重置 selectionMode
|
||||
if (selectionMode !== 'route') {
|
||||
selectionMode = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addPolygonPoint(lat, lng) {
|
||||
polygonPoints.push([lat, lng]);
|
||||
|
||||
// 添加邊界點標記
|
||||
var marker = L.circleMarker([lat, lng], {
|
||||
radius: 5,
|
||||
color: '#FF6B6B',
|
||||
@@ -491,7 +489,6 @@ class DroneMap:
|
||||
}).addTo(selectionLayer);
|
||||
polygonMarkers.push(marker);
|
||||
|
||||
// 如果有多個點,繪製連線
|
||||
if (polygonPoints.length > 1) {
|
||||
L.polyline(polygonPoints, {
|
||||
color: '#FF6B6B',
|
||||
@@ -516,7 +513,6 @@ class DroneMap:
|
||||
return;
|
||||
}
|
||||
|
||||
// 繪製最終多邊形
|
||||
var polygon = L.polygon(polygonPoints, {
|
||||
color: '#FF6B6B',
|
||||
fillColor: '#FF6B6B',
|
||||
@@ -524,14 +520,12 @@ class DroneMap:
|
||||
weight: 2
|
||||
}).addTo(selectionLayer);
|
||||
|
||||
// 發送多邊形數據到Python
|
||||
if (bridge) {
|
||||
var pointsStr = JSON.stringify(polygonPoints);
|
||||
bridge.polygonSelected(pointsStr);
|
||||
console.log('多邊形選擇完成:', polygonPoints);
|
||||
}
|
||||
|
||||
// 重置狀態
|
||||
selectionMode = null;
|
||||
document.getElementById('select-polygon-btn').classList.remove('active');
|
||||
map.dragging.enable();
|
||||
@@ -540,14 +534,12 @@ class DroneMap:
|
||||
function finishRectSelection(bounds) {
|
||||
var selectedDrones = [];
|
||||
|
||||
// 清除臨時矩形
|
||||
if (tempRectangle) {
|
||||
selectionLayer.removeLayer(tempRectangle);
|
||||
tempRectangle = null;
|
||||
}
|
||||
|
||||
if (selectionMode === 'drones') {
|
||||
// 框選無人機模式:先清除全選
|
||||
if (bridge) {
|
||||
bridge.clearAllDroneSelection();
|
||||
}
|
||||
@@ -556,17 +548,14 @@ class DroneMap:
|
||||
var pos = markers[droneId].getLatLng();
|
||||
if (bounds.contains(pos)) {
|
||||
selectedDrones.push(droneId);
|
||||
// 觸發無人機點擊信號
|
||||
if (bridge) {
|
||||
bridge.emitDroneClicked(droneId);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('框選無人機:', selectedDrones);
|
||||
// 不保留選擇框,直接完成
|
||||
|
||||
} else if (selectionMode === 'rect') {
|
||||
// 框選區域模式
|
||||
var rectPoints = [
|
||||
[bounds.getNorth(), bounds.getWest()],
|
||||
[bounds.getNorth(), bounds.getEast()],
|
||||
@@ -574,7 +563,6 @@ class DroneMap:
|
||||
[bounds.getSouth(), bounds.getWest()]
|
||||
];
|
||||
|
||||
// 繪製最終矩形
|
||||
L.rectangle(bounds, {
|
||||
color: '#FF6B6B',
|
||||
fillColor: '#FF6B6B',
|
||||
@@ -582,7 +570,6 @@ class DroneMap:
|
||||
weight: 2
|
||||
}).addTo(selectionLayer);
|
||||
|
||||
// 添加四個角的標記
|
||||
rectPoints.forEach(point => {
|
||||
L.circleMarker(point, {
|
||||
radius: 5,
|
||||
@@ -592,7 +579,6 @@ class DroneMap:
|
||||
}).addTo(selectionLayer);
|
||||
});
|
||||
|
||||
// 發送矩形數據到Python
|
||||
if (bridge) {
|
||||
var pointsStr = JSON.stringify(rectPoints);
|
||||
bridge.rectangleSelected(pointsStr);
|
||||
@@ -603,8 +589,90 @@ class DroneMap:
|
||||
// 重置狀態
|
||||
selectionMode = null;
|
||||
document.getElementById('select-drones-btn').classList.remove('active');
|
||||
document.getElementById('select-rect-btn').classList.remove('active');
|
||||
map.dragging.enable();
|
||||
|
||||
// 如果仍在 Grid Sweep 模式,重新進入框選
|
||||
var currentMode = document.getElementById('mission-mode-select').value;
|
||||
if (currentMode === 'GRID_SWEEP') {
|
||||
setTimeout(function() {
|
||||
selectionMode = 'rect';
|
||||
map.dragging.disable();
|
||||
console.log('Grid Sweep: 重新進入框選模式');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// 路徑標記函數 (跟隨模式用)
|
||||
// ================================================================================
|
||||
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, '個點');
|
||||
}
|
||||
}
|
||||
// ================================================================================
|
||||
|
||||
@@ -631,6 +699,36 @@ class DroneMap:
|
||||
console.log('暫停任務');
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// 任務模式切換
|
||||
// ================================================================================
|
||||
function onMissionModeChanged(mode) {
|
||||
// 先清除選擇狀態
|
||||
selectionMode = null;
|
||||
clearSelectionMode();
|
||||
clearRoutePoints();
|
||||
|
||||
// 隱藏確認路徑按鈕
|
||||
document.getElementById('confirm-route-btn').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('confirm-route-btn').style.display = 'block';
|
||||
console.log('跟隨模式: 進入路徑標記模式,點擊地圖添加路徑點');
|
||||
}
|
||||
|
||||
if (bridge) {
|
||||
bridge.missionModeChanged(mode);
|
||||
console.log('任務模式切換:', mode);
|
||||
}
|
||||
}
|
||||
// ================================================================================
|
||||
|
||||
function initTrajectory(id) {
|
||||
@@ -696,7 +794,7 @@ class DroneMap:
|
||||
})
|
||||
.on('click', function () {
|
||||
if (mapDragStarted) {
|
||||
return; // 如果是拖移地圖,不處理點擊
|
||||
return;
|
||||
}
|
||||
if (bridge) {
|
||||
bridge.emitDroneClicked(id);
|
||||
@@ -711,7 +809,7 @@ class DroneMap:
|
||||
})
|
||||
.on('click', function() {
|
||||
if (mapDragStarted) {
|
||||
return; // 如果是拖移地圖,不處理點擊
|
||||
return;
|
||||
}
|
||||
if (bridge) {
|
||||
bridge.emitDroneClicked(id);
|
||||
@@ -722,7 +820,7 @@ class DroneMap:
|
||||
|
||||
if (!initialized || id < focusedId) {
|
||||
focusedId = id;
|
||||
map.setView([lat, lon], 19); // 第一台無人機:重置並放大到最大
|
||||
map.setView([lat, lon], 19);
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
@@ -738,18 +836,15 @@ class DroneMap:
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// 【新增】任務規劃視覺化函式
|
||||
// 任務規劃視覺化函式
|
||||
// ================================================================================
|
||||
function drawMissionPlan(centerLat, centerLon, targetLat, targetLon) {
|
||||
// 清除舊的任務規劃標記
|
||||
clearMissionPlan();
|
||||
|
||||
// 保存中心點和目標點位置
|
||||
centerPosition = {lat: centerLat, lng: centerLon};
|
||||
targetPosition = {lat: targetLat, lng: targetLon};
|
||||
updateMissionInfo();
|
||||
|
||||
// 繪製中心點標記 "C"(縮小到 20px)
|
||||
var centerIcon = L.divIcon({
|
||||
className: 'mission-center',
|
||||
html: `<div style="
|
||||
@@ -775,7 +870,6 @@ class DroneMap:
|
||||
zIndexOffset: 2000
|
||||
}).addTo(missionPlanGroup);
|
||||
|
||||
// 繪製目標點標記 "★"(星星符號)
|
||||
var targetIcon = L.divIcon({
|
||||
className: 'mission-target',
|
||||
html: `<div style="
|
||||
@@ -805,19 +899,16 @@ class DroneMap:
|
||||
}
|
||||
|
||||
function clearMissionPlan() {
|
||||
// 清除中心點標記
|
||||
if (centerMarker) {
|
||||
missionPlanGroup.removeLayer(centerMarker);
|
||||
centerMarker = null;
|
||||
}
|
||||
|
||||
// 清除目標點標記
|
||||
if (targetMarker) {
|
||||
missionPlanGroup.removeLayer(targetMarker);
|
||||
targetMarker = null;
|
||||
}
|
||||
|
||||
// 清除位置資訊
|
||||
centerPosition = null;
|
||||
targetPosition = null;
|
||||
updateMissionInfo();
|
||||
@@ -854,7 +945,6 @@ class DroneMap:
|
||||
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});")
|
||||
@@ -863,7 +953,6 @@ class DroneMap:
|
||||
combined_js = "\n".join(js_commands)
|
||||
self.map_view.page().runJavaScript(combined_js)
|
||||
|
||||
# 清空待更新緩存
|
||||
self.pending_map_updates.clear()
|
||||
|
||||
def clear_trajectories(self):
|
||||
@@ -877,18 +966,10 @@ class DroneMap:
|
||||
self.map_view.page().runJavaScript(f"focusOn('{drone_id}');")
|
||||
|
||||
# ================================================================================
|
||||
# 【新增】任務規劃視覺化方法
|
||||
# 任務規劃視覺化方法
|
||||
# ================================================================================
|
||||
def draw_mission_plan(self, center_lat, center_lon, target_lat, target_lon):
|
||||
"""
|
||||
在地圖上繪製任務規劃
|
||||
|
||||
Args:
|
||||
center_lat: 中心點緯度
|
||||
center_lon: 中心點經度
|
||||
target_lat: 目標點緯度
|
||||
target_lon: 目標點經度
|
||||
"""
|
||||
"""在地圖上繪製任務規劃"""
|
||||
if self.map_loaded:
|
||||
js_code = f"drawMissionPlan({center_lat:.6f}, {center_lon:.6f}, {target_lat:.6f}, {target_lon:.6f});"
|
||||
self.map_view.page().runJavaScript(js_code)
|
||||
@@ -941,16 +1022,26 @@ class DroneMap:
|
||||
"""獲取多邊形選擇信號"""
|
||||
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
|
||||
|
||||
class MapBridge(QObject):
|
||||
"""JavaScript 和 Python 之間的橋接類"""
|
||||
gps_signal = pyqtSignal(float, float) # lat, lon
|
||||
drone_clicked = pyqtSignal(str) # drone_id
|
||||
clear_all_drone_selection = pyqtSignal() # clear all drone selection
|
||||
select_all_drones = pyqtSignal() # select all drones
|
||||
start_mission_signal = pyqtSignal(float, float, float, float) # center_lat, center_lon, target_lat, target_lon
|
||||
pause_mission_signal = pyqtSignal() # pause mission
|
||||
rectangle_selected = pyqtSignal(str) # JSON string of rectangle points
|
||||
polygon_selected = pyqtSignal(str) # JSON string of polygon points
|
||||
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 字串)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -999,4 +1090,16 @@ class MapBridge(QObject):
|
||||
def polygonSelected(self, points_json):
|
||||
"""供 JavaScript 調用的方法 - 多邊形選擇完成"""
|
||||
self.polygon_selected.emit(points_json)
|
||||
print(f"🔷 多邊形區域已選擇: {points_json}")
|
||||
print(f"🔷 多邊形區域已選擇: {points_json}")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def missionModeChanged(self, mode):
|
||||
"""供 JavaScript 調用的方法 - 任務模式切換"""
|
||||
self.mission_mode_changed.emit(mode)
|
||||
print(f"🔄 任務模式切換: {mode}")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def routeConfirmed(self, points_json):
|
||||
"""供 JavaScript 調用的方法 - 路徑確認"""
|
||||
self.route_confirmed.emit(points_json)
|
||||
print(f"📍 路徑已確認: {points_json}")
|
||||
Reference in New Issue
Block a user