2.8.1
This commit is contained in:
+32
-3
@@ -1318,6 +1318,7 @@ class ControlStationUI(QMainWindow):
|
|||||||
"""切換 group tab — 只需切換 active group 並刷新 UI"""
|
"""切換 group tab — 只需切換 active group 並刷新 UI"""
|
||||||
if index < 0:
|
if index < 0:
|
||||||
self.active_group_id = None
|
self.active_group_id = None
|
||||||
|
self._sync_map_follow_for_active_group()
|
||||||
return
|
return
|
||||||
|
|
||||||
tab_text = self.group_tab_widget.tabText(index)
|
tab_text = self.group_tab_widget.tabText(index)
|
||||||
@@ -1345,6 +1346,13 @@ class ControlStationUI(QMainWindow):
|
|||||||
group = self._get_active_group()
|
group = self._get_active_group()
|
||||||
selected = group.selected_drone_ids if group else set()
|
selected = group.selected_drone_ids if group else set()
|
||||||
current_group_id = group.group_id if group else None
|
current_group_id = group.group_id if group else None
|
||||||
|
selection_history = getattr(
|
||||||
|
self, '_last_map_selection_by_group', {})
|
||||||
|
previous_selected = selection_history.get(current_group_id, set())
|
||||||
|
newly_selected = set(selected) - set(previous_selected)
|
||||||
|
if current_group_id is not None:
|
||||||
|
selection_history[current_group_id] = set(selected)
|
||||||
|
self._last_map_selection_by_group = selection_history
|
||||||
|
|
||||||
# 左側 drone checkbox
|
# 左側 drone checkbox
|
||||||
for drone_id, panel in self.drones.items():
|
for drone_id, panel in self.drones.items():
|
||||||
@@ -1372,6 +1380,23 @@ class ControlStationUI(QMainWindow):
|
|||||||
panel.update_status()
|
panel.update_status()
|
||||||
|
|
||||||
self._sync_map_group_colors()
|
self._sync_map_group_colors()
|
||||||
|
self._sync_map_follow_for_active_group()
|
||||||
|
if newly_selected:
|
||||||
|
# 選取 UAV 時移動到該機目前位置;非任務狀態只移動一次。
|
||||||
|
self.drone_map.focus_on_drone(sorted(newly_selected)[0])
|
||||||
|
|
||||||
|
def _sync_map_follow_for_active_group(self):
|
||||||
|
"""只在 active group 的任務執行中啟用地圖 UAV 跟隨。"""
|
||||||
|
group = self._get_active_group()
|
||||||
|
running = bool(
|
||||||
|
group is not None
|
||||||
|
and group.executor is not None
|
||||||
|
and group.executor.state == MissionState.RUNNING
|
||||||
|
)
|
||||||
|
preferred_drone_id = None
|
||||||
|
if running and group.selected_drone_ids:
|
||||||
|
preferred_drone_id = sorted(group.selected_drone_ids)[0]
|
||||||
|
self.drone_map.set_mission_following(running, preferred_drone_id)
|
||||||
|
|
||||||
def _sync_map_group_colors(self):
|
def _sync_map_group_colors(self):
|
||||||
"""同步每台 drone 的 group 顏色到地圖箭頭外框。"""
|
"""同步每台 drone 的 group 顏色到地圖箭頭外框。"""
|
||||||
@@ -1538,6 +1563,7 @@ class ControlStationUI(QMainWindow):
|
|||||||
group.executor.hover_stable_sec = ep.get(
|
group.executor.hover_stable_sec = ep.get(
|
||||||
'hover_stable_sec', group.executor.hover_stable_sec)
|
'hover_stable_sec', group.executor.hover_stable_sec)
|
||||||
group.executor.start(group.planned_waypoints)
|
group.executor.start(group.planned_waypoints)
|
||||||
|
self._sync_map_follow_for_active_group()
|
||||||
panel = self.group_panels.get(group_id)
|
panel = self.group_panels.get(group_id)
|
||||||
if panel:
|
if panel:
|
||||||
panel.update_status()
|
panel.update_status()
|
||||||
@@ -1554,6 +1580,7 @@ class ControlStationUI(QMainWindow):
|
|||||||
elif group.executor.state == MissionState.PAUSED:
|
elif group.executor.state == MissionState.PAUSED:
|
||||||
group.executor.resume()
|
group.executor.resume()
|
||||||
self.statusBar().showMessage(f"Group {group_id}: 任務已恢復", 3000)
|
self.statusBar().showMessage(f"Group {group_id}: 任務已恢復", 3000)
|
||||||
|
self._sync_map_follow_for_active_group()
|
||||||
panel = self.group_panels.get(group_id)
|
panel = self.group_panels.get(group_id)
|
||||||
if panel:
|
if panel:
|
||||||
panel.update_status()
|
panel.update_status()
|
||||||
@@ -1565,6 +1592,7 @@ class ControlStationUI(QMainWindow):
|
|||||||
return
|
return
|
||||||
if group.executor:
|
if group.executor:
|
||||||
group.executor.stop()
|
group.executor.stop()
|
||||||
|
self._sync_map_follow_for_active_group()
|
||||||
group.planned_waypoints = None
|
group.planned_waypoints = None
|
||||||
group.mission_target = None
|
group.mission_target = None
|
||||||
self.drone_map.clear_mission_plan_for_group(group_id)
|
self.drone_map.clear_mission_plan_for_group(group_id)
|
||||||
@@ -1917,6 +1945,7 @@ class ControlStationUI(QMainWindow):
|
|||||||
|
|
||||||
def _on_group_mission_completed(self, group_id):
|
def _on_group_mission_completed(self, group_id):
|
||||||
"""群組任務完成回呼"""
|
"""群組任務完成回呼"""
|
||||||
|
self._sync_map_follow_for_active_group()
|
||||||
panel = self.group_panels.get(group_id)
|
panel = self.group_panels.get(group_id)
|
||||||
if panel:
|
if panel:
|
||||||
panel.update_status()
|
panel.update_status()
|
||||||
@@ -2257,6 +2286,9 @@ class ControlStationUI(QMainWindow):
|
|||||||
self.statusBar().showMessage("請先建立任務群組", 3000)
|
self.statusBar().showMessage("請先建立任務群組", 3000)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 不論後續是否能進行任務規劃,都先記錄最新點擊的目標點。
|
||||||
|
group.mission_target = (float(lat), float(lon))
|
||||||
|
|
||||||
mode_map = {
|
mode_map = {
|
||||||
'M_FORMATION': MissionType.M_FORMATION,
|
'M_FORMATION': MissionType.M_FORMATION,
|
||||||
'CIRCLE_FORMATION': MissionType.CIRCLE_FORMATION,
|
'CIRCLE_FORMATION': MissionType.CIRCLE_FORMATION,
|
||||||
@@ -2265,9 +2297,6 @@ class ControlStationUI(QMainWindow):
|
|||||||
if mission_type is None:
|
if mission_type is None:
|
||||||
return # Grid Sweep / Leader-Follower 由各自的觸發方式處理
|
return # Grid Sweep / Leader-Follower 由各自的觸發方式處理
|
||||||
|
|
||||||
# XBee GOTO 直接使用該 Group 最近一次點擊的地圖座標,與文字欄位解耦。
|
|
||||||
group.mission_target = (float(lat), float(lon))
|
|
||||||
|
|
||||||
selected_drones = self._get_group_drones(group)
|
selected_drones = self._get_group_drones(group)
|
||||||
if len(selected_drones) == 0:
|
if len(selected_drones) == 0:
|
||||||
self.statusBar().showMessage(f"Group {group.group_id}: 請先分配無人機", 3000)
|
self.statusBar().showMessage(f"Group {group.group_id}: 請先分配無人機", 3000)
|
||||||
|
|||||||
+56
-11
@@ -21,6 +21,8 @@ class DroneMap:
|
|||||||
self.font_scale = 1.0
|
self.font_scale = 1.0
|
||||||
self.socket_colors = {}
|
self.socket_colors = {}
|
||||||
self.drone_group_colors = {}
|
self.drone_group_colors = {}
|
||||||
|
self.mission_following_enabled = False
|
||||||
|
self.mission_following_drone_id = None
|
||||||
|
|
||||||
# 創建橋接對象
|
# 創建橋接對象
|
||||||
self.bridge = MapBridge()
|
self.bridge = MapBridge()
|
||||||
@@ -199,16 +201,18 @@ class DroneMap:
|
|||||||
bridge = channel.objects.bridge;
|
bridge = channel.objects.bridge;
|
||||||
});
|
});
|
||||||
|
|
||||||
var map = L.map('map').setView([0, 0], 19);
|
var map = L.map('map', {maxZoom: 20}).setView([0, 0], 19);
|
||||||
|
|
||||||
// 創建不同的地圖圖層
|
// 創建不同的地圖圖層
|
||||||
var streetLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
var streetLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
maxZoom: 19,
|
maxNativeZoom: 19,
|
||||||
|
maxZoom: 20,
|
||||||
attribution: '© OpenStreetMap contributors'
|
attribution: '© OpenStreetMap contributors'
|
||||||
});
|
});
|
||||||
|
|
||||||
var satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
var satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||||
maxZoom: 19,
|
maxNativeZoom: 19,
|
||||||
|
maxZoom: 20,
|
||||||
attribution: 'Tiles © Esri'
|
attribution: 'Tiles © Esri'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -217,7 +221,6 @@ class DroneMap:
|
|||||||
currentLayer.addTo(map);
|
currentLayer.addTo(map);
|
||||||
var isSatellite = true;
|
var isSatellite = true;
|
||||||
|
|
||||||
|
|
||||||
// 地圖點擊事件
|
// 地圖點擊事件
|
||||||
map.on('click', function(e) {
|
map.on('click', function(e) {
|
||||||
if (selectionMode === 'polygon') {
|
if (selectionMode === 'polygon') {
|
||||||
@@ -349,6 +352,7 @@ class DroneMap:
|
|||||||
var socketColors = {};
|
var socketColors = {};
|
||||||
var droneGroupColors = {};
|
var droneGroupColors = {};
|
||||||
var focusedId = null;
|
var focusedId = null;
|
||||||
|
var missionFollowingEnabled = false;
|
||||||
var initialized = false;
|
var initialized = false;
|
||||||
var trajectoryGroup = L.layerGroup().addTo(map);
|
var trajectoryGroup = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
@@ -767,15 +771,34 @@ class DroneMap:
|
|||||||
|
|
||||||
function focusOn(id) {
|
function focusOn(id) {
|
||||||
if (!markers[id]) return;
|
if (!markers[id]) return;
|
||||||
focusedId = id;
|
// 非任務狀態只移動一次;任務執行中才記住焦點持續跟隨。
|
||||||
|
focusedId = missionFollowingEnabled ? id : null;
|
||||||
var latlng = markers[id].getLatLng();
|
var latlng = markers[id].getLatLng();
|
||||||
autoCenteringMap = true;
|
autoCenteringMap = true;
|
||||||
map.flyTo(latlng, map.getZoom());
|
map.flyTo(latlng, map.getZoom());
|
||||||
setTimeout(() => { autoCenteringMap = false; }, 500);
|
setTimeout(() => { autoCenteringMap = false; }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
function setMissionFollowingEnabled(enabled, preferredId) {
|
||||||
|
missionFollowingEnabled = Boolean(enabled);
|
||||||
|
if (!missionFollowingEnabled) {
|
||||||
|
// 非任務狀態不跟隨 UAV 或目標點,保留使用者手動地圖視角。
|
||||||
|
focusedId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任務已在執行時保留原有焦點;尚無焦點時跟隨指定 UAV。
|
||||||
if (focusedId && markers[focusedId]) {
|
if (focusedId && markers[focusedId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (preferredId && markers[preferredId]) {
|
||||||
|
focusOn(preferredId);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
if (missionFollowingEnabled && focusedId && markers[focusedId]) {
|
||||||
var latlng = markers[focusedId].getLatLng();
|
var latlng = markers[focusedId].getLatLng();
|
||||||
autoFollowPanTo(latlng);
|
autoFollowPanTo(latlng);
|
||||||
}
|
}
|
||||||
@@ -793,7 +816,7 @@ class DroneMap:
|
|||||||
.setLatLng([lat, lon])
|
.setLatLng([lat, lon])
|
||||||
.setRotationAngle(heading);
|
.setRotationAngle(heading);
|
||||||
idLabels[id].setLatLng([lat, lon]);
|
idLabels[id].setLatLng([lat, lon]);
|
||||||
if (id === focusedId) {
|
if (missionFollowingEnabled && id === focusedId) {
|
||||||
autoFollowPanTo(markers[id].getLatLng());
|
autoFollowPanTo(markers[id].getLatLng());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -813,7 +836,6 @@ class DroneMap:
|
|||||||
if (bridge) {
|
if (bridge) {
|
||||||
bridge.emitDroneClicked(id);
|
bridge.emitDroneClicked(id);
|
||||||
}
|
}
|
||||||
focusOn(id);
|
|
||||||
})
|
})
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
||||||
@@ -828,14 +850,23 @@ class DroneMap:
|
|||||||
if (bridge) {
|
if (bridge) {
|
||||||
bridge.emitDroneClicked(id);
|
bridge.emitDroneClicked(id);
|
||||||
}
|
}
|
||||||
focusOn(id);
|
|
||||||
})
|
})
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
||||||
if (!initialized || id < focusedId) {
|
if (!initialized) {
|
||||||
|
// 第一次收到 UAV 位置時只做一次初始置中,
|
||||||
|
// 非任務狀態不設 focusedId,所以後續不會跟隨。
|
||||||
|
if (missionFollowingEnabled) {
|
||||||
|
focusedId = id;
|
||||||
|
}
|
||||||
|
autoCenteringMap = true;
|
||||||
|
map.setView([lat, lon], 19);
|
||||||
|
setTimeout(() => { autoCenteringMap = false; }, 300);
|
||||||
|
initialized = true;
|
||||||
|
} else if (missionFollowingEnabled &&
|
||||||
|
(!focusedId || id < focusedId)) {
|
||||||
focusedId = id;
|
focusedId = id;
|
||||||
map.setView([lat, lon], 19);
|
map.setView([lat, lon], 19);
|
||||||
initialized = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -975,6 +1006,9 @@ class DroneMap:
|
|||||||
self.set_font_scale(self.font_scale)
|
self.set_font_scale(self.font_scale)
|
||||||
self.set_socket_colors(self.socket_colors)
|
self.set_socket_colors(self.socket_colors)
|
||||||
self.set_drone_group_colors(self.drone_group_colors)
|
self.set_drone_group_colors(self.drone_group_colors)
|
||||||
|
self.set_mission_following(
|
||||||
|
self.mission_following_enabled,
|
||||||
|
self.mission_following_drone_id)
|
||||||
else:
|
else:
|
||||||
_log("ERROR", "地圖載入失敗")
|
_log("ERROR", "地圖載入失敗")
|
||||||
|
|
||||||
@@ -1021,6 +1055,17 @@ class DroneMap:
|
|||||||
if self.map_loaded:
|
if self.map_loaded:
|
||||||
self.map_view.page().runJavaScript(f"focusOn('{drone_id}');")
|
self.map_view.page().runJavaScript(f"focusOn('{drone_id}');")
|
||||||
|
|
||||||
|
def set_mission_following(self, enabled, preferred_drone_id=None):
|
||||||
|
"""只在任務執行中啟用 UAV 自動跟隨。"""
|
||||||
|
self.mission_following_enabled = bool(enabled)
|
||||||
|
self.mission_following_drone_id = preferred_drone_id
|
||||||
|
if self.map_loaded:
|
||||||
|
preferred_json = json.dumps(preferred_drone_id)
|
||||||
|
self.map_view.page().runJavaScript(
|
||||||
|
"setMissionFollowingEnabled("
|
||||||
|
f"{str(self.mission_following_enabled).lower()}, "
|
||||||
|
f"{preferred_json});")
|
||||||
|
|
||||||
def set_font_scale(self, scale):
|
def set_font_scale(self, scale):
|
||||||
"""設定地圖 HTML 控制項的字體倍率。"""
|
"""設定地圖 HTML 控制項的字體倍率。"""
|
||||||
self.font_scale = scale
|
self.font_scale = scale
|
||||||
|
|||||||
Reference in New Issue
Block a user