import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:math'; import 'dart:math' as math; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:geolocator/geolocator.dart'; import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; import 'package:maibu_satabot_v2/components/confrim_dialog.dart'; import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart'; import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart'; import 'package:maibu_satabot_v2/features/devices/data/models/device_add_path_point_model.dart' as work_area_model; import 'package:maibu_satabot_v2/features/devices/data/models/device_work_area_param_model.dart' as work_area_model; import 'package:maibu_satabot_v2/features/devices/data/repositories/generate_path_repository_Impl.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_work_record_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_record_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_state.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirectionLine.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart'; import 'package:maibu_satabot_v2/components/input_confirm_dialog.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/tracepoint.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/CenterLocation.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/HeadingPointer.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/startpoint_area.dart'; import 'package:image/image.dart' as _image; // 注意命名空间冲突,使用 as img import 'dart:ui' as ui; import '../path_list_pages.dart'; // 定义轨迹点类型(经纬度) typedef PlotPoint = LatLng; // 保持 PlotData 类不变 class PlotData { final String id; final String plotName; final String imageUrl; // 图片URL(本地/assets/网络都可) final String? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用) PlotData({required this.id, required this.plotName, required this.imageUrl, this.jsonData}); } class PlotDataPath { final dynamic? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用) PlotDataPath({this.jsonData}); } class MapPageEnterprise extends StatefulWidget { const MapPageEnterprise({Key? key}) : super(key: key); @override State createState() => _MapPageEnterpriseState(); } class _MapPageEnterpriseState extends State { final _dispatcher = sl(); late StreamSubscription _sub; bool _isRefreshing = false; // 新增:页面刷新状态标志 // 🔥 关键修复:添加截图全局Key final GlobalKey _mapRepaintKey = GlobalKey(); final MapController _mapController = MapController(); // 初始化轨迹管理器(泛型指定为LatLng) late final TracePoint _traceManager; late String workMode = "弓字模式"; // 作业模式:默认值为"弓字模式" WorkStatus _workStatus = WorkStatus.idle; LatLng? _currentLatLng; StreamSubscription? _positionSub; bool _hasMovedOnce = false; bool _isPanelOpen = false; // 控制面板显示/隐藏 bool _directionBoxOpen = false; //航线面板显示/隐藏 bool _saveBoxOpen = false; //保存按钮显示/隐藏 bool _isListBoxOpen = false; //列表面板显示/隐藏 // 新增:存储选中的地块(null表示未选中) PlotData? _selectedPlot; PlotDataPath? _selectedPlotPath; // 存储选中地块的路径数据(可选) // 新增:控制底部作业面板显示 bool _isWorkPanelOpen = false; bool _isWorkAreaCompleted = false; RobotMode _currentRobotMode = RobotMode.point; //打点模式 AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域 WorkMode? _currentWorkMode = WorkMode.bow; // 当前作业模式 // 新增:存储转换后的path和outer坐标 List gcjPathPoints = []; List gcjOuterPoints = []; // ========== 核心新增状态 ========== final List _markedPoints = []; // 存储所有打点坐标 LatLng _mapCenter = const LatLng(39.9042, 116.4074); final double _headingAngle = 0.0; // 当前机器航向角(单位:度) List? _currentTracePoints; //收到机器的点坐标 List typedPathList = []; // 存储生成路径的坐标列表(已转换为LatLng) // ========== 障碍物模式核心状态 ========== List> _obstacleHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个空洞) List _currentObstaclePoints = []; // 当前正在绘制的障碍物打点 bool _isObstacleEditing = false; // 是否处于障碍物编辑状态 //打点 double _workDistance = 0.0; // 作业行距(单位:米) double _angle = 0.0; // 航线方向角(单位:度) @override void initState() { super.initState(); _traceManager = TracePoint(); _initLocationEnterprise(); _sub = _dispatcher.onCommand(0x12).listen(_onDeviceData); // 设置事件回调(更新UI) _traceManager.onCurrentPointUpdated = (point) { setState(() { _currentTracePoints = _traceManager.getTracePoint(); }); }; _traceManager.onCompletePointAdded = (point) { setState(() { _currentTracePoints = _traceManager.getTracePoint(); }); }; // 示例:切换到导航模式 _traceManager.setMode(TPMode.LOCATION); // 监听地图移动事件,实时更新连线 _mapController.mapEventStream.listen((event) { if (event is MapEventMove || event is MapEventMoveEnd) { if (mounted && _mapController.camera != null) { final originalCenter = _mapController.camera!.center; final highPrecisionCenter = LatLng( originalCenter.latitude, // 原始纬度(保留全部小数位) originalCenter.longitude, // 原始经度(保留全部小数位) ); setState(() { _mapCenter = highPrecisionCenter; }); } } }); } // ========== 新增:计算坐标列表的边界范围 ========== LatLngBounds? calculateBounds(List points) { if (points.isEmpty) return null; // 使用静态方法 fromPoints 创建边界(所有latlong2版本都支持) return LatLngBounds.fromPoints(points); } // ========== 新增:移动地图到坐标中心 ========== // ========== 移动地图到坐标中心(无需修改) ========== void moveMapToPointsCenter(List points) { if (points.isEmpty || !mounted) return; // 计算边界 final bounds = calculateBounds(points); if (bounds == null) return; // 移动地图到边界中心(自动适配缩放级别) _mapController.fitBounds( bounds, options: FitBoundsOptions( padding: const EdgeInsets.all(50), // 边缘留白(避免内容贴边) //maxZoom: 18, // 最大缩放级别(防止过度放大) ), ); print('地图已移动到绘制内容中心,边界范围:$bounds'); } // ========== 新增:安全转换任意类型为double ========== double? safeToDouble(dynamic value) { if (value == null) return null; if (value is double) return value; if (value is int) return value.toDouble(); if (value is String) { return double.tryParse(value); // 字符串转数字(失败返回null) } return null; } void _onDeviceData(RawPacket packet) { try { debugPrint('>>> 收到0x12设备数据: ${packet}'); debugPrint('>>> 收到设备数据: ${utf8.decode(packet.payload)}'); final csv = utf8.decode(packet.payload); final fields = csv.split(','); // 至少需 16 个字段(索引 10=航向角, 13=卫星数, 14=定位质量) if (fields.length >= 16) { //setState(() { // _voltage = fields[0]; // 电压 // _leftSpeed = fields[1]; // 左轮目标速度 // _rightSpeed = fields[2]; // 右轮目标速度 // _pitch = fields[11]; // 俯仰角 // _roll = fields[12]; // 翻滚角 // _latitude = fields[16]; // 纬度 // _longitude = fields[17]; // 经度 // _timeStamp = '${fields[16]} ${fields[17]}'; // 时间戳(合并字段) // _knifeCuttingSpeed = fields[19]; // 割刀速度 // _controlMode = fields[20]; // 控制模式 // _workingArea = fields[21]; // 作业面积 // _battery = fields[22]; // 电量 // _obstacleFlag = fields[23]; // 障碍物标志 // _yaw = fields[10]; // 航向角 // _satelliteCnt = fields[13]; // 卫星数 // _qual = fields[14]; // 定位质量 //}); } } catch (_) { // 解析失败时忽略,避免崩溃 } } void _showSavePlotDialog() { showInputConfirmDialog( context: context, title: '保存地块', // 自定义标题 hintText: '请输入地块名称(如:北地块、一号田)', // 自定义输入提示 labelText: '地块名称', // 自定义输入框标签 confirmText: '保存', // 确认按钮文字 cancelText: '取消', // 取消按钮文字 // 输入校验器(可选) inputValidator: (inputText) { if (inputText.isEmpty) { return '地块名称不能为空!'; } if (inputText.length > 20) { return '地块名称不能超过20个字符!'; } return null; // 校验通过 }, onConfirm: (plotName) async { // 1. 关闭输入弹窗,显示截图加载中 Navigator.pop(context); showDialog( context: context, barrierDismissible: false, // 禁止点击外部关闭 builder: (ctx) => const Center( child: SizedBox(width: 60, height: 60, child: CircularProgressIndicator(strokeWidth: 3, color: Colors.white)), ), ); // 2. 执行截图(核心调用) String? imgBase64 = await _captureMapToJpgBase64(quality: 80); // 4. 处理截图结果并保存 if (mounted) { if (imgBase64 != null) { _savePlotData(plotName, imgBase64); // 传入 Base64 } else { // 截图失败时,允许空图片保存(可根据业务调整为强制失败) _savePlotData(plotName, null); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块保存成功,但地图截图生成失败!'), backgroundColor: Colors.amber)); } } }, ); } void _debugPrintMultipartRequest(http.MultipartRequest request) { debugPrint('===================================='); debugPrint('📡 请求 URL: ${request.url}'); debugPrint('📡 请求方法: ${request.method}'); // 1. 打印请求头(包含自动生成的 boundary) debugPrint('📡 请求头: ${request.headers}'); // 2. 打印普通字段 (虽然我们现在没有普通字段,但保留以防万一) if (request.fields.isNotEmpty) { debugPrint('📝 普通字段:'); request.fields.forEach((key, value) { debugPrint(' - $key: $value'); }); } else { debugPrint('📝 普通字段: 无'); } // 3. 打印文件/Blob 字段 (核心:这里包含 file 和 workRecord) debugPrint('📁 上传文件/Blob 数量: ${request.files.length}'); for (var file in request.files) { debugPrint('------------------------------------'); debugPrint(' 字段名 (name): ${file.field}'); debugPrint(' 文件名 (filename): ${file.filename}'); debugPrint(' 内容类型 (contentType): ${file.contentType}'); // 特殊处理:如果是 workRecord,打印其内容 if (file.field == 'workRecord' && file is http.MultipartFile) { // 将 ByteStream 转为 Uint8List 以查看内容 file.finalize().first.then((bytes) { String content = utf8.decode(bytes); // 格式化 JSON 打印,更易读 try { dynamic jsonObj = jsonDecode(content); debugPrint(' 内容 (content): ${const JsonEncoder.withIndent(' ').convert(jsonObj)}'); } catch (e) { debugPrint(' 内容 (content): $content'); } }); } else { // 对于图片文件,打印大小即可,避免打印海量 Base64 file.finalize().first.then((bytes) { debugPrint(' 文件大小: ${bytes.length} 字节 (约 ${(bytes.length / 1024).toStringAsFixed(1)} KB)'); }); } } debugPrint('===================================='); } // 3. 保存地块数据的核心方法(这里写你的实际保存逻辑) // 🔥 关键修改:增加 imgBase64 参数 // 🔥 新的保存方法,严格匹配后端结构 Future _savePlotData(String plotName, String? imgBase64) async { // 1. 获取用户ID final userId = context.read().state.user?.userId ?? ""; if (userId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('用户ID为空,无法保存'), backgroundColor: Colors.red)); return; } // 2. 构造 SavePath 数据 (严格对应你的 JS 结构) final Map savePath = { //'img': imgBase64 ?? '', // 截图的 Base64 字符串 'name': plotName, // 地块名称 'path': _currentWorkMode == WorkMode.custom ? jsonEncode([]) // 自定义模式 path 为空数组 : jsonEncode( typedPathList .map( (point) => { // 路径点数组 'lon': point.longitude, 'lat': point.latitude, }, ) .toList(), ), 'outer': jsonEncode( gcjOuterPoints.map((point) { final wgs84 = gcj02ToWgs84(point.latitude, point.longitude); return {'lng': wgs84.longitude, 'lat': wgs84.latitude}; }).toList(), ), 'planModel': _currentWorkMode == WorkMode.bow ? 0 : 2, // 作业模式值 }; // 2. 解析并打印 Path try { final List pathData = jsonDecode(savePath['path'] as String); print('\n🗺️ 路径数据 (共 ${pathData.length} 个点):'); // 格式化打印前 3 个点,避免数据过多刷屏 for (int i = 0; i < math.min(3, pathData.length); i++) { final point = pathData[i] as Map; print(' 点 $i: lat=${point['lat']}, lon=${point['lon']}'); } if (pathData.length > 3) { print(' ... 还有 ${pathData.length - 3} 个点'); } } catch (e) { print('❌ 解析 path 失败: $e'); } // 3. 解析并打印 Outer try { final List outerData = jsonDecode(savePath['outer'] as String); print('\n📏 外边界数据 (共 ${outerData.length} 个点):'); // 格式化打印所有外边界点(通常数量少) for (int i = 0; i < outerData.length; i++) { final point = outerData[i] as Map; print(' 点 $i: lat=${point['lat']}, lon=${point['lon']}'); } } catch (e) { print('❌ 解析 outer 失败: $e'); } // 3. 构造 workRecord (包裹一层) final Map workRecord = { 'workName': plotName, 'userId': userId, 'jsonData': jsonEncode(savePath), // 将 savePath 转为 JSON 字符串 }; final String workRecordJson = jsonEncode(workRecord); final http.MultipartRequest request = http.MultipartRequest('POST', Uri.parse('https://serviceri.satabot.com/iot/workRecord/add')); // 5. 处理文件:将 Base64 转为 MultipartFile (对应 JS 的 dataURLtoFile) if (imgBase64 != null && imgBase64.isNotEmpty) { // 移除 Base64 头部 (如果有的话) String base64String = imgBase64; if (base64String.startsWith('data:image/jpeg;base64,')) { base64String = base64String.split(',').last; } Uint8List bytes = base64Decode(base64String); // 添加文件 (对应 formData.append("file", file)) request.files.add(http.MultipartFile.fromBytes('file', bytes, filename: 'image.jpg', contentType: http.MediaType('image', 'jpeg'))); } // 6. 添加 workRecord (对应 formData.append("workRecord", Blob)) request.files.add( http.MultipartFile.fromString( 'workRecord', // 键名必须是 workRecord workRecordJson, filename: 'workRecord.json', // 后端可能需要这个文件名来识别 contentType: http.MediaType('application', 'json'), // 关键:指定 JSON 类型 ), ); //_debugPrintMultipartRequest(request); // 7. 发送请求 try { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('正在保存地块...'), backgroundColor: Colors.blue)); final http.StreamedResponse response = await request.send(); final String responseBody = await response.stream.bytesToString(); debugPrint('保存地块响应状态: ${response.statusCode}'); debugPrint('保存地块响应体: $responseBody'); if (response.statusCode == 200) { // 保存成功 setState(() { _markedPoints.clear(); // 清空打点 gcjPathPoints.clear(); // 清空路径 gcjOuterPoints.clear(); // 清空外边界 _isPanelOpen = false; // 关闭操作面板 _isWorkAreaCompleted = false; // 重置作业区域完成状态 }); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块「$plotName」保存成功!'), backgroundColor: Colors.green)); } else { throw Exception('服务器错误: ${response.statusCode}, $responseBody'); } } catch (e) { debugPrint('保存失败: $e'); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'), backgroundColor: Colors.red)); } } List _getPolygonPoints() { if (_markedPoints.isEmpty || _mapController.camera == null) return []; List polygonPoints = List.from(_markedPoints); polygonPoints.add(_mapCenter); return polygonPoints; } void _undoLastPoint() { if (_markedPoints.isNotEmpty) { setState(() { _markedPoints.removeLast(); // 删除最后一个打点 }); debugPrint('撤回成功,剩余打点数量:${_markedPoints.length}'); } else { debugPrint('无打点可撤回'); // 可选:提示用户 ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无打点可撤回'))); } } // ========== 核心:清空所有打点和轨迹 ========== void _deleteAllPoints() { if (_markedPoints.isNotEmpty) { setState(() { _markedPoints.clear(); // 清空所有打点 }); debugPrint('删除成功,已清空所有打点和轨迹'); // 可选:提示用户 ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已清空所有打点和轨迹'))); } else { debugPrint('无打点可删除'); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无打点可删除'))); } } /// =============================== /// 企业级定位初始化(已修复坐标系) /// =============================== Future _initLocationEnterprise() async { final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return; LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission != LocationPermission.whileInUse && permission != LocationPermission.always) { return; } // 1️⃣ 首次强制获取定位 final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.bestForNavigation); final gcj = wgs84ToGcj02(position.latitude, position.longitude); _updateLocation(gcj, moveMap: true); // 2️⃣ 实时监听(不再强制移动地图) _positionSub = Geolocator.getPositionStream(locationSettings: const LocationSettings(accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 2)) .listen((pos) { final gcj = wgs84ToGcj02(pos.latitude, pos.longitude); _updateLocation(gcj, moveMap: false); }); } // ========== 新增:页面刷新初始化方法 ========== Future _handleRefresh() async { // 防止重复刷新 if (_isRefreshing) return; setState(() { _isRefreshing = true; // 标记开始刷新 }); try { // 1. 取消旧的定位监听 await _positionSub?.cancel(); // 2. 重置所有页面状态(恢复初始值) setState(() { // 清空打点和轨迹数据 _markedPoints.clear(); gcjPathPoints.clear(); gcjOuterPoints.clear(); _currentTracePoints = null; _isWorkAreaCompleted = false; _saveBoxOpen = false; // 重置面板状态 _isPanelOpen = false; _directionBoxOpen = false; _isListBoxOpen = false; _isWorkPanelOpen = false; _obstacleHoles.clear(); _currentObstaclePoints.clear(); _isObstacleEditing = false; // 重置选中状态 _selectedPlot = null; _selectedPlotPath = null; // 重置作业模式 _currentRobotMode = RobotMode.point; _currentAreaMode = AreaMode.work; _currentWorkMode = WorkMode.bow; // 重置参数 _workDistance = 0.0; _angle = 0.0; _mapCenter = const LatLng(39.9042, 116.4074); _hasMovedOnce = false; _currentLatLng = null; }); // 3. 重新初始化定位 await _initLocationEnterprise(); // 4. 重置轨迹管理器 _traceManager.reset(); // 5. 重新加载作业记录(保持数据最新) 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 (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('页面已刷新完成'), backgroundColor: Colors.green, duration: Duration(seconds: 2))); } } catch (e) { // 错误处理 debugPrint('刷新失败:$e'); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('刷新失败:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red, duration: Duration(seconds: 3))); } } finally { // 标记刷新结束 if (mounted) { setState(() { _isRefreshing = false; }); } } } /// =============================== /// 更新位置 /// =============================== void _updateLocation(LatLng latLng, {bool moveMap = false}) { setState(() { _currentLatLng = latLng; }); if (moveMap && !_hasMovedOnce) { _hasMovedOnce = true; _mapController.move(latLng, 17); } } /// 手动回到当前位置 void _moveToCurrentLocation() { if (_currentLatLng == null) return; _mapController.move(_currentLatLng!, 17); } // ========== 障碍物模式:完成当前障碍物绘制 ========== void _completeObstacle() { if (_currentObstaclePoints.length < 3) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('障碍物区域至少需要3个打点!'), backgroundColor: Colors.red)); return; } setState(() { _obstacleHoles.add(List.from(_currentObstaclePoints)); // 保存当前障碍物组 _currentObstaclePoints.clear(); // 清空当前绘制的障碍物点 _isObstacleEditing = false; // 退出编辑状态 }); debugPrint('完成障碍物绘制,当前障碍物组数:${_obstacleHoles.length}'); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已添加第${_obstacleHoles.length}个障碍物区域'), backgroundColor: Colors.green)); } void _undoObstaclePoint() { if (_currentObstaclePoints.isNotEmpty) { setState(() => _currentObstaclePoints.removeLast()); debugPrint('撤回障碍物打点,剩余:${_currentObstaclePoints.length}个'); } else if (_obstacleHoles.isNotEmpty) { // 无当前绘制点时,删除最后一组障碍物 setState(() => _obstacleHoles.removeLast()); debugPrint('删除最后一组障碍物,剩余组数:${_obstacleHoles.length}'); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无障碍物打点可撤回'))); } } // ========== 障碍物模式:清空所有障碍物 ========== void _clearAllObstacles() { setState(() { _obstacleHoles.clear(); _currentObstaclePoints.clear(); _isObstacleEditing = false; }); debugPrint('已清空所有障碍物区域'); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已清空所有障碍物区域'), backgroundColor: Colors.amber)); } // ========== 核心方法:打点逻辑 ========== void _addMarkedPoint() { setState(() { 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}'); } }); debugPrint('$_markedPoints,新增打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude.toStringAsFixed(20)}, ${_mapCenter.longitude.toStringAsFixed(20)}'); } // ========== 新增:返回上一级页面的方法 ========== void _navigateBack() { // 关闭当前页面,返回上一级 Navigator.of(context).pop(); } @override void dispose() { _positionSub?.cancel(); _traceManager.reset(); _mapController.dispose(); _sub.cancel(); super.dispose(); } // 模拟添加完成点 void _addCompletePoint() { final newPoint = LatLng(39.9042 + (DateTime.now().millisecond / 10000), 116.4074 + (DateTime.now().millisecond / 10000)); _traceManager.upsert(newPoint, TPAction.ADD); } // 模拟更新当前点 void _updateCurrentPoint() { final newPoint = LatLng(39.9042 + (DateTime.now().millisecond / 10000), 116.4074 + (DateTime.now().millisecond / 10000)); _traceManager.upsert(newPoint); } // 🔥 改动2:修改 _buildPlotListItem 方法,增加删除时调用Bloc的逻辑 Widget _buildPlotListItem(PlotData plot, Function(PlotData) onDelete) { return GestureDetector( // 核心:点击列表项触发选中逻辑 onTap: () async { // 1. 清空之前的轨迹数据 setState(() { gcjPathPoints = []; gcjOuterPoints = []; _currentTracePoints = null; }); await context.read().loadSelectedPath(plot.plotName); final cubitState = context.read().state; final _loadedPlot = cubitState.pathData; print('加载地块「${plot.plotName}」的路径数据: $_loadedPlot'); // 2. 立即解析并更新状态(在事件回调中,不在 build 中) //if (_loadedPlot is List) { // final List rawList = _loadedPlot as List; // final pathRecords = rawList.where((item) => item is Map).cast>().toList(); // // 打印:过滤后的 pathRecords 长度和第一条数据的顶层字段 // print('🔍 过滤后 pathRecords 长度: ${pathRecords.length}'); // if (pathRecords.isNotEmpty) { // final Map firstRecord = pathRecords.first; // print('📋 第一条记录的顶层字段:'); // print(' - id: ${firstRecord['id']}'); // print(' - workName: ${firstRecord['workName']}'); // print(' - userId: ${firstRecord['userId']}'); // print(' - jsonData (原始字符串长度): ${firstRecord['jsonData']?.toString().length ?? 0} 字符'); // print('------------------------------------'); // final selectedPlotPath = PlotDataPath(jsonData: firstRecord); // try { // final dynamic nestedJsonRaw = selectedPlotPath.jsonData['jsonData']; // print('📦 嵌套 jsonData 原始类型: ${nestedJsonRaw.runtimeType}'); // print('📦 嵌套 jsonData 前200字符: ${nestedJsonRaw?.toString().substring(0, min(200, nestedJsonRaw?.toString().length ?? 0))}...'); // if (nestedJsonRaw != null && nestedJsonRaw is String) { // final String nestedJsonStr = nestedJsonRaw; // final dynamic parsedJson = jsonDecode(nestedJsonStr); // print('✅ 嵌套 JSON 解析成功,类型: ${parsedJson.runtimeType}'); // // 格式化打印完整的嵌套 JSON(排除 img 字段,避免刷屏) // if (parsedJson is Map) { // final Map nestedJson = Map.from(parsedJson); // // 截断 img 字段的 Base64 数据 // if (nestedJson.containsKey('img')) { // final String imgBase64 = nestedJson['img'] as String; // nestedJson['img'] = imgBase64.substring(0, min(50, imgBase64.length)) + '...(Base64 已截断)'; // } // print('📝 格式化嵌套 JSON 数据:'); // print(const JsonEncoder.withIndent(' ').convert(nestedJson)); // print('------------------------------------'); // // 解析作业模式并打印 // final dynamic planModel = nestedJson['planModel']; // workMode = planModel == WorkMode.bow.value ? "弓字模式" : "自定义模式"; // print('⚙️ 解析出作业模式: $workMode (原始 planModel 值: $planModel)'); // // 解析 Path 并打印明细 // final dynamic rawPath = nestedJson['path']; // List pathList = rawPath is List ? rawPath : []; // print('🗺️ 解析出 Path 数据:'); // print(' - Path 类型: ${rawPath.runtimeType}'); // print(' - Path 长度: ${pathList.length} 个点'); // // 逐点打印前 5 个 Path 坐标(避免点太多刷屏) // for (int i = 0; i < min(5, pathList.length); i++) { // final dynamic point = pathList[i]; // print(' - Path 点 $i: lat=${point['lat']}, lon=${point['lon']} (或 lng=${point['lng']})'); // } // if (pathList.length > 5) { // print(' - ... 还有 ${pathList.length - 5} 个点未显示'); // } // // 解析 Outer 并打印明细 // final dynamic rawOuter = nestedJson['outer']; // List outerList = rawOuter is List ? rawOuter : []; // print('📏 解析出 Outer 外边界数据:'); // print(' - Outer 类型: ${rawOuter.runtimeType}'); // print(' - Outer 长度: ${outerList.length} 个点'); // // 逐点打印前 5 个 Outer 坐标 // for (int i = 0; i < min(5, outerList.length); i++) { // final dynamic point = outerList[i]; // print(' - Outer 点 $i: lat=${point['lat']}, lon=${point['lon']} (或 lng=${point['lng']})'); // } // if (outerList.length > 5) { // print(' - ... 还有 ${outerList.length - 5} 个点未显示'); // } // // 后续坐标转换逻辑(保持你的原有代码) // // ...(这里放你原来的坐标转换、setState 逻辑) // } else { // print('❌ 解析失败:嵌套 JSON 不是 Map 类型,实际类型: ${parsedJson.runtimeType}'); // } // } else { // print('❌ 解析失败:nestedJsonRaw 为空或不是字符串,实际值: $nestedJsonRaw,类型: ${nestedJsonRaw?.runtimeType}'); // } // } catch (e, stackTrace) { // print('❌ 解析嵌套 JSON 时发生异常: $e'); // print('📜 异常堆栈: $stackTrace'); // } // } else { // print('❌ 过滤后 pathRecords 为空,无有效数据'); // } //} if (_loadedPlot is List) { final List rawList = _loadedPlot as List; final pathRecords = rawList.where((item) => item is Map).cast>().toList(); if (pathRecords.isNotEmpty) { final selectedPlotPath = PlotDataPath(jsonData: pathRecords.first); try { final dynamic nestedJsonRaw = selectedPlotPath.jsonData['jsonData']; if (nestedJsonRaw != null && nestedJsonRaw is String) { final String nestedJsonStr = nestedJsonRaw; final dynamic parsedJson = jsonDecode(nestedJsonStr); workMode = parsedJson['planModel'] == WorkMode.bow.value ? "弓字模式" : "自定义模式"; debugPrint('解析出的作业模式: ${parsedJson['planModel']},WorkMode.bow.value=${WorkMode.bow.value}, 解析结果: $workMode'); if (parsedJson is Map) { final Map nestedJson = parsedJson; // 替换你原来的 rawPath/rawOuter 解析部分 final dynamic rawPath = nestedJson['path']; final dynamic rawOuter = nestedJson['outer']; debugPrint('解析后的路径数据11: pathList=$rawPath,'); debugPrint('解析后的外边界数据22: outerList=$rawOuter,'); // ========== 修复核心:先解码字符串,再处理类型 ========== List pathList = []; if (rawPath is String) { try { pathList = jsonDecode(rawPath) as List; } catch (e) { debugPrint('path 字符串解码失败: $e, rawPath=$rawPath'); pathList = []; } } else if (rawPath is List) { pathList = rawPath; } else { pathList = []; } List outerList = []; if (rawOuter is String) { try { outerList = jsonDecode(rawOuter) as List; } catch (e) { debugPrint('outer 字符串解码失败: $e, rawOuter=$rawOuter'); outerList = []; } } else if (rawOuter is List) { outerList = rawOuter; } else { outerList = []; } // ========== 修复结束 ========== debugPrint('解析后的路径数据: pathList=$pathList, 长度=${pathList.length}'); debugPrint('解析后的外边界数据: outerList=$outerList, 长度=${outerList.length}'); List newPathPoints = []; List newOuterPoints = []; for (var item in pathList) { double? lat = safeToDouble(item['lat']); double? lon = safeToDouble(item['lon']); if (lat != null && lon != null) { newPathPoints.add(convertWGS84ToGCJ02(lat, lon)); } } for (var item in outerList) { double? lng = safeToDouble(item['lng']); double? lat = safeToDouble(item['lat']); if (lng != null && lat != null) { newOuterPoints.add(convertWGS84ToGCJ02(lat, lng)); } } // 在这里更新状态(安全,因为在事件回调中) setState(() { gcjPathPoints = newPathPoints; gcjOuterPoints = newOuterPoints; if (_currentWorkMode == WorkMode.custom) gcjPathPoints = newOuterPoints; // 自定义模式不显示路径 _selectedPlot = plot; _isListBoxOpen = false; _isWorkPanelOpen = true; _isPanelOpen = false; // 关闭操作面板,专注作业界面 }); // 移动地图到中心 List allPoints = []; allPoints.addAll(gcjPathPoints); allPoints.addAll(gcjOuterPoints); if (allPoints.isNotEmpty) { moveMapToPointsCenter(allPoints); } } } } catch (e) { print('解析/转换坐标失败:$e'); setState(() { gcjPathPoints = []; gcjOuterPoints = []; _selectedPlot = plot; _isListBoxOpen = false; _isWorkPanelOpen = true; _isPanelOpen = false; // 关闭操作面板,专注作业界面 }); } } else { setState(() { gcjPathPoints = []; gcjOuterPoints = []; _selectedPlot = plot; _isListBoxOpen = false; _isWorkPanelOpen = true; _isPanelOpen = false; // 关闭操作面板,专注作业界面 }); } } else { setState(() { gcjPathPoints = []; gcjOuterPoints = []; _selectedPlot = plot; _isListBoxOpen = false; _isWorkPanelOpen = true; }); } }, child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all( color: _selectedPlot?.id == plot.id ? Colors.blue : Colors.grey[100]!, width: _selectedPlot?.id == plot.id ? 2 : 1, // 选中项高亮边框 ), ), child: Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( plot.imageUrl, width: 30, height: 30, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( width: 30, height: 30, color: Colors.grey[200], child: const Icon(Icons.image_outlined, color: Colors.grey), ); }, ), ), const SizedBox(width: 10), Expanded( child: Text( plot.plotName, style: const TextStyle(fontSize: 16, color: Colors.black87), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), IconButton( onPressed: () => _showDeleteConfirmDialog(plot, onDelete), icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), ); } /// 开始作业 void _startWork() async { if (gcjPathPoints.isEmpty) return; setState(() { _workStatus = WorkStatus.working; }); // 步骤1:将 List 转换为 List final List pathModels = gcjPathPoints.map((latLng) { return work_area_model.DeviceAddPathPointModel( latitude: latLng.latitude, // 纬度 longitude: latLng.longitude, // 经度 ); }).toList(); // 步骤2:将 List 转换为 Queue(队列) final Queue pathQueue = Queue.from(pathModels); // 步骤3:调用 Cubit 方法(类型匹配) await context.read().startRoutePlanning(pathQueue); debugPrint('开始作业:${_selectedPlot!.plotName},路径数据:$gcjPathPoints'); // 可选:显示作业提示 ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已开始'), backgroundColor: Colors.green)); } /// 暂停作业 void _pauseWork() async { setState(() { _workStatus = WorkStatus.paused; }); await context.read().pauseRoutePlanning(); // 模拟暂停逻辑(替换为你的实际暂停代码) debugPrint('暂停作业:${_selectedPlot!.plotName}'); } void _stopWork() async { setState(() { _workStatus = WorkStatus.idle; }); await context.read().stopRoutePlanning(); // 模拟停止逻辑(替换为你的实际停止代码) debugPrint('停止作业:${_selectedPlot!.plotName}'); // 可选:显示停止提示 ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已停止'), backgroundColor: Colors.redAccent)); } /// 继续作业 void _resumeWork() async { setState(() { _workStatus = WorkStatus.working; }); await context.read().resumeRoutePlanning(); } void _showDeleteConfirmDialog(PlotData plot, Function(PlotData) onDelete) { ConfirmDialog.show( context: context, title: '确认删除', content: '是否删除地块「${plot.plotName}」?删除后不可恢复。', confirmText: '删除', confirmTextColor: Colors.red, onConfirm: () { onDelete(plot); }, ); } Widget _buildWorkPanel() { return BlocBuilder( builder: (context, state) { // 无选中地块或无路径数据时返回空 if (_selectedPlot == null || state.pathData == null) { return const SizedBox.shrink(); } return Positioned( left: 0, right: 0, bottom: 0, child: Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, -2))], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 1. 地块基础信息 Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( _selectedPlot!.imageUrl, width: 80, height: 80, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( width: 80, height: 80, color: Colors.grey[200], child: const Icon(Icons.image_outlined, color: Colors.grey, size: 32), ); }, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _selectedPlot!.plotName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text('作业模式:$workMode', style: const TextStyle(fontSize: 14, color: Colors.grey)), ], ), ), IconButton( onPressed: () { setState(() { _isWorkPanelOpen = false; _selectedPlot = null; gcjPathPoints = []; gcjOuterPoints = []; }); }, icon: const Icon(Icons.close, color: Colors.grey, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 24, minHeight: 24), ), ], ), const SizedBox(height: 16), // 2. 错误提示(如果加载失败) if (state.errorMessage != null) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.red[200]!), ), child: Row( children: [ const Icon(Icons.error_outline, color: Colors.redAccent, size: 16), const SizedBox(width: 8), Expanded( child: Text( state.errorMessage!, style: const TextStyle(fontSize: 14, color: Colors.redAccent), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), // 3. 开始作业按钮(加载中禁用) // 放在 Column/Row 的 children: [ ... ] 内部 _workStatus == WorkStatus.idle ? SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: state.isLoading || gcjPathPoints.isEmpty ? null : () { // 开始作业逻辑 _startWork(); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00C853), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), elevation: 2, disabledBackgroundColor: Colors.grey[300], disabledForegroundColor: Colors.grey[600], ), child: state.isLoading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Text('开始作业', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), ), ) : Row( children: [ // 暂停/继续图标按钮 Expanded( child: SizedBox( width: 50, height: 50, child: ElevatedButton( onPressed: () => _workStatus == WorkStatus.working ? _pauseWork() : _resumeWork(), style: ElevatedButton.styleFrom( backgroundColor: _workStatus == WorkStatus.working ? Colors.amber : const Color(0xFF00C853), foregroundColor: Colors.white, shape: const CircleBorder(), elevation: 2, // 调整内边距,让图标居中更协调 padding: const EdgeInsets.symmetric(horizontal: 8), ), child: Icon( // 作业中显示暂停图标,暂停中显示播放(继续)图标 _workStatus == WorkStatus.working ? Icons.pause : Icons.play_arrow, size: 24, // 图标尺寸 ), ), ), ), const SizedBox(width: 12), // 停止图标按钮 Expanded( child: SizedBox( width: 50, height: 50, child: ElevatedButton( onPressed: () => _stopWork(), style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, foregroundColor: Colors.white, shape: const CircleBorder(), elevation: 2, padding: const EdgeInsets.symmetric(horizontal: 8), ), child: const Icon( Icons.stop, // 停止图标(也可以用 Icons.close) size: 24, ), ), ), ), ], ), //SizedBox( // width: double.infinity, // height: 50, // child: ElevatedButton( // onPressed: state.isLoading || gcjPathPoints.isEmpty // ? null // : () { // // 作业逻辑:使用加载的pathData // debugPrint('开始作业:${_selectedPlot!.plotName},路径数据:$gcjPathPoints'); // }, // style: ElevatedButton.styleFrom( // backgroundColor: const Color(0xFF00C853), // foregroundColor: Colors.white, // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), // elevation: 2, // disabledBackgroundColor: Colors.grey[300], // disabledForegroundColor: Colors.grey[600], // ), // child: state.isLoading // ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) // : const Text('开始作业', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), // ), //), ], ), ), ); }, ); } Future _captureMapToJpgBase64({int quality = 80}) async { try { // 1. 第一步:截取 PNG (Flutter 唯一支持的格式) RenderRepaintBoundary? boundary = _mapRepaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) { debugPrint('截图失败:渲染对象未找到'); return null; } // 🔥 注意:这里是 dart:ui 的 Image,需显式指定 ui.Image ui.Image image = await boundary.toImage(pixelRatio: 2.0); ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { debugPrint('截图失败:字节数据为空'); return null; } Uint8List pngBytes = byteData.buffer.asUint8List(); // 2. 第二步:使用 image 库解码 // 🚨 核心修复:变量类型必须显式写为 _image.Image? _image.Image? decodedImage = _image.decodeImage(pngBytes); if (decodedImage == null) { debugPrint('转换失败:无法解码 PNG'); return null; } // 3. 第三步:编码为 JPG List jpgBytes = _image.encodeJpg(decodedImage, quality: quality); Uint8List finalBytes = Uint8List.fromList(jpgBytes); // 4. 第四步:转换为 Base64 String base64Str = base64Encode(finalBytes); debugPrint('JPG Base64 长度:${base64Str.length} (体积更小)'); return base64Str; } catch (e) { debugPrint('生成 JPG Base64 异常:$e'); return null; } } // 🔥 改动4:新增方法 - 将Bloc的workRecords转换为PlotData列表 List _convertWorkRecordsToPlotData(List> records) { return records.map((record) { return PlotData( id: record['id']?.toString() ?? DateTime.now().microsecondsSinceEpoch.toString(), // 唯一ID plotName: record['workName'] ?? '未命名地块', // 地块名称(从接口字段取) imageUrl: record['imgUrl'] ?? '', // 图片URL(从接口字段取,无则为空) jsonData: record['jsonData'], // 原始数据的JSON字符串(可选,便于调试或后续使用) ); }).toList(); } // ========== 抽象:生成路径的核心函数 ========== Future _generatePath({bool showTips = true}) async { if (_currentWorkMode == WorkMode.custom) { setState(() { _saveBoxOpen = true; // 显示保存按钮 _isWorkAreaCompleted = true; // 标记为完成,允许保存 gcjPathPoints = List.from(_markedPoints); // outer直接使用打点坐标 gcjOuterPoints = List.from(_markedPoints); typedPathList = []; // 路径列表置空 }); return; } // 1. 校验必要条件 if (_currentAreaMode == AreaMode.work && _markedPoints.isEmpty) { if (showTips) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先添加作业区域打点!'), backgroundColor: Colors.red)); } return; } if (_currentAreaMode == AreaMode.work && _markedPoints.length < 3) { if (showTips) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业区域至少需要3个打点才能生成路径!'), backgroundColor: Colors.red)); } return; } if (_currentWorkMode == null) { if (showTips) { 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.2 航向角 final heading = _angle == -1 ? -1 : _angle.toInt(); // 2.3 外边界(作业区域) final outerBoundary = work_area_model.OuterBoundary( position: _markedPoints.map((latLng) { final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude); return work_area_model.Position(lat: wgs84Point.latitude, lon: wgs84Point.longitude); }).toList(), sideWidth: _workDistance, ); // ========== 核心新增:转换障碍物为holes参数 ========== final holes = []; if (_obstacleHoles.isNotEmpty) { for (var holePoints in _obstacleHoles) { // 转换每组障碍物点为WGS84坐标系 final holePosition = holePoints.map((latLng) { final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude); return work_area_model.Position(lat: wgs84Point.latitude, lon: wgs84Point.longitude); }).toList(); // 添加到holes数组(sideWidth固定为0) holes.add(work_area_model.HoleBoundary(position: holePosition, sideWidth: 0.0)); } } try { // 将holes转换为普通Map,便于JSON序列化 final holesJson = holes.map((hole) { return { 'sideWidth': hole.sideWidth, 'position': hole.position.map((p) => {'lat': p.lat, 'lon': p.lon}).toList(), }; }).toList(); // 格式化打印JSON(带缩进,更易读) debugPrint('holes参数(JSON格式):\n${const JsonEncoder.withIndent(' ').convert(holesJson)}'); } catch (e) { debugPrint('holes转JSON失败:$e'); } final workType = _currentWorkMode == WorkMode.bow ? 0 : 2; // 3. 调用 Cubit 的 generatePath 方法(传入holes参数) await context.read().generatePath( reference: referencePoint, heading: heading, outer: outerBoundary, holes: holes, // 传入障碍物参数 workType: workType, ); // 4. 处理生成结果(原有逻辑不变) final cubitState = context.read().state; print('${cubitState.generatedPath},路径生成结果'); if (cubitState.errorMessage != null) { if (showTips) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成失败:${cubitState.errorMessage}'), backgroundColor: Colors.red)); } } else if (cubitState.generatedPath != null) { if (showTips) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('路径生成成功!'), backgroundColor: Colors.green)); } debugPrint('生成的路径数据:${cubitState.generatedPath}'); if (cubitState.generatedPath is List) { setState(() { _isWorkAreaCompleted = true; _saveBoxOpen = true; final pathList = cubitState.generatedPath as List; typedPathList = pathList.whereType().toList(); debugPrint('✅ 过滤并转换后的路径点列表:typedPathList 长度=${typedPathList.length}'); //打印 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); } } } catch (e) { debugPrint('路径生成异常:$e'); if (showTips) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成异常:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red)); } } } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56 final maxTop = screenHeight - menuHeight; return Scaffold( body: SafeArea( child: Stack( children: [ RepaintBoundary( key: _mapRepaintKey, child: FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: _currentLatLng ?? const LatLng(39.9042, 116.4074), initialZoom: 20, maxZoom: 22, // 禁止地图点击事件(避免和中心标冲突) onTap: (_, __) {}, // 空实现,禁用地图点击响应 ), children: [ /// 高德瓦片(GCJ-02) TileLayer( urlTemplate: 'https://webrd02.is.autonavi.com/appmaptile' '?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1' '&key=bbb1f0f20eed6bf679eddf2625630aba', ), // 绘制path折线(Line模式) if (gcjPathPoints.isNotEmpty) PolylineLayer( polylines: [ Polyline( points: gcjPathPoints, // 转换后的GCJ02坐标 color: const Color.fromARGB(255, 223, 228, 116), // 折线颜色(可自定义) strokeWidth: 1.0, // 折线宽度 isDotted: false, // 非虚线(Line模式) borderColor: Colors.white, // 可选:添加白色描边,提升辨识度 borderStrokeWidth: 0.5, ), ], ), // 绘制outer边框(Polygon模式) if (gcjOuterPoints.isNotEmpty && _currentWorkMode == WorkMode.bow) PolygonLayer( polygons: [ Polygon( points: gcjOuterPoints, // 转换后的GCJ02坐标 color: Colors.green.withOpacity(0.1), // 内部填充色(透明) borderColor: Colors.green, // 边框颜色 borderStrokeWidth: 1.0, // 边框宽度 isFilled: true, // 开启填充(即使透明,也需要开启才能显示边框) ), ], ), if (gcjPathPoints.isNotEmpty && _currentWorkMode == WorkMode.custom) PolylineLayer( polylines: [ Polyline( points: gcjPathPoints, // 转换后的GCJ02坐标 color: Colors.green, // 内部填充色(透明) strokeWidth: 3.0, // 折线宽度 isDotted: false, // 非虚线(Line模式) borderColor: Colors.white, // 可选:添加白色描边,提升辨识度 borderStrokeWidth: 0.5, ), ], ), /// 中心标与历史打点的虚线连线 if (!_isWorkAreaCompleted) PolylineLayer( polylines: [ for (int i = 0; i < _markedPoints.length - 1; i++) Polyline(points: [_markedPoints[i], _markedPoints[i + 1]], color: Colors.orange.withOpacity(0.5), strokeWidth: 1.5), ], ), if (_markedPoints.isNotEmpty && !_isWorkAreaCompleted) PolylineLayer( polylines: [ Polyline(points: [_mapCenter, _markedPoints.last], color: Colors.blue.withOpacity(0.5), strokeWidth: 1.5), ], ), /// 当前定位 Marker if (_currentLatLng != null) MarkerLayer( markers: [ Marker( point: _currentLatLng!, width: 40, height: 40, child: CustomPaint( size: const Size(40, 40), painter: HeadingMarkerPainter(headingAngle: _headingAngle), ), ), ], ), if (_currentWorkMode == WorkMode.bow && _markedPoints.isNotEmpty && !_isWorkAreaCompleted) PolygonLayer( polygons: [ Polygon( points: _getPolygonPoints(), color: Colors.green.withOpacity(0.2), borderColor: Colors.green.withOpacity(0.5), borderStrokeWidth: 1, isFilled: true, ), ], ), if (_obstacleHoles.isNotEmpty) PolygonLayer( polygons: _obstacleHoles.map((holePoints) { return Polygon( points: holePoints, color: Colors.red.withOpacity(0.2), // 红色半透明填充 borderColor: Colors.red, // 红色边框 borderStrokeWidth: 1.5, isFilled: true, ); }).toList(), ), // 2. 正在绘制的障碍物打点连线(红色虚线) if (_isObstacleEditing && _currentObstaclePoints.isNotEmpty) PolylineLayer( polylines: [ // 已绘制的障碍物点连线 for (int i = 0; i < _currentObstaclePoints.length - 1; i++) Polyline( points: [_currentObstaclePoints[i], _currentObstaclePoints[i + 1]], color: Colors.red.withOpacity(0.8), strokeWidth: 1.5, isDotted: true, // 虚线区分作业区域 ), // 最后一个点到地图中心的连线 Polyline(points: [_mapCenter, _currentObstaclePoints.last], color: Colors.red.withOpacity(0.5), strokeWidth: 1.5, isDotted: true), ], ), // 3. 障碍物打点标记(红色) if (_currentAreaMode == AreaMode.obstacle) MarkerLayer( markers: [ // 已完成的障碍物打点 for (var holeIndex = 0; holeIndex < _obstacleHoles.length; holeIndex++) for (var pointIndex = 0; pointIndex < _obstacleHoles[holeIndex].length; pointIndex++) Marker( point: _obstacleHoles[holeIndex][pointIndex], width: 80, height: 40, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 16, height: 16, decoration: const BoxDecoration( color: Colors.red, // 红色标记区分作业区域 shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 2)], ), child: Center( child: Text( '${holeIndex + 1}-${pointIndex + 1}', // 格式:组号-点号 style: const TextStyle(color: Colors.white, fontSize: 8, fontWeight: FontWeight.bold), ), ), ), ], ), ), // 正在绘制的障碍物打点 for (var pointIndex = 0; pointIndex < _currentObstaclePoints.length; pointIndex++) Marker( point: _currentObstaclePoints[pointIndex], width: 80, height: 40, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 16, height: 16, decoration: const BoxDecoration( color: Colors.orange, // 橙色标记区分正在绘制 shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 2)], ), child: Center( child: Text( '${pointIndex + 1}', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ), ), ], ), ), ], ), /// 历史打点的绿色标记 MarkerLayer( markers: _markedPoints.asMap().entries.map((entry) { int index = entry.key + 1; // 打点序号(从1开始) LatLng point = entry.value; return Marker( point: point, width: 80, height: 40, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), // 向下偏移8px(抵消默认的顶部对齐) // 绿色打点标记 Container( width: 16, height: 16, decoration: const BoxDecoration( color: Color(0xFF00C853), // 绿色主题色 shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 2)], ), child: Center( child: Text( '$index', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ), ), ], ), ); }).toList(), ), ], ), ), // 地图核心组件 if (_isRefreshing) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.3), child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator(color: Colors.white, strokeWidth: 3)], ), ), ), ), // 悬浮返回按钮(核心修改) Positioned( top: 10, left: 10, child: GestureDetector( // 自定义点击事件 onTap: _navigateBack, // 自定义点击反馈(替代 IconButton 的高亮/水波纹) child: Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 3, offset: const Offset(0, 2))], ), // 核心:用 Stack 实现点击高亮层 + 图标层 child: Stack( alignment: Alignment.center, // 所有子组件居中 children: [ // 1. 点击高亮层(默认隐藏,点击时显示) Positioned.fill( child: Container( decoration: BoxDecoration( color: Colors.transparent, // 默认透明 borderRadius: BorderRadius.circular(20), ), ), ), // 2. 图标层(绝对居中) const Icon(Icons.arrow_back_ios, color: Colors.black87, size: 20), ], ), ), ), ), /// 核心:地图正中心固定定位标 Positioned( left: 0, right: 0, top: 0, bottom: 0, child: Center( // 自定义十字标(中心精准对齐地图中心) child: SizedBox( width: 16, // 十字整体宽度 height: 16, // 十字整体高度 child: CustomPaint( painter: CrosshairPainter(), // 自定义十字画笔 ), ), ), ), //保存按钮 if (_saveBoxOpen) Positioned( right: 80, top: maxTop > 10 ? 11 : maxTop, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: const Color(0xFF00C853), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4, offset: const Offset(0, 2))], ), child: IconButton( onPressed: () { _showSavePlotDialog(); }, icon: const Icon(Icons.save, color: Colors.white, size: 18), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ), ), // 右侧悬浮菜单 Positioned( height: 336, right: 16, top: maxTop > 10 ? 11 : maxTop, child: FloatingActionButton( onPressed: _moveToCurrentLocation, child: new VerticalFloatMenu( onEditTap: (bool isOpen) { setState(() { _isPanelOpen = isOpen; }); }, onListBox: (bool isOpen) { // 🔥 改动5:加载作业记录(原有逻辑保留) final userId = context.read().state.user?.userId ?? ""; print('加载作业记录,当前用户ID:$userId'); context.read().loadWorkRecords(userId); setState(() { _isListBoxOpen = isOpen; _isWorkPanelOpen = false; }); }, onWorkModeSelected: (mode) { _currentWorkMode = mode; debugPrint('外部收到作业模式:$mode'); }, onRefreshTap: _handleRefresh, // 推荐用局部刷新 ), ), ), // 底部操作面板 if (_isPanelOpen) Positioned( left: 0, right: 0, bottom: 0, child: BottomOperationPanel( initialRobotMode: RobotMode.point, initialAreaMode: AreaMode.work, initialWorkMode: _currentWorkMode!, onComplete: () async { debugPrint( '操作完成回调:当前机器人模式=$_currentRobotMode,当前区域模式=$_currentAreaMode,当前作业模式:$_currentWorkMode,作业区域点:$_markedPoints,作业行距=$_workDistance,航线方向角=$_angle', ); if (_currentAreaMode == AreaMode.obstacle) { // 障碍物模式:完成当前障碍物绘制 _completeObstacle(); await _generatePath(showTips: true); } else { // 作业区域模式:生成路径 await _generatePath(showTips: true); } // 5. 关闭操作面板 setState(() { //_isPanelOpen = false; // 关闭操作面板 }); }, onUndoTap: () { // 区分模式:作业区域撤回/障碍物撤回 if (_currentAreaMode == AreaMode.work) { _undoLastPoint(); } else { _undoObstaclePoint(); } }, onDeleteTap: () { // 区分模式:作业区域清空/障碍物清空 if (_currentAreaMode == AreaMode.work) { _deleteAllPoints(); } else { _clearAllObstacles(); } }, onRobotModeChanged: (mode) { setState(() => _currentRobotMode = mode); debugPrint('外部收到机器人模式:$mode'); }, onAreaModeChanged: (mode) { setState(() => _currentAreaMode = mode); debugPrint('外部收到区域模式:$mode'); }, onAddTap: () { _addMarkedPoint(); debugPrint('外部处理加号按钮点击,已添加打点'); }, onDistanceTap: (distance) { _workDistance = distance; debugPrint('外部处理作业行距距离设置,当前距离:${distance.toStringAsFixed(1)}米'); if (_markedPoints.length >= 3 && _currentWorkMode != null) { _generatePath(showTips: false); } }, onSettingTap: () { setState(() { _directionBoxOpen = true; }); debugPrint('外部处理设置按钮点击'); }, onLandTap: () { debugPrint('外部处理地块标签点击'); }, onRouteTap: () { debugPrint('外部处理航线标签点击'); }, ), ), // 航线方向面板 if (_directionBoxOpen) Positioned( left: 0, right: 0, bottom: 0, child: RouteDirectionPanel( initialOptimalHeading: true, initialDirection: 0.0, onValueChanged: (result) { debugPrint('最优航向:${result['optimalHeading']},角度:${result['direction']}'); _angle = result['optimalHeading'] == true ? -1 : result['direction']; debugPrint('外部处理航线方向设置,当前角度:${_angle}'); if (_markedPoints.length >= 3 && _currentWorkMode != null) { debugPrint('外部处理航线方向设置11222:${_angle}'); _generatePath(showTips: false); } }, onCancel: () { setState(() => _directionBoxOpen = false); }, ), ), // 列表面板 - 使用BlocBuilder监听DevicesCubit状态 if (_isListBoxOpen) BlocBuilder( builder: (context, state) { final plotList = _convertWorkRecordsToPlotData(state.workRecords ?? []); return Positioned.fill( child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Expanded( child: GestureDetector( onTap: () { setState(() { _isListBoxOpen = false; }); }, child: Container(color: Colors.black.withOpacity(0.3)), ), ), Container( width: double.infinity, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, -2))], ), constraints: const BoxConstraints(maxHeight: 600, minHeight: 200), child: Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '地块列表(共${plotList.length}条)', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87), ), IconButton( onPressed: () { setState(() { _isListBoxOpen = false; }); }, icon: const Icon(Icons.close, color: Colors.grey, size: 20), ), ], ), ), const Divider(height: 1, color: Colors.grey), Expanded( child: plotList.isEmpty ? const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inbox_outlined, color: Colors.grey, size: 48), SizedBox(height: 16), Text('暂无地块数据', style: TextStyle(color: Colors.grey, fontSize: 16)), ], ), ) : ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: plotList.length, itemBuilder: (context, index) { final plot = plotList[index]; // 传入删除回调(调用Bloc的删除方法) return _buildPlotListItem(plot, (deletedPlot) async { await context.read().deleteWorkRecord(deletedPlot.plotName); final userId = context.read().state.user?.userId ?? ""; await context.read().loadWorkRecords(userId); setState(() {}); }); }, ), ), ], ), ), ], ), ); }, ), if (_isWorkPanelOpen) _buildWorkPanel(), ], ), ), ); } } // 转换单个WGS84坐标到GCJ02 LatLng convertWGS84ToGCJ02(double lat, double lon) { return wgs84ToGcj02(lat, lon); } /// ======================================================= /// 坐标转换:WGS84 -> GCJ02(国内高德/腾讯通用) /// ======================================================= const double _pi = 3.14159265358979324; const double _a = 6378245.0; const double _ee = 0.00669342162296594323; LatLng wgs84ToGcj02(double lat, double lon) { if (_outOfChina(lat, lon)) { return LatLng(lat, lon); } double dLat = _transformLat(lon - 105.0, lat - 35.0); double dLon = _transformLon(lon - 105.0, lat - 35.0); double radLat = lat / 180.0 * _pi; double magic = sin(radLat); magic = 1 - _ee * magic * magic; double sqrtMagic = sqrt(magic); dLat = (dLat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrtMagic) * _pi); dLon = (dLon * 180.0) / (_a / sqrtMagic * cos(radLat) * _pi); double mgLat = lat + dLat; double mgLon = lon + dLon; return LatLng(mgLat, mgLon); } LatLng gcj02ToWgs84(double lat, double lon) { if (_outOfChina(lat, lon)) { return LatLng(lat, lon); } double dLat = _transformLat(lon - 105.0, lat - 35.0); double dLon = _transformLon(lon - 105.0, lat - 35.0); double radLat = lat / 180.0 * _pi; double magic = sin(radLat); magic = 1 - _ee * magic * magic; double sqrtMagic = sqrt(magic); dLat = (dLat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrtMagic) * _pi); dLon = (dLon * 180.0) / (_a / sqrtMagic * cos(radLat) * _pi); double mgLat = lat - dLat; double mgLon = lon - dLon; return LatLng(mgLat, mgLon); } bool _outOfChina(double lat, double lon) { return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271; } double _transformLat(double x, double y) { double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(x.abs()); ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0; ret += (20.0 * sin(y * _pi) + 40.0 * sin(y / 3.0 * _pi)) * 2.0 / 3.0; ret += (160.0 * sin(y / 12.0 * _pi) + 320 * sin(y * _pi / 30.0)) * 2.0 / 3.0; return ret; } double _transformLon(double x, double y) { double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(x.abs()); ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0; ret += (20.0 * sin(x * _pi) + 40.0 * sin(x / 3.0 * _pi)) * 2.0 / 3.0; ret += (150.0 * sin(x / 12.0 * _pi) + 300.0 * sin(x / 30.0 * _pi)) * 2.0 / 3.0; return ret; }