1.优化了无人的轨迹的绘制的过程中的 飞机不动路线自己动的不和谐现象,实现了飞机实时根据推送的数据绘制珍格格飞机任务的过程,同时也优化了地图加载慢 白底色的问题。

2.完善了遗留一些页面的多语言支持的。
This commit is contained in:
2026-07-03 13:39:18 +08:00
parent 0db64a96e7
commit 2124f3bc02

View File

@@ -19,6 +19,14 @@ import '../../../../../../core/managers/drone_task_state_manager.dart';
import '../../../../../../core/network/mqtt/domain/entities/drone_osd_entity.dart';
import '../../../../../../core/network/mqtt/data/datasources/drone_osd_datasource.dart';
/// 模拟轨迹模式枚举
enum SimulationMode {
circle, // 圆形路径
polygon, // 多边形路径(六边形)
bow, // 弓字形路径(Zigzag)
rectangle, // 矩形路径
}
/// 可拖动的悬浮按钮组件
/// 点击弹出底部弹窗显示设备状态详情
class FloatBarWidget extends StatefulWidget {
@@ -69,6 +77,9 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
MapController? _mapController;
bool _isUserDragging = false;
/// 用来同步刷新弹窗内部的 State
void Function(void Function())? _bottomSheetSetState;
@override
void initState() {
@@ -89,6 +100,7 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
_droneOsdDataSource?.dispose();
_mapController?.dispose();
_destroyRtcEngine();
_simulationTimer?.cancel();
droneTaskStateManager.currentTaskInfo.removeListener(_onTaskInfoChanged);
super.dispose();
}
@@ -159,7 +171,10 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
/// 初始化无人机相关组件
void _initDroneComponents() {
if (_droneTaskInfo == null) return;
if (_droneTaskInfo == null) {
debugPrint('❌ [FloatBarWidget] _droneTaskInfo 为空,无法初始化');
return;
}
debugPrint('🛸 [FloatBarWidget] 初始化无人机组件');
debugPrint(' droneSn: ${_droneTaskInfo!.droneSn}');
@@ -174,30 +189,34 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
// 初始化地图控制器
_mapController = MapController();
// 立即设置初始位置,确保飞机能立即显示
setState(() {
_currentPosition = const LatLng(39.9042, 116.4074);
_currentHeading = 90;
_trajectoryPoints = [const LatLng(39.9042, 116.4074)];
});
// 启动模拟轨迹数据(用于测试)
_startSimulationTrajectory();
debugPrint('✅ [FloatBarWidget] 地图控制器已初始化');
// 只有在线时才启动 OSD 监听和视频流加载
if (_droneTaskInfo!.isOnline) {
debugPrint('🟢 [FloatBarWidget] 无人机在线,启动完整功能');
// 初始化 OSD 数据源
_droneOsdDataSource = sl<DroneOsdDataSource>();
_startOsdListening();
// 加载视频流
_loadVideoStream();
// 🔥 启动模拟轨迹用于测试(可以选择不同模式)
// 模式选项: circle(圆形)、polygon(六边形)、bow(弓字形)、rectangle(矩形)
// 等天气好了,注释掉下面这行即可使用真实 MQTT 数据
debugPrint('🚀 [FloatBarWidget] 准备启动模拟轨迹...');
_startSimulationTrajectory(mode: SimulationMode.circle, pointCount: 30);
} else {
debugPrint('⚠️ [FloatBarWidget] 无人机离线,不启动 OSD 监听和视频流');
debugPrint('🚀 [FloatBarWidget] 但仍然启动模拟轨迹用于测试...');
setState(() {
_videoError = '无人机离线,无视频信号';
});
// 离线时也启动模拟轨迹,方便测试
_startSimulationTrajectory(mode: SimulationMode.circle, pointCount: 30);
}
}
@@ -225,129 +244,311 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
/// 处理 OSD 数据更新
void _handleOsdUpdate(DroneOsdEntity osd) {
// ⚠️ 调试期间:忽略 MQTT 数据,只看模拟轨迹
if (_simulationTimer != null && _simulationTimer!.isActive) {
debugPrint('⚠️ [FloatBarWidget] 模拟轨迹运行中,忽略 MQTT 数据');
return;
}
final rawData = osd.rawData;
// 获取经纬度
double? latitude = (rawData['latitude'] as num?)?.toDouble();
double? longitude = (rawData['longitude'] as num?)?.toDouble();
double? heading = (rawData['heading'] as num?)?.toDouble();
debugPrint('📍 [FloatBarWidget] 收到 OSD 数据: ${rawData.keys.join(', ')}');
if (latitude != null &&
longitude != null &&
latitude != 0 &&
longitude != 0) {
// 1. 解析基础数据
double? lat = (rawData['latitude'] as num?)?.toDouble() ??
(rawData['lat'] as num?)?.toDouble();
double? lng = (rawData['longitude'] as num?)?.toDouble() ??
(rawData['lng'] as num?)?.toDouble() ??
(rawData['lon'] as num?)?.toDouble();
double? heading = (rawData['heading'] as num?)?.toDouble() ??
(rawData['yaw'] as num?)?.toDouble();
if (lat != null && lng != null && lat != 0 && lng != 0) {
final newPos = LatLng(lat, lng);
setState(() {
_currentPosition = LatLng(latitude, longitude);
_currentHeading = heading;
// 计算航向:如果 MQTT 没给角度,我们根据上一个点自动算一个方向
if (heading != null) {
_currentHeading = heading;
} else if (_trajectoryPoints.isNotEmpty) {
final last = _trajectoryPoints.last;
// 使用 atan2 计算方位角
_currentHeading = atan2(lng - last.longitude, lat - last.latitude) * 180 / pi;
}
// 添加到轨迹点
if (_currentPosition != null) {
_trajectoryPoints.add(_currentPosition!);
// 限制轨迹点数量,避免内存问题
if (_trajectoryPoints.length > 500) {
_trajectoryPoints.removeAt(0);
// 更新当前位置(驱动飞机 Marker)
_currentPosition = newPos;
// 轨迹“拉烟”逻辑:距离过滤(防止 GPS 抖动导致轨迹像乱麻)
if (_trajectoryPoints.isEmpty) {
_trajectoryPoints.add(newPos);
} else {
final distance = _calculateDistance(
_trajectoryPoints.last.latitude,
_trajectoryPoints.last.longitude,
lat,
lng,
);
// 只有移动超过 1.5 米才画线,否则认为是原地漂移
if (distance > 1.5) {
_trajectoryPoints.add(newPos);
// 性能优化:只保留最近 1000 个点
if (_trajectoryPoints.length > 1000) _trajectoryPoints.removeAt(0);
}
}
});
// 自动移动地图中心
if (_mapController != null && _currentPosition != null) {
_mapController?.move(_currentPosition!, 17);
// 2. 地图跟随:如果用户在手动拖地图,就不要强制跳回中心
if (!_isUserDragging && _mapController != null) {
_mapController?.move(newPos, _mapController!.camera.zoom);
}
} else {
debugPrint('⚠️ [FloatBarWidget] 位置数据无效: lat=$lat, lng=$lng');
}
}
/// 模拟轨迹数据(用于测试)
/// 简化为三个固定点,飞机在这三个点之间缓慢移动
void _startSimulationTrajectory() {
debugPrint('🎯 [FloatBarWidget] 启动模拟轨迹数据(三点模式)');
/// 计算两点之间的距离(米)- Haversine 公式
double _calculateDistance(double lat1, double lon1, double lat2, double lon2) {
const double earthRadius = 6371000; // 地球半径(米)
final dLat = _toRadians(lat2 - lat1);
final dLon = _toRadians(lon2 - lon1);
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(_toRadians(lat1)) * cos(_toRadians(lat2)) *
sin(dLon / 2) * sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
return earthRadius * c;
}
double _toRadians(double degrees) {
return degrees * pi / 180;
}
/// 启动智能模拟轨迹(支持多种路径模式)
void _startSimulationTrajectory({
SimulationMode mode = SimulationMode.circle,
int pointCount = 20,
}) {
debugPrint('🎯 [FloatBarWidget] ========== 开始启动模拟轨迹 ==========');
debugPrint(' 模式: $mode');
debugPrint(' 点数: $pointCount');
debugPrint(' mounted: $mounted');
// 如果已经有定时器在运行,先取消
if (_simulationTimer != null && _simulationTimer!.isActive) {
debugPrint('⚠️ [FloatBarWidget] 检测到已有定时器,先取消');
_simulationTimer!.cancel();
}
// 三个点间距拉大,形成明显的三角形路径
final waypoints = [
const LatLng(39.9042, 116.4074), // 起点
const LatLng(39.9060, 116.4110), // 东北方向
const LatLng(39.9020, 116.4120), // 东南方向
];
// 生成路径点集
final waypoints = _generateWaypoints(mode, pointCount);
debugPrint(' 生成的路径点数: ${waypoints.length}');
if (waypoints.isEmpty) {
debugPrint('❌ [FloatBarWidget] 路径点生成失败');
return;
}
debugPrint(' 第一个点: ${waypoints[0]}');
int currentWaypoint = 0;
int nextWaypoint = 1;
int currentIndex = 0;
double progress = 0;
const double speed = 0.02; // 每步移动2%
const double speed = 0.05; // 每步移动5%,更快看到效果
// 初始化位置
debugPrint('📍 [FloatBarWidget] 设置初始位置...');
setState(() {
_currentPosition = waypoints[0];
_currentHeading = 90;
_trajectoryPoints = [waypoints[0]];
_currentPosition = waypoints[0]; // 从起点开始
_trajectoryPoints = [waypoints[0]]; // 轨迹从起点开始
debugPrint(' ✅ 起始位置: $_currentPosition');
debugPrint(' ✅ 轨迹点数: ${_trajectoryPoints.length}');
});
// 使用定时器模拟缓慢移动(500ms间隔,肉眼可见移动)
_simulationTimer = Timer.periodic(const Duration(milliseconds: 500), (
timer,
) {
// 首次定位到起始区域
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_mapController != null) {
debugPrint('🗺️ [FloatBarWidget] 移动地图到起始点...');
_mapController!.move(waypoints[0], 16);
debugPrint(' ✅ 地图已移动');
} else {
debugPrint('⚠️ [FloatBarWidget] 地图控制器为空,无法移动');
}
});
debugPrint('⏱️ [FloatBarWidget] 启动定时器(300ms间隔)...');
debugPrint(' 📊 当前 mounted: $mounted');
debugPrint(' 📊 当前 _currentPosition: $_currentPosition');
// 使用定时器模拟飞行(300ms间隔,流畅移动)
_simulationTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) {
debugPrint('🔥 [定时器触发] mounted=$mounted, currentIndex=$currentIndex, progress=${progress.toStringAsFixed(2)}');
if (!mounted) {
debugPrint('⚠️ [FloatBarWidget] Widget 未挂载,取消定时器');
timer.cancel();
return;
}
// 首次触发时定位地图到起始区域
if (progress == speed && _mapController != null) {
try {
_mapController!.move(waypoints[0], 16);
} catch (_) {}
}
final start = waypoints[currentIndex];
final nextIndex = (currentIndex + 1) % waypoints.length;
final end = waypoints[nextIndex];
// 计算当前点到下一个点的移动
final start = waypoints[currentWaypoint];
final end = waypoints[nextWaypoint];
// 线性插值
// 线性插值 - 计算轨迹的"前端"位置
progress += speed;
if (progress >= 1.0) {
progress = 0;
currentWaypoint = nextWaypoint;
nextWaypoint = (currentWaypoint + 1) % waypoints.length;
currentIndex = nextIndex;
// 到达起点时继续循环(不要停!)
if (currentIndex == 0) {
debugPrint('✅ [FloatBarWidget] 🎉 完成一圈,继续飞行...');
// timer.cancel(); // 不要停止,继续飞
// return; // 不要返回
}
}
// 计算当前位置
final lat = start.latitude + (end.latitude - start.latitude) * progress;
final lng =
start.longitude + (end.longitude - start.longitude) * progress;
// 计算轨迹前端位置(水流到哪里了)
final trailLat = start.latitude + (end.latitude - start.latitude) * progress;
final trailLng = start.longitude + (end.longitude - start.longitude) * progress;
final trailPosition = LatLng(trailLat, trailLng);
// 计算航向角
final dx = end.longitude - start.longitude;
final dy = end.latitude - start.latitude;
final heading = atan2(dy, dx) * 180 / pi;
// 核心改进:计算飞机的航向角(让头朝向前进方向)
final angle = atan2(end.longitude - start.longitude, end.latitude - start.latitude) * 180 / pi;
setState(() {
_currentPosition = LatLng(lat, lng);
_currentHeading = heading;
// 1. 刷新悬浮条自身(如果需要)
if (mounted) {
setState(() {
_currentPosition = trailPosition;
_currentHeading = angle; // 赋予飞机灵魂
// 轨迹产生(拉烟)
_trajectoryPoints.add(trailPosition);
if (_trajectoryPoints.length > 800) _trajectoryPoints.removeAt(0);
debugPrint(' 🔄 setState更新: 飞机=$_currentPosition, 航向=${angle.toStringAsFixed(1)}°, 轨迹点=${_trajectoryPoints.length}');
});
}
// 添加轨迹点
if (_currentPosition != null) {
_trajectoryPoints.add(_currentPosition!);
if (_trajectoryPoints.length > 500) {
_trajectoryPoints.removeAt(0);
}
}
// 2. 🔥 核心:刷新已经打开的弹窗内部
_bottomSheetSetState?.call(() {
// 弹窗内部组件会重新执行 build,读取到最新的 _currentPosition 和 _currentHeading
debugPrint(' 📱 弹窗已刷新');
});
// 移动地图中心到无人机当前位置
if (_currentPosition != null) {
_mapController?.move(_currentPosition!, 16);
// 3. 移动地图中心
if (!_isUserDragging) {
_mapController?.move(trailPosition, 16);
}
debugPrint(
'📍 模拟位置: lat=${lat.toStringAsFixed(6)}, lng=${lng.toStringAsFixed(6)}, heading=${heading.toStringAsFixed(1)}, progress=${progress.toStringAsFixed(2)}',
);
if (progress < 0.1 || progress > 0.9) {
debugPrint(
'✈️ 飞机飞行: 进度=${(progress * 100).toStringAsFixed(0)}%, '
'轨迹点=${_trajectoryPoints.length}, '
'位置=(${trailLat.toStringAsFixed(4)}, ${trailLng.toStringAsFixed(4)})',
);
}
});
debugPrint(' 模拟定时器已启动,三点循环模式(500ms间隔)');
debugPrint(' ✅ 模拟定时器已启动');
debugPrint(' ✈️ 飞机将沿路径飞行(像拉烟画画)');
debugPrint(' 🛤️ 轨迹将从飞机尾部产生');
debugPrint('🎯 [FloatBarWidget] ========== 模拟轨迹启动完成 ==========');
}
/// 生成不同类型的路径点集
List<LatLng> _generateWaypoints(SimulationMode mode, int count) {
const centerLat = 39.9042;
const centerLng = 116.4074;
switch (mode) {
case SimulationMode.circle:
return _generateCirclePath(centerLat, centerLng, count);
case SimulationMode.polygon:
return _generatePolygonPath(centerLat, centerLng, count);
case SimulationMode.bow:
return _generateBowPath(centerLat, centerLng, count);
case SimulationMode.rectangle:
return _generateRectanglePath(centerLat, centerLng, count);
}
}
/// 生成圆形路径
List<LatLng> _generateCirclePath(double centerLat, double centerLng, int points) {
final List<LatLng> path = [];
const double radius = 0.002; // 约200米半径
const double angleStep = (2 * pi) / 30; // 30个点形成圆
for (int i = 0; i < 30; i++) {
final angle = i * angleStep;
final lat = centerLat + radius * cos(angle);
final lng = centerLng + radius * sin(angle);
path.add(LatLng(lat, lng));
}
path.add(path[0]); // 闭合圆形
debugPrint('🔵 生成圆形路径: ${path.length}个点');
return path;
}
/// 生成多边形路径(六边形)
List<LatLng> _generatePolygonPath(double centerLat, double centerLng, int points) {
final List<LatLng> path = [];
const double radius = 0.0025; // 约250米
const int sides = 6; // 六边形
const double angleStep = (2 * pi) / sides;
for (int i = 0; i <= sides; i++) {
final angle = i * angleStep - pi / 2; // 从顶部开始
final lat = centerLat + radius * cos(angle);
final lng = centerLng + radius * sin(angle);
path.add(LatLng(lat, lng));
}
debugPrint('⬡ 生成六边形路径: ${path.length}个点');
return path;
}
/// 生成弓字形路径(Zigzag)
List<LatLng> _generateBowPath(double centerLat, double centerLng, int points) {
final List<LatLng> path = [];
const double width = 0.004; // 宽度约400米
const double height = 0.003; // 高度约300米
const int rows = 5; // 5行
for (int row = 0; row <= rows; row++) {
final lat = centerLat - height / 2 + (height / rows) * row;
// 偶数行从左到右,奇数行从右到左
if (row % 2 == 0) {
path.add(LatLng(lat, centerLng - width / 2));
path.add(LatLng(lat, centerLng + width / 2));
} else {
path.add(LatLng(lat, centerLng + width / 2));
path.add(LatLng(lat, centerLng - width / 2));
}
}
debugPrint('〰️ 生成弓字形路径: ${path.length}个点, $rows行');
return path;
}
/// 生成矩形路径
List<LatLng> _generateRectanglePath(double centerLat, double centerLng, int points) {
final List<LatLng> path = [];
const double width = 0.004; // 宽约400米
const double height = 0.003; // 高约300米
// 四个角点(顺时针)
path.add(LatLng(centerLat + height / 2, centerLng - width / 2)); // 左上
path.add(LatLng(centerLat + height / 2, centerLng + width / 2)); // 右上
path.add(LatLng(centerLat - height / 2, centerLng + width / 2)); // 右下
path.add(LatLng(centerLat - height / 2, centerLng - width / 2)); // 左下
path.add(path[0]); // 闭合
debugPrint('▭ 生成矩形路径: ${path.length}个点');
return path;
}
/// 加载视频流
@@ -492,8 +693,13 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.85,
),
builder: (ctx) => _buildBottomSheetContent(status),
);
builder: (ctx) => StatefulBuilder( // 🔥 关键:使用 StatefulBuilder
builder: (context, setSheetState) {
_bottomSheetSetState = setSheetState; // 捕获弹窗内部的刷新函数
return _buildBottomSheetContent(status);
},
),
).then((_) => _bottomSheetSetState = null); // 弹窗关闭时置空,防止内存泄漏
}
/// 构建底部弹窗内容
@@ -1308,33 +1514,18 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
MarkerLayer(
markers: [
Marker(
key: const ValueKey('drone_current_marker'),
key: const ValueKey('drone_marker'), // 加上 Key 确保状态刷新
point: _currentPosition!,
width: 60,
height: 60,
width: 40,
height: 40,
child: Transform.rotate(
angle: (_currentHeading ?? 0) * pi / 180,
// 注意:Icons.flight 默认头朝上(0度),如果你的图标偏了,在这里加个 pi/2 之类的偏移
angle: (_currentHeading ?? 0) * pi / 180,
alignment: Alignment.center,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(2, 2),
),
],
),
child: const Padding(
padding: EdgeInsets.all(8),
child: Icon(
Icons.flight,
color: Color(0xFF165DFF),
size: 40,
),
),
child: const Icon(
Icons.flight,
color: Color(0xFF165DFF),
size: 32,
),
),
),