Merge remote-tracking branch 'origin/feature/my' into feature/my

This commit is contained in:
2026-03-09 13:10:40 +08:00
4 changed files with 347 additions and 185 deletions

View File

@@ -22,7 +22,6 @@ class PathHttpDatasourceImpl implements PathHttpDatasource {
// 2. 格式化打印JSON(带缩进,清晰展示嵌套结构)
final jsonString = const JsonEncoder.withIndent(' ').convert(body);
print('完整JSON请求体:\n$jsonString');
final _jsonString = jsonEncode(body);
print('完整JSON请求体:\n$jsonString');

View File

@@ -18,6 +18,9 @@ import '../../../devices/presentation/bloc/devices_cubit.dart';
import '../../../devices/presentation/bloc/device_status_bloc.dart';
import '../../../devices/presentation/bloc/device_status_state.dart';
// 配置:数据超时时间(5秒)
const int DATA_TIMEOUT_SECONDS = 5;
class RunningStatusPage extends StatefulWidget {
const RunningStatusPage({super.key});
@@ -48,6 +51,41 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
double _timeIndex = 0;
// 新增:超时检测相关变量
Timer? _dataTimeoutTimer;
bool _isDataTimeout = false; // 标记是否数据超时
@override
void initState() {
super.initState();
// 初始化超时计时器
_startDataTimeoutTimer();
}
@override
void dispose() {
// 销毁计时器,防止内存泄漏
_dataTimeoutTimer?.cancel();
super.dispose();
}
// 启动/重置数据超时计时器
void _startDataTimeoutTimer() {
// 先取消现有计时器
_dataTimeoutTimer?.cancel();
// 启动新计时器:超过指定时间未收到数据则标记为超时
_dataTimeoutTimer = Timer(Duration(seconds: DATA_TIMEOUT_SECONDS), () {
if (mounted) {
setState(() {
_isDataTimeout = true;
// 超时后清空历史图表数据(可选)
_resetChartData();
});
}
});
}
// 核心修复:数据点X轴坐标强制映射为0-5(对应6个标签),保证一一对应
List<FlSpot> _getMappedSpots(List<FlSpot> data) {
final List<FlSpot> mapped = [];
@@ -74,6 +112,11 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
if (currentDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("数据刷新成功!"), duration: Duration(seconds: 1)));
// 刷新后重置超时状态
if (_isDataTimeout) {
setState(() => _isDataTimeout = false);
}
_startDataTimeoutTimer();
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("暂无设备,无法刷新"), duration: Duration(seconds: 1)));
}
@@ -375,6 +418,11 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
// ====================== 图表视图 ======================
Widget _buildChartContentView(DeviceStatusState state) {
// 新增:如果数据超时,直接显示loading
if (_isDataTimeout) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF1677FF), strokeWidth: 4));
}
if (state is! DeviceStatusUpdated) {
return const Center(
child: CircularProgressIndicator(
@@ -602,6 +650,11 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
// ====================== 卡片视图 ======================
Widget _buildCardContentView(DeviceStatusState state) {
// 新增:如果数据超时,直接显示loading
if (_isDataTimeout) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF1677FF), strokeWidth: 4));
}
if (state is DeviceStatusUpdated) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
@@ -766,11 +819,12 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
String qual = '--';
String satelliteCnt = '--';
String headingStatus = "--";
// 新增:超时状态下显示--
String qual = _isDataTimeout ? '--' : '--';
String satelliteCnt = _isDataTimeout ? '--' : '--';
String headingStatus = _isDataTimeout ? "--" : "--";
if (state is DeviceStatusUpdated) {
if (!_isDataTimeout && state is DeviceStatusUpdated) {
headingStatus = state.status.headingStatus == 0 ? '未初始化' : '已初始化';
int qualValue = 0;
try {
@@ -780,7 +834,13 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
}
qual = LocationUtils.parseLocationQuality(qualValue);
satelliteCnt = state.status.satelliteCnt.toString();
} else if (state is DeviceStatusError) {
// 收到新数据,重置超时计时器和状态
_startDataTimeoutTimer();
if (_isDataTimeout) {
setState(() => _isDataTimeout = false);
}
} else if (!_isDataTimeout && state is DeviceStatusError) {
qual = '-';
satelliteCnt = '-';
headingStatus = '-';
@@ -873,7 +933,7 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
});
}
if (state is DeviceStatusUpdated) {
if (!_isDataTimeout && state is DeviceStatusUpdated) {
debugPrint('📈 [UI] 检测到 Updated 状态,准备追加图表数据');
_appendChartData(state);
}
@@ -886,7 +946,21 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
);
}
void _resetChartData() {}
// 新增:清空图表历史数据
void _resetChartData() {
_leftMeasureHistory.clear();
_rightMeasureHistory.clear();
_leftTargetHistory.clear();
_rightTargetHistory.clear();
_leftCurrentHistory.clear();
_rightCurrentHistory.clear();
_leftTempHistory.clear();
_rightTempHistory.clear();
_knifeHistory.clear();
_chipTempHistory.clear();
_voltageHistory.clear();
_timeIndex = 0;
}
}
// ====================== 带动画的圆形仪表盘 Widget ======================

View File

@@ -70,7 +70,6 @@ class MapPageEnterprise extends StatefulWidget {
class _MapPageEnterpriseState extends State<MapPageEnterprise> {
final _dispatcher = sl<NetMessageDispatcher>();
late StreamSubscription<RawPacket> _sub;
bool _isRefreshing = false; // 新增:页面刷新状态标志
bool _isVideoDialogOpen = false; // 控制视频弹窗显示
String _videoStreamUrl = "";
@@ -88,9 +87,9 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
WorkStatus _workStatus = WorkStatus.idle;
bool isStartWork = false; //是否开始作业
LatLng? _currentLatLng;
StreamSubscription<Position>? _positionSub;
bool _hasMovedOnce = false;
LatLng? _currentLatLng; //gc格式
LatLng? _currentWgsLatLng;
bool _isPanelOpen = false; // 控制面板显示/隐藏
bool _directionBoxOpen = false; //航线面板显示/隐藏
bool _saveBoxOpen = false; //保存按钮显示/隐藏
@@ -101,7 +100,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 新增:控制底部作业面板显示
bool _isWorkPanelOpen = false;
bool _isWorkAreaCompleted = false;
RobotMode _currentRobotMode = RobotMode.point; //打点模式
RobotMode _currentRobotMode = RobotMode.robot; //打点模式
AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域
WorkMode? _currentWorkMode = WorkMode.bow; // 当前作业模式
// 新增:存储转换后的path和outer坐标
@@ -112,14 +111,18 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
List<PlotPoint>? gctracePoint = [];
// ========== 核心新增状态 ==========
final List<LatLng> _markedPoints = []; // 存储所有打点坐标
final List<LatLng> _markedPoints = []; // 存储所有s十字准星打点坐标
List<LatLng> _robotModeWgsPoints = []; // Robot模式下从设备获取的wgs打点
List<LatLng> _robotModeObsWgsPoints = []; // Robot模式下从设备获取的wgs打点 障碍物
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
double _headingAngle = 0.0; // 当前机器航向角(单位:度)
List<LatLng>? _currentTracePoints; //收到机器的点坐标
List typedPathList = <work_area_model.DeviceAddPathPointModel>[]; // 存储生成路径的坐标列表(已转换为LatLng)
// ========== 障碍物模式核心状态 ==========
List<List<LatLng>> _obstacleHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个空洞)
List<List<LatLng>> _obstacleHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个障碍物)
List<List<LatLng>> _obstacleWgsHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个障碍物)
List<LatLng> _currentObstaclePoints = []; // 当前正在绘制的障碍物打点
bool _isObstacleEditing = false; // 是否处于障碍物编辑状态
@@ -180,14 +183,21 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 计算边界
final bounds = calculateBounds(points);
if (bounds == null) return;
final currentZoom = _mapController.camera?.zoom ?? 18.0;
// 移动地图到边界中心(自动适配缩放级别)
_mapController.fitBounds(
bounds,
options: FitBoundsOptions(
padding: const EdgeInsets.all(50), // 边缘留白(避免内容贴边)
//maxZoom: 18, // 最大缩放级别(防止过度放大)
),
//_mapController.fitBounds(
// bounds,
// options: FitBoundsOptions(
// padding: const EdgeInsets.all(50), // 边缘留白(避免内容贴边)
// //maxZoom: 18, // 最大缩放级别(防止过度放大)
// ),
//);
_mapController.move(
bounds.center, // 边界中心点
currentZoom, // 保留当前缩放比例
);
print('地图已移动到绘制内容中心,边界范围:$bounds');
@@ -204,6 +214,17 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
return null;
}
bool __isValidLatLng(LatLng? latLng) {
if (latLng == null) return false;
// 排除赤道0,0坐标,同时校验经纬度范围(避免非法值)
return latLng.latitude != 0.0 &&
latLng.longitude != 0.0 &&
latLng.latitude >= -90 &&
latLng.latitude <= 90 &&
latLng.longitude >= -180 &&
latLng.longitude <= 180;
}
bool _isValidLatLng(double lat, double lng) {
return lat != 0 && lng != 0 && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
}
@@ -507,69 +528,62 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
setState(() {
// 1. 作业区域模式
if (_currentAreaMode == AreaMode.work) {
if (_markedPoints.isNotEmpty) {
if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isNotEmpty && _markedPoints.isNotEmpty) {
// Robot模式撤回
_robotModeWgsPoints.removeLast();
_markedPoints.removeLast();
// 作业点撤回后,若路径已生成,重新生成路径(保持同步)
if (_isWorkAreaCompleted && _markedPoints.length >= 3) {
_generatePath(showTips: false);
}
ToastUtils.showInfo(context, '已撤回作业区域最后一个点,剩余${_markedPoints.length}个点');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回作业区域最后一个点,剩余${_markedPoints.length}个点')));
ToastUtils.showInfo(context, '已撤回Robot模式最后一个点,剩余${_robotModeWgsPoints.length}个点');
} else {
ToastUtils.showInfo(context, '暂无作业区域点可撤回');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可撤回')));
}
return;
}
// 2. 空洞模式(完成后优先撤回已完成的空洞)
// 2. 障碍物模式(逻辑不变,Robot模式下障碍物打点和Point模式一致)
if (_currentAreaMode == AreaMode.obstacle) {
if (_currentObstaclePoints.isNotEmpty) {
_currentObstaclePoints.removeLast();
// 如果当前空洞的点撤空了,退出编辑状态
_robotModeObsWgsPoints.removeLast();
if (_currentObstaclePoints.isEmpty) {
_isObstacleEditing = false;
}
ToastUtils.showInfo(context, '已撤回当前空洞最后一个点,剩余${_currentObstaclePoints.length}个点');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回当前空洞最后1个点,剩余${_currentObstaclePoints.length}个点')));
return; // 撤完点就结束,不执行后面的逻辑
}
// 第二步:当前空洞无点可撤 → 再撤回「上一个完整空洞」
if (_obstacleHoles.isNotEmpty) {
_obstacleHoles.removeLast();
// 撤完空洞后重新生成路径(排除该空洞)
if (_isWorkAreaCompleted) {
_generatePath(showTips: false);
}
ToastUtils.showInfo(context, '已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞')));
ToastUtils.showInfo(context, '已撤回当前障碍物最后一个点,剩余${_currentObstaclePoints.length}个点');
return;
}
// 第三步:无任何可撤内容
ToastUtils.showInfo(context, '暂无空洞可撤回');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无空洞可撤回')));
if (_obstacleHoles.isNotEmpty) {
_obstacleHoles.removeLast();
if (_isWorkAreaCompleted) {
_generatePath(showTips: false);
}
ToastUtils.showInfo(context, '已撤回上一个完整障碍物,剩余${_obstacleHoles.length}个障碍物');
return;
}
ToastUtils.showInfo(context, '暂无障碍物可撤回');
}
});
}
// 兼容原有调用(可保留,也可直接用上面的_unifiedUndo)
void _undoLastPoint() => _undoAction();
void _undoObstaclePoint() => _undoAction();
// ========== 统一删除方法(适配完成后状态) ==========
void _deleteAllAction() {
setState(() {
// 1. 作业区域模式:清空作业点 + 重置路径
if (_currentAreaMode == AreaMode.work) {
if (_markedPoints.isEmpty) {
ToastUtils.showInfo(context, '暂无作业区域点可删除');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可删除')));
return;
if (_currentRobotMode == RobotMode.robot) {
_robotModeWgsPoints.clear();
_markedPoints.clear();
ToastUtils.showSuccess(context, '已清空所有作业点');
} else {
// 原有Point模式清空
if (_markedPoints.isEmpty) {
ToastUtils.showInfo(context, '暂无作业区域点可删除');
return;
}
_markedPoints.clear();
}
_markedPoints.clear();
gcjPathPoints.clear();
@@ -581,28 +595,25 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已清空所有作业区域点和路径')));
}
// 2. 空洞模式:清空所有空洞 + 恢复UI + 重新生成路径
// 2. 障碍物模式:清空所有障碍物 + 恢复UI + 重新生成路径
else if (_currentAreaMode == AreaMode.obstacle) {
if (_obstacleHoles.isEmpty && _currentObstaclePoints.isEmpty) {
ToastUtils.showInfo(context, '暂无空洞可删除');
ToastUtils.showInfo(context, '暂无障碍物可删除');
return;
}
_obstacleHoles.clear();
_currentObstaclePoints.clear();
_robotModeObsWgsPoints.clear();
_isObstacleEditing = false;
// 清空后重新生成路径(无空洞)
// 清空后重新生成路径(无障碍物)
if (_isWorkAreaCompleted) {
_generatePath(showTips: false);
}
ToastUtils.showSuccess(context, '已清空所有空洞');
ToastUtils.showSuccess(context, '已清空所有障碍物');
}
});
}
// 兼容原有调用
void _deleteAllPoints() => _deleteAllAction();
void _clearAllObstacles() => _deleteAllAction();
// ========== 核心:清空所有打点和轨迹 ==========
//void _deleteAllPoints() {
// if (_markedPoints.isNotEmpty) {
@@ -623,38 +634,31 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
Future<void> _initLocationEnterprise() async {
// 1. 设置轨迹模式为 LOCATION(核心:确保模式正确)
_traceManager.setMode(TPMode.LOCATION);
// 2. 清空旧的定位监听(如果有)
await _positionSub?.cancel();
_positionSub = null;
// 3. 初始化默认坐标(可选,避免地图空白)
setState(() {
_currentLatLng = const LatLng(39.9042, 116.4074); // 北京默认坐标
});
}
// ========== 新增:页面刷新初始化方法 ==========
Future<void> _handleRefresh() async {
// 防止重复刷新
if (_isRefreshing) return;
// 🔥 保存当前定位(刷新前先记录)
final savedCurrentLatLng = _currentLatLng;
final savedCurrentWgsLatLng = _currentWgsLatLng;
final savedMapCenter = _mapCenter;
setState(() {
_isRefreshing = true; // 标记开始刷新
});
try {
// 1. 取消旧的定位监听
await _positionSub?.cancel();
// 2. 重置所有页面状态(恢复初始值)
setState(() {
_traceManager.setMode(TPMode.LOCATION);
// 清空打点和轨迹数据
_markedPoints.clear();
gcjPathPoints.clear();
_robotModeWgsPoints.clear();
_robotModeObsWgsPoints.clear();
gcjOuterPoints.clear();
_currentTracePoints = null;
_isWorkAreaCompleted = false;
_saveBoxOpen = false;
@@ -680,9 +684,9 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 重置参数
_workDistance = 0.0;
_angle = 0.0;
_mapCenter = const LatLng(39.9042, 116.4074);
_hasMovedOnce = false;
_currentLatLng = null;
_currentLatLng = savedCurrentLatLng;
_currentWgsLatLng = savedCurrentWgsLatLng;
_mapCenter = savedMapCenter ?? _currentLatLng ?? const LatLng(39.9042, 116.4074);
});
// 3. 重新初始化定位
@@ -692,13 +696,16 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
_traceManager.reset();
// 5. 重新加载作业记录(保持数据最新)
final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
if (userId.isNotEmpty) {
await context.read<DevicesCubit>().loadWorkRecords(userId);
}
// final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
// if (userId.isNotEmpty) {
// await context.read<DevicesCubit>().loadWorkRecords(userId);
// }
// 6. 重置地图视角到初始位置
_mapController.move(const LatLng(39.9042, 116.4074), 17);
if (savedCurrentLatLng != null) {
_mapController.move(savedCurrentLatLng, _mapController.camera?.zoom ?? 17);
} else {
_mapController.move(const LatLng(39.9042, 116.4074), 17);
}
// 刷新成功提示
if (mounted) {
@@ -724,29 +731,13 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}
}
/// ===============================
/// 更新位置
/// ===============================
void _updateLocation(LatLng latLng, {bool moveMap = false}) {
setState(() {
_currentLatLng = latLng;
});
if (moveMap) {
//_hasMovedOnce = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_hasMovedOnce = true;
_mapController.move(latLng, 17);
});
}
}
/// 手动回到当前位置
void _moveToCurrentLocation() {
if (_currentLatLng == null) return;
_mapController.move(_currentLatLng!, 17);
}
// ========== 障碍物模式:完成当前障碍物绘制 ==========
// ========== 障碍物模式:完成当前障碍物绘制 ==========
void _completeObstacle() {
if (_currentObstaclePoints.length < 3) {
@@ -755,13 +746,20 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}
setState(() {
_obstacleHoles.add(List.from(_currentObstaclePoints)); // 保存当前障碍物组
// 保存当前障碍物组(Robot模式下直接用WGS84坐标)
if (_currentRobotMode == RobotMode.robot && _robotModeObsWgsPoints.isNotEmpty) {
final gcjPoints = _robotModeObsWgsPoints.map((wgs) => wgs84ToGcj02(wgs.latitude, wgs.longitude)).toList();
_obstacleHoles.add(gcjPoints);
_obstacleWgsHoles.add(List.from(_robotModeObsWgsPoints));
} else {
_obstacleHoles.add(List.from(_currentObstaclePoints));
}
_currentObstaclePoints.clear(); // 清空当前绘制的障碍物点
_robotModeObsWgsPoints.clear(); // 清空Robot模式下的障碍物WGS84坐标
_isObstacleEditing = false; // 退出编辑状态
});
debugPrint('完成障碍物绘制,当前障碍物组数:${_obstacleHoles.length}');
ToastUtils.showSuccess(context, '已添加第${_obstacleHoles.length}个障碍物区域');
}
@@ -796,14 +794,34 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
return;
}
setState(() {
if (_currentAreaMode == AreaMode.work) {
_markedPoints.add(_mapCenter);
debugPrint('新增作业区域打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude}, ${_mapCenter.longitude}');
if (_currentRobotMode == RobotMode.point) {
if (_currentAreaMode == AreaMode.work) {
_markedPoints.add(_mapCenter);
debugPrint('新增作业区域打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude}, ${_mapCenter.longitude}');
} else {
// 障碍物模式:添加到当前障碍物打点
_currentObstaclePoints.add(_mapCenter);
_isObstacleEditing = true; // 标记进入障碍物编辑状态
debugPrint('新增障碍物打点:第${_currentObstaclePoints.length}个点,经纬度:${_mapCenter.latitude}, ${_mapCenter.longitude}');
}
} else {
// 障碍物模式:添加到当前障碍物打点
_currentObstaclePoints.add(_mapCenter);
_isObstacleEditing = true; // 标记进入障碍物编辑状态
debugPrint('新增障碍物打点:第${_currentObstaclePoints.length}个点,经纬度:${_mapCenter.latitude}, ${_mapCenter.longitude}');
if (_currentLatLng == null || !_isValidLatLng(_currentLatLng!.latitude, _currentLatLng!.longitude)) {
ToastUtils.showError(context, '设备坐标无效,无法添加打点!');
return;
}
// 2. Robot模式-作业区域打点
if (_currentAreaMode == AreaMode.work) {
_markedPoints.add(_currentLatLng!); // 从设备实时坐标取最后一个值
_robotModeWgsPoints.add(_currentWgsLatLng!);
debugPrint('Robot模式新增作业打点:第${_markedPoints.length}个点,经纬度:${_currentLatLng!.latitude}, ${_currentLatLng!.longitude}');
} else {
// 3. Robot模式-障碍物打点
_currentObstaclePoints.add(_currentLatLng!);
_robotModeObsWgsPoints.add(_currentWgsLatLng!);
_isObstacleEditing = true;
debugPrint('Robot模式新增障碍物打点:第${_currentObstaclePoints.length}个点,经纬度:${_currentLatLng!.latitude}, ${_currentLatLng!.longitude}');
}
}
});
debugPrint('$_markedPoints,新增打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude.toStringAsFixed(20)}, ${_mapCenter.longitude.toStringAsFixed(20)}');
@@ -817,10 +835,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
@override
void dispose() {
_positionSub?.cancel();
_traceManager.reset();
_mapController.dispose();
_sub.cancel();
super.dispose();
}
@@ -883,7 +899,6 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
setState(() {
gcjPathPoints = [];
gcjOuterPoints = [];
_currentTracePoints = null;
});
await context.read<DevicesCubit>().loadSelectedPath(plot.plotName);
@@ -1160,20 +1175,28 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}
final gcjPoint = wgs84ToGcj02(lat, lng);
// 关键:判断Widget是否还挂载,避免空指针
final initCenter = bd09ToGcj02(
'120.81992468426961', // 经度
'32.04532826114155',
);
_currentWgsLatLng = LatLng(lat, lng);
_currentLatLng = gcjPoint; // ✅ 状态变量在setState内更新
return FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _currentLatLng ?? const LatLng(39.9042, 116.4074),
initialZoom: 20,
initialCenter: __isValidLatLng(_currentLatLng) ? _currentLatLng! : initCenter,
initialZoom: 22,
maxZoom: 22,
// 禁止地图点击事件(避免和中心标冲突)
minZoom: 3,
enableScrollWheel: true,
onTap: (_, __) {}, // 空实现,禁用地图点击响应
),
children: [
/// 高德瓦片(GCJ-02)
TileLayer(
//https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1'
// '&key=bbb1f0f20eed6bf679eddf2625630aba
urlTemplate:
'https://webrd02.is.autonavi.com/appmaptile'
'?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1'
@@ -1230,7 +1253,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
Polyline(points: [_markedPoints[i], _markedPoints[i + 1]], color: Colors.orange.withOpacity(0.5), strokeWidth: 1.5),
],
),
if (_markedPoints.isNotEmpty && !_isWorkAreaCompleted)
if (_markedPoints.isNotEmpty && !_isWorkAreaCompleted && _currentRobotMode == RobotMode.point)
PolylineLayer(
polylines: [
Polyline(points: [_mapCenter, _markedPoints.last], color: Colors.blue.withOpacity(0.5), strokeWidth: 1.5),
@@ -1252,7 +1275,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
],
),
if (_currentWorkMode == WorkMode.bow && _markedPoints.isNotEmpty && !_isWorkAreaCompleted)
if (_currentWorkMode == WorkMode.bow && _markedPoints.isNotEmpty && !_isWorkAreaCompleted && _currentRobotMode == RobotMode.point)
PolygonLayer(
polygons: [
Polygon(
@@ -1276,22 +1299,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
);
}).toList(),
),
if (gctracePoint!.isNotEmpty)
PolylineLayer(
polylines: [
// 已绘制的障碍物点连线
for (int i = 0; i < gctracePoint!.length - 1; i++)
Polyline(
points: [gctracePoint![i], gctracePoint![i + 1]],
color: Colors.white,
strokeWidth: 1.5,
isDotted: true, // 虚线区分作业区域
),
],
),
//开始作业之后
// 2. 正在绘制的障碍物打点连线(红色虚线)
if (_isObstacleEditing && _currentObstaclePoints.isNotEmpty)
if (_isObstacleEditing && _currentObstaclePoints.isNotEmpty && _currentRobotMode == RobotMode.point)
PolylineLayer(
polylines: [
// 已绘制的障碍物点连线
@@ -1406,6 +1417,18 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
);
}).toList(),
),
if (gctracePoint!.isNotEmpty && isStartWork)
PolylineLayer(
polylines: [
for (int i = 0; i < gctracePoint!.length - 1; i++)
Polyline(
points: [gctracePoint![i], gctracePoint![i + 1]],
color: Colors.white,
strokeWidth: 1.5,
isDotted: true, // 虚线区分作业区域
),
],
),
],
);
}
@@ -1755,14 +1778,13 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
gcjOuterPoints = List.from(_markedPoints);
typedPathList = []; // 路径列表置空
});
return;
}
// 1. 校验必要条件
if (_currentAreaMode == AreaMode.work && _markedPoints.isEmpty) {
if (showTips) {
ToastUtils.showError(context, '请先添加作业区域打点!');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先添加作业区域打点!'), backgroundColor: Colors.red));
}
return;
}
@@ -1775,54 +1797,97 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
if (_currentWorkMode == null) {
if (showTips) {
ToastUtils.showError(context, '请先选择作业模式!');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择作业模式!'), backgroundColor: Colors.red));
}
return;
}
try {
// 2. 转换页面数据为 generatePath 所需的参数格式
// 2.1 参考点(取作业区域第一个打点)
final firstPoint = _markedPoints.first;
final wgs84FirstPoint = gcj02ToWgs84(firstPoint.latitude, firstPoint.longitude);
final referencePoint = work_area_model.ReferencePoint(lat: wgs84FirstPoint.latitude, lon: wgs84FirstPoint.longitude);
// 2.1 参考点(区分Robot/Point模式)
LatLng firstPointWgs; // 最终传给接口的WGS84格式参考点
if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isNotEmpty) {
// Robot模式:直接使用原始WGS84坐标(无需转换)
firstPointWgs = _robotModeWgsPoints.first;
} else {
// Point模式:先取十字准星的GCJ02坐标 → 转换为WGS84
if (_markedPoints.isEmpty) return;
LatLng firstPointGcj = _markedPoints.first;
firstPointWgs = gcj02ToWgs84(firstPointGcj.latitude, firstPointGcj.longitude);
}
// 参考点使用转换后的WGS84坐标
final referencePoint = work_area_model.ReferencePoint(lat: firstPointWgs.latitude, lon: firstPointWgs.longitude);
// 2.2 航向角
final heading = _angle == -1 ? -1 : _angle.toInt();
// 2.3 外边界(作业区域)
final outerBoundary = work_area_model.OuterBoundary(
position: _markedPoints.map((latLng) {
// 2.3 外边界(作业区域)- 核心区分Robot/Point模式
List<work_area_model.Position> outerPositions = [];
if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isNotEmpty) {
// Robot模式:直接使用原始WGS84坐标,跳过GCJ02→WGS84转换
outerPositions = _robotModeWgsPoints.map((latLng) {
return work_area_model.Position(lat: latLng.latitude, lon: latLng.longitude);
}).toList();
} else {
// Point模式:十字准星的GCJ02坐标 → 转换为WGS84
outerPositions = _markedPoints.map((latLng) {
final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude);
return work_area_model.Position(lat: wgs84Point.latitude, lon: wgs84Point.longitude);
}).toList(),
sideWidth: _workDistance,
);
}).toList();
}
final outerBoundary = work_area_model.OuterBoundary(position: outerPositions, sideWidth: _workDistance);
// 2.4 障碍物处理(同样区分Robot/Point模式)
work_area_model.HoleBoundary? holeBoundary;
if (_obstacleHoles.isNotEmpty) {
// 1. 构造二维 Position 数组
List<List<work_area_model.Position>> positionList = [];
for (var holePoints in _obstacleHoles) {
List<work_area_model.Position> innerPositionList = holePoints.map((latLng) {
final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude);
return work_area_model.Position(lat: wgs84Point.latitude, lon: wgs84Point.longitude);
}).toList();
if (_currentRobotMode == RobotMode.robot && _robotModeObsWgsPoints.isNotEmpty) {
for (var holePoints in _obstacleWgsHoles) {
List<work_area_model.Position> innerPositionList = [];
positionList.add(innerPositionList);
innerPositionList = holePoints.map((latLng) {
return work_area_model.Position(lat: latLng.latitude, lon: latLng.longitude);
}).toList();
positionList.add(innerPositionList);
}
} else {
for (var holePoints in _obstacleHoles) {
List<work_area_model.Position> innerPositionList = [];
// Point模式:GCJ02→WGS84转换
innerPositionList = holePoints.map((latLng) {
final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude);
return work_area_model.Position(lat: wgs84Point.latitude, lon: wgs84Point.longitude);
}).toList();
positionList.add(innerPositionList);
}
}
// 2. 创建 HoleBoundary 对象(无外层 Map)
for (var holePoints in _obstacleHoles) {
List<work_area_model.Position> innerPositionList = [];
if (_currentRobotMode == RobotMode.robot && _robotModeObsWgsPoints.isNotEmpty) {
// Robot模式:使用障碍物原始WGS84坐标
innerPositionList = holePoints.map((latLng) {
return work_area_model.Position(lat: latLng.latitude, lon: latLng.longitude);
}).toList();
} else {
// Point模式:GCJ02→WGS84转换
innerPositionList = holePoints.map((latLng) {
final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude);
return work_area_model.Position(lat: wgs84Point.latitude, lon: wgs84Point.longitude);
}).toList();
}
positionList.add(innerPositionList);
}
holeBoundary = work_area_model.HoleBoundary(position: positionList, sideWidth: 0.0);
}
// 🔥 第二步:构造符合方法参数要求的 Map<String, HoleBoundary>
// 用一个临时 Map 来满足类型要求,后续在 Cubit 中取值即可
Map<String, work_area_model.HoleBoundary> holes = {};
if (holeBoundary != null) {
holes = {'tempKey': holeBoundary}; // 键名任意,仅为满足类型要求
holes = {'tempKey': holeBoundary};
}
final workType = _currentWorkMode == WorkMode.bow ? 0 : 2;
@@ -1831,7 +1896,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
reference: referencePoint,
heading: heading,
outer: outerBoundary,
holes: holes.isNotEmpty ? holes : {}, // 无障碍物时传空对象
holes: holes.isNotEmpty ? holes : {},
workType: workType,
);
@@ -1842,7 +1907,6 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
if (cubitState.errorMessage != null) {
if (showTips) {
ToastUtils.showError(context, '路径生成失败:${cubitState.errorMessage}');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成失败:${cubitState.errorMessage}'), backgroundColor: Colors.red));
}
} else if (cubitState.generatedPath != null) {
if (showTips) {
@@ -1857,17 +1921,16 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
final pathList = cubitState.generatedPath as List;
typedPathList = pathList.whereType<work_area_model.DeviceAddPathPointModel>().toList();
debugPrint('✅ 过滤并转换后的路径点列表:typedPathList 长度=${typedPathList.length}');
//打印
// 路径点转换为GCJ02(无论哪种模式,最终地图显示都需要GCJ02)
gcjPathPoints = typedPathList.map((point) {
double wgs84Lat = point.latitude;
double wgs84Lon = point.longitude;
final gcjPoint = wgs84ToGcj02(wgs84Lat, wgs84Lon);
return LatLng(gcjPoint.latitude, gcjPoint.longitude);
}).toList();
// 外边界保持原有逻辑
gcjOuterPoints = List.from(_markedPoints);
//_markedPoints.clear();
});
moveMapToPointsCenter(gcjPathPoints);
}
@@ -1876,7 +1939,6 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
debugPrint('路径生成异常:$e');
if (showTips) {
ToastUtils.showError(context, '路径生成异常:${e.toString().substring(0, 50)}');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成异常:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red));
}
}
}
@@ -1889,13 +1951,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
final userState = context.watch<AppUserCubit>().state;
final deviceId = context.watch<DevicesCubit>().state.selectedDevice?.deviceName;
if (deviceId != null && userState.user?.token != null) {
if (deviceId != null && userState.user != null && userState.user!.token != null) {
_videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
} else {
_videoStreamUrl = ''; // 无设备/用户信息时置空
_videoStreamUrl = '';
print("设备未选中或用户未登录,无法生成视频流地址");
}
return BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
// 初始化默认值
@@ -1986,22 +2047,23 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
/// 核心:地图正中心固定定位标
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: Center(
// 自定义十字标(中心精准对齐地图中心)
child: SizedBox(
width: 16, // 十字整体宽度
height: 16, // 十字整体高度
child: CustomPaint(
painter: CrosshairPainter(), // 自定义十字画笔
if (_currentRobotMode == RobotMode.point)
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: Center(
// 自定义十字标(中心精准对齐地图中心)
child: SizedBox(
width: 16, // 十字整体宽度
height: 16, // 十字整体高度
child: CustomPaint(
painter: CrosshairPainter(), // 自定义十字画笔
),
),
),
),
),
//保存按钮
if (_saveBoxOpen)
@@ -2068,8 +2130,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
right: 0,
bottom: 0,
child: BottomOperationPanel(
initialRobotMode: RobotMode.point,
initialAreaMode: AreaMode.work,
initialRobotMode: _currentRobotMode,
initialAreaMode: _currentAreaMode,
initialWorkMode: _currentWorkMode!,
canUndo: _isWorkAreaCompleted && _currentAreaMode == AreaMode.work ? false : true,
onComplete: () async {
@@ -2302,6 +2364,33 @@ List<LatLng> batchWgs84ToGcj02(List<LatLng> wgs84Points) {
}).toList();
}
LatLng bd09ToGcj02(dynamic bdLng, dynamic bdLat) {
// 1. 先将字符串转为double(兼容你的初始值格式)
double lng = _safeToDouble(bdLng);
double lat = _safeToDouble(bdLat);
// 2. BD09转GCJ02核心公式
double x = lng - 0.0065;
double y = lat - 0.006;
double z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * _pi);
double theta = math.atan2(y, x) - 0.000003 * math.cos(x * _pi);
double gcjLng = z * math.cos(theta);
double gcjLat = z * math.sin(theta);
return LatLng(gcjLat, gcjLng);
}
double _safeToDouble(dynamic value) {
if (value == null) return 0.0;
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is String) {
// 字符串转数字,失败返回0.0
return double.tryParse(value) ?? 0.0;
}
return 0.0;
}
LatLng gcj02ToWgs84(double lat, double lon) {
if (_outOfChina(lat, lon)) {
return LatLng(lat, lon);

View File

@@ -28,7 +28,7 @@ class BottomOperationPanel extends StatefulWidget {
const BottomOperationPanel({
super.key,
this.initialRobotMode = RobotMode.point,
this.initialRobotMode = RobotMode.robot,
this.initialAreaMode = AreaMode.work,
this.initialSelectedTab = '地块', // 默认选中地块
this.initialWorkMode = WorkMode.bow,