diff --git a/lib/features/devices/data/datasources/impl/path_http_datasource_impl.dart b/lib/features/devices/data/datasources/impl/path_http_datasource_impl.dart index 3f1c9bee..96064cd0 100644 --- a/lib/features/devices/data/datasources/impl/path_http_datasource_impl.dart +++ b/lib/features/devices/data/datasources/impl/path_http_datasource_impl.dart @@ -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'); diff --git a/lib/features/home/presentation/pages/running_status_page.dart b/lib/features/home/presentation/pages/running_status_page.dart index a6bbc82a..1247df5f 100644 --- a/lib/features/home/presentation/pages/running_status_page.dart +++ b/lib/features/home/presentation/pages/running_status_page.dart @@ -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 { 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 _getMappedSpots(List data) { final List mapped = []; @@ -74,6 +112,11 @@ class _RunningStatusPageState extends State { 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 { // ====================== 图表视图 ====================== 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 { // ====================== 卡片视图 ====================== 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 { BlocBuilder( 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 { } 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 { }); } - if (state is DeviceStatusUpdated) { + if (!_isDataTimeout && state is DeviceStatusUpdated) { debugPrint('📈 [UI] 检测到 Updated 状态,准备追加图表数据'); _appendChartData(state); } @@ -886,7 +946,21 @@ class _RunningStatusPageState extends State { ); } - 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 ====================== diff --git a/lib/features/home/presentation/widgets/map/testmap_pages.dart b/lib/features/home/presentation/widgets/map/testmap_pages.dart index 1cef43d8..97e28176 100644 --- a/lib/features/home/presentation/widgets/map/testmap_pages.dart +++ b/lib/features/home/presentation/widgets/map/testmap_pages.dart @@ -70,7 +70,6 @@ class MapPageEnterprise extends StatefulWidget { class _MapPageEnterpriseState extends State { final _dispatcher = sl(); - late StreamSubscription _sub; bool _isRefreshing = false; // 新增:页面刷新状态标志 bool _isVideoDialogOpen = false; // 控制视频弹窗显示 String _videoStreamUrl = ""; @@ -88,9 +87,9 @@ class _MapPageEnterpriseState extends State { WorkStatus _workStatus = WorkStatus.idle; bool isStartWork = false; //是否开始作业 - LatLng? _currentLatLng; - StreamSubscription? _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 { // 新增:控制底部作业面板显示 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 { List? gctracePoint = []; // ========== 核心新增状态 ========== - final List _markedPoints = []; // 存储所有打点坐标 + final List _markedPoints = []; // 存储所有s十字准星打点坐标 + List _robotModeWgsPoints = []; // Robot模式下从设备获取的wgs打点 + List _robotModeObsWgsPoints = []; // Robot模式下从设备获取的wgs打点 障碍物 + LatLng _mapCenter = const LatLng(39.9042, 116.4074); double _headingAngle = 0.0; // 当前机器航向角(单位:度) - List? _currentTracePoints; //收到机器的点坐标 List typedPathList = []; // 存储生成路径的坐标列表(已转换为LatLng) // ========== 障碍物模式核心状态 ========== - List> _obstacleHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个空洞) + List> _obstacleHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个障碍物) + + List> _obstacleWgsHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个障碍物) List _currentObstaclePoints = []; // 当前正在绘制的障碍物打点 bool _isObstacleEditing = false; // 是否处于障碍物编辑状态 @@ -180,14 +183,21 @@ class _MapPageEnterpriseState extends State { // 计算边界 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 { 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 { 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 { //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 { Future _initLocationEnterprise() async { // 1. 设置轨迹模式为 LOCATION(核心:确保模式正确) _traceManager.setMode(TPMode.LOCATION); - - // 2. 清空旧的定位监听(如果有) - await _positionSub?.cancel(); - _positionSub = null; - - // 3. 初始化默认坐标(可选,避免地图空白) - setState(() { - _currentLatLng = const LatLng(39.9042, 116.4074); // 北京默认坐标 - }); } // ========== 新增:页面刷新初始化方法 ========== Future _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 { // 重置参数 _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 { _traceManager.reset(); // 5. 重新加载作业记录(保持数据最新) - final userId = context.read().state.user?.userId ?? ""; - if (userId.isNotEmpty) { - await context.read().loadWorkRecords(userId); - } + // final userId = context.read().state.user?.userId ?? ""; + // if (userId.isNotEmpty) { + // await context.read().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 { } } - /// =============================== - /// 更新位置 - /// =============================== - 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 { } 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 { 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 { @override void dispose() { - _positionSub?.cancel(); _traceManager.reset(); _mapController.dispose(); - _sub.cancel(); super.dispose(); } @@ -883,7 +899,6 @@ class _MapPageEnterpriseState extends State { setState(() { gcjPathPoints = []; gcjOuterPoints = []; - _currentTracePoints = null; }); await context.read().loadSelectedPath(plot.plotName); @@ -1160,20 +1175,28 @@ class _MapPageEnterpriseState extends State { } 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 { 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 { ), ], ), - 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 { ); }).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 { ); }).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 { 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 { 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 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> positionList = []; - for (var holePoints in _obstacleHoles) { - List 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 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 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 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 - // 用一个临时 Map 来满足类型要求,后续在 Cubit 中取值即可 Map 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 { reference: referencePoint, heading: heading, outer: outerBoundary, - holes: holes.isNotEmpty ? holes : {}, // 无障碍物时传空对象 + holes: holes.isNotEmpty ? holes : {}, workType: workType, ); @@ -1842,7 +1907,6 @@ class _MapPageEnterpriseState extends State { 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 { final pathList = cubitState.generatedPath as List; typedPathList = pathList.whereType().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 { 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 { final userState = context.watch().state; final deviceId = context.watch().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( builder: (context, state) { // 初始化默认值 @@ -1986,22 +2047,23 @@ class _MapPageEnterpriseState extends State { ), /// 核心:地图正中心固定定位标 - 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 { 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 batchWgs84ToGcj02(List 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); diff --git a/lib/features/home/presentation/widgets/startpoint_area.dart b/lib/features/home/presentation/widgets/startpoint_area.dart index c323acfe..e1efc38c 100644 --- a/lib/features/home/presentation/widgets/startpoint_area.dart +++ b/lib/features/home/presentation/widgets/startpoint_area.dart @@ -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,