master
ken910606 4 weeks ago
parent ebd219c88e
commit ed4e4dce10

@ -1318,6 +1318,7 @@ class ControlStationUI(QMainWindow):
"""切換 group tab — 只需切換 active group 並刷新 UI"""
if index < 0:
self.active_group_id = None
self._sync_map_follow_for_active_group()
return
tab_text = self.group_tab_widget.tabText(index)
@ -1345,6 +1346,13 @@ class ControlStationUI(QMainWindow):
group = self._get_active_group()
selected = group.selected_drone_ids if group else set()
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
for drone_id, panel in self.drones.items():
@ -1372,6 +1380,23 @@ class ControlStationUI(QMainWindow):
panel.update_status()
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):
"""同步每台 drone 的 group 顏色到地圖箭頭外框。"""
@ -1538,6 +1563,7 @@ class ControlStationUI(QMainWindow):
group.executor.hover_stable_sec = ep.get(
'hover_stable_sec', group.executor.hover_stable_sec)
group.executor.start(group.planned_waypoints)
self._sync_map_follow_for_active_group()
panel = self.group_panels.get(group_id)
if panel:
panel.update_status()
@ -1554,6 +1580,7 @@ class ControlStationUI(QMainWindow):
elif group.executor.state == MissionState.PAUSED:
group.executor.resume()
self.statusBar().showMessage(f"Group {group_id}: 任務已恢復", 3000)
self._sync_map_follow_for_active_group()
panel = self.group_panels.get(group_id)
if panel:
panel.update_status()
@ -1565,6 +1592,7 @@ class ControlStationUI(QMainWindow):
return
if group.executor:
group.executor.stop()
self._sync_map_follow_for_active_group()
group.planned_waypoints = None
group.mission_target = None
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):
"""群組任務完成回呼"""
self._sync_map_follow_for_active_group()
panel = self.group_panels.get(group_id)
if panel:
panel.update_status()
@ -2257,6 +2286,9 @@ class ControlStationUI(QMainWindow):
self.statusBar().showMessage("請先建立任務群組", 3000)
return
# 不論後續是否能進行任務規劃,都先記錄最新點擊的目標點。
group.mission_target = (float(lat), float(lon))
mode_map = {
'M_FORMATION': MissionType.M_FORMATION,
'CIRCLE_FORMATION': MissionType.CIRCLE_FORMATION,
@ -2265,9 +2297,6 @@ class ControlStationUI(QMainWindow):
if mission_type is None:
return # Grid Sweep / Leader-Follower 由各自的觸發方式處理
# XBee GOTO 直接使用該 Group 最近一次點擊的地圖座標,與文字欄位解耦。
group.mission_target = (float(lat), float(lon))
selected_drones = self._get_group_drones(group)
if len(selected_drones) == 0:
self.statusBar().showMessage(f"Group {group.group_id}: 請先分配無人機", 3000)

@ -21,6 +21,8 @@ class DroneMap:
self.font_scale = 1.0
self.socket_colors = {}
self.drone_group_colors = {}
self.mission_following_enabled = False
self.mission_following_drone_id = None
# 創建橋接對象
self.bridge = MapBridge()
@ -199,16 +201,18 @@ class DroneMap:
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', {
maxZoom: 19,
maxNativeZoom: 19,
maxZoom: 20,
attribution: '© OpenStreetMap contributors'
});
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'
});
@ -217,7 +221,6 @@ class DroneMap:
currentLayer.addTo(map);
var isSatellite = true;
// 地圖點擊事件
map.on('click', function(e) {
if (selectionMode === 'polygon') {
@ -349,6 +352,7 @@ class DroneMap:
var socketColors = {};
var droneGroupColors = {};
var focusedId = null;
var missionFollowingEnabled = false;
var initialized = false;
var trajectoryGroup = L.layerGroup().addTo(map);
@ -767,15 +771,34 @@ class DroneMap:
function focusOn(id) {
if (!markers[id]) return;
focusedId = id;
// 非任務狀態只移動一次任務執行中才記住焦點持續跟隨
focusedId = missionFollowingEnabled ? id : null;
var latlng = markers[id].getLatLng();
autoCenteringMap = true;
map.flyTo(latlng, map.getZoom());
setTimeout(() => { autoCenteringMap = false; }, 500);
}
setInterval(() => {
function setMissionFollowingEnabled(enabled, preferredId) {
missionFollowingEnabled = Boolean(enabled);
if (!missionFollowingEnabled) {
// 非任務狀態不跟隨 UAV 或目標點保留使用者手動地圖視角
focusedId = null;
return;
}
// 任務已在執行時保留原有焦點尚無焦點時跟隨指定 UAV
if (focusedId && markers[focusedId]) {
return;
}
if (preferredId && markers[preferredId]) {
focusOn(preferredId);
initialized = true;
}
}
setInterval(() => {
if (missionFollowingEnabled && focusedId && markers[focusedId]) {
var latlng = markers[focusedId].getLatLng();
autoFollowPanTo(latlng);
}
@ -793,7 +816,7 @@ class DroneMap:
.setLatLng([lat, lon])
.setRotationAngle(heading);
idLabels[id].setLatLng([lat, lon]);
if (id === focusedId) {
if (missionFollowingEnabled && id === focusedId) {
autoFollowPanTo(markers[id].getLatLng());
}
} else {
@ -813,7 +836,6 @@ class DroneMap:
if (bridge) {
bridge.emitDroneClicked(id);
}
focusOn(id);
})
.addTo(map);
@ -828,14 +850,23 @@ class DroneMap:
if (bridge) {
bridge.emitDroneClicked(id);
}
focusOn(id);
})
.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;
map.setView([lat, lon], 19);
}
}
}
@ -975,6 +1006,9 @@ class DroneMap:
self.set_font_scale(self.font_scale)
self.set_socket_colors(self.socket_colors)
self.set_drone_group_colors(self.drone_group_colors)
self.set_mission_following(
self.mission_following_enabled,
self.mission_following_drone_id)
else:
_log("ERROR", "地圖載入失敗")
@ -1021,6 +1055,17 @@ class DroneMap:
if self.map_loaded:
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):
"""設定地圖 HTML 控制項的字體倍率。"""
self.font_scale = scale

Loading…
Cancel
Save