Files
flutterApp/lib/features/home/presentation/widgets/map/testmap_pages.dart

2531 lines
106 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:collection';
2026-02-26 14:08:02 +08:00
import 'dart:convert';
import 'dart:math';
2026-03-01 16:13:37 +08:00
import 'dart:math' as math;
2026-03-01 15:11:12 +08:00
import 'dart:typed_data';
import 'package:flutter/material.dart';
2026-03-01 15:11:12 +08:00
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart';
2026-03-10 15:58:52 +08:00
import 'package:go_router/go_router.dart';
2026-03-01 15:11:12 +08:00
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart';
import 'package:maibu_satabot_v2/components/confrim_dialog.dart';
2026-03-03 10:28:50 +08:00
import 'package:maibu_satabot_v2/components/toast.dart';
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
2026-03-05 15:46:41 +08:00
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
2026-02-26 14:08:02 +08:00
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';
2026-03-10 15:58:52 +08:00
import 'package:maibu_satabot_v2/core/router/route_paths.dart';
2026-02-28 17:06:14 +08:00
import 'package:maibu_satabot_v2/features/devices/data/models/device_add_path_point_model.dart' as work_area_model;
2026-02-28 16:20:21 +08:00
import 'package:maibu_satabot_v2/features/devices/data/models/device_work_area_param_model.dart' as work_area_model;
2026-02-28 09:55:14 +08:00
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';
2026-02-28 09:55:14 +08:00
import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_record_usecase.dart';
2026-03-06 17:19:26 +08:00
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_event.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.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';
2026-02-25 20:00:33 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirectionLine.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/commonFn.dart';
2026-02-25 17:22:59 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart';
2026-02-28 16:20:21 +08:00
import 'package:maibu_satabot_v2/components/input_confirm_dialog.dart';
2026-02-26 14:08:02 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/tracepoint.dart';
2026-02-25 20:36:46 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/CenterLocation.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/HeadingPointer.dart';
2026-03-10 15:06:28 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/dji_tile_layer.dart';
2026-03-06 17:19:26 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/obsToast.dart';
2026-02-25 17:22:59 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/startpoint_area.dart';
2026-03-01 15:11:12 +08:00
import 'package:image/image.dart' as _image; // 注意命名空间冲突,使用 as img
2026-03-05 15:46:41 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/video.dart';
2026-03-10 17:04:43 +08:00
import 'package:shared_preferences/shared_preferences.dart';
2026-03-01 15:11:12 +08:00
import 'dart:ui' as ui;
import '../path_list_pages.dart';
2026-02-26 14:08:02 +08:00
// 定义轨迹点类型(经纬度)
typedef PlotPoint = LatLng;
2026-03-10 17:04:43 +08:00
// 本地存储Key常量
const String kSavedPlotData = 'saved_plot_data';
const String kSavedGcjPathPoints = 'saved_gcj_path_points';
const String kSavedGcjOuterPoints = 'saved_gcj_outer_points';
const String kSavedMarkedPoints = 'saved_marked_points';
const String kSavedObstacleHoles = 'saved_obstacle_holes';
const String kSavedIsWorkAreaCompleted = 'saved_is_work_area_completed';
2026-02-26 14:08:02 +08:00
// 保持 PlotData 类不变
2026-02-26 10:44:56 +08:00
class PlotData {
final String id;
final String plotName;
2026-02-26 10:44:56 +08:00
final String imageUrl; // 图片URL(本地/assets/网络都可)
final String? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用)
2026-02-28 16:20:21 +08:00
PlotData({required this.id, required this.plotName, required this.imageUrl, this.jsonData});
2026-02-26 10:44:56 +08:00
}
class PlotDataPath {
final dynamic? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用)
PlotDataPath({this.jsonData});
}
class MapPageEnterprise extends StatefulWidget {
const MapPageEnterprise({Key? key}) : super(key: key);
@override
State<MapPageEnterprise> createState() => _MapPageEnterpriseState();
}
class _MapPageEnterpriseState extends State<MapPageEnterprise> {
2026-02-26 14:08:02 +08:00
final _dispatcher = sl<NetMessageDispatcher>();
2026-02-28 14:33:55 +08:00
bool _isRefreshing = false; // 新增:页面刷新状态标志
2026-03-05 15:46:41 +08:00
bool _isVideoDialogOpen = false; // 控制视频弹窗显示
String _videoStreamUrl = "";
Offset _videoPopupPos = const Offset(0, 100); // 弹窗初始位置
2026-03-01 15:11:12 +08:00
// 🔥 关键修复:添加截图全局Key
final GlobalKey _mapRepaintKey = GlobalKey();
2026-03-06 17:19:26 +08:00
// 4. 标记点集合(flutter_map需要Set类型)
Set<Marker> _markers = {};
2026-02-26 14:08:02 +08:00
final MapController _mapController = MapController();
2026-02-26 14:08:02 +08:00
// 初始化轨迹管理器(泛型指定为LatLng)
late final TracePoint<PlotPoint> _traceManager;
2026-02-28 15:50:31 +08:00
late String workMode = "弓字模式"; // 作业模式:默认值为"弓字模式"
2026-03-05 15:46:41 +08:00
2026-03-02 09:31:46 +08:00
WorkStatus _workStatus = WorkStatus.idle;
2026-03-06 17:19:26 +08:00
bool isStartWork = false; //是否开始作业
2026-03-07 21:21:31 +08:00
LatLng? _currentLatLng; //gc格式
LatLng? _currentWgsLatLng;
2026-02-25 17:22:59 +08:00
bool _isPanelOpen = false; // 控制面板显示/隐藏
2026-02-25 20:00:33 +08:00
bool _directionBoxOpen = false; //航线面板显示/隐藏
bool _saveBoxOpen = false; //保存按钮显示/隐藏
2026-02-26 10:44:56 +08:00
bool _isListBoxOpen = false; //列表面板显示/隐藏
bool _showHeadingWarn = false; // 航向角未初始化提示
bool _showControlModeWarn = false; // 远程模式提示
2026-02-26 14:08:02 +08:00
// 新增:存储选中的地块(null表示未选中)
PlotData? _selectedPlot;
PlotDataPath? _selectedPlotPath; // 存储选中地块的路径数据(可选)
2026-02-26 14:08:02 +08:00
// 新增:控制底部作业面板显示
bool _isWorkPanelOpen = false;
bool _isWorkAreaCompleted = false;
2026-03-09 11:06:43 +08:00
RobotMode _currentRobotMode = RobotMode.robot; //打点模式
2026-02-25 20:00:33 +08:00
AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域
WorkMode? _currentWorkMode = WorkMode.bow; // 当前作业模式
// 新增:存储转换后的path和outer坐标
List<LatLng> gcjPathPoints = [];
List<LatLng> gcjOuterPoints = [];
2026-02-25 20:00:33 +08:00
2026-03-11 09:15:44 +08:00
List<dynamic> startWorkList = [];
2026-03-06 17:19:26 +08:00
List<PlotPoint>? tracePoint = []; //从tracepoint中获取的经纬度坐标
List<PlotPoint>? gctracePoint = [];
2026-02-25 20:09:18 +08:00
// ========== 核心新增状态 ==========
2026-03-10 17:04:43 +08:00
List<LatLng> _markedPoints = []; // 存储所有s十字准星打点坐标
2026-03-07 21:21:31 +08:00
List<LatLng> _robotModeWgsPoints = []; // Robot模式下从设备获取的wgs打点
List<LatLng> _robotModeObsWgsPoints = []; // Robot模式下从设备获取的wgs打点 障碍物
2026-02-26 10:44:56 +08:00
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
2026-03-06 17:19:26 +08:00
double _headingAngle = 0.0; // 当前机器航向角(单位:度)
2026-03-01 15:11:12 +08:00
List typedPathList = <work_area_model.DeviceAddPathPointModel>[]; // 存储生成路径的坐标列表(已转换为LatLng)
2026-02-26 09:17:14 +08:00
2026-03-02 13:25:21 +08:00
// ========== 障碍物模式核心状态 ==========
2026-03-09 11:06:43 +08:00
List<List<LatLng>> _obstacleHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个障碍物)
2026-03-07 21:21:31 +08:00
2026-03-09 11:06:43 +08:00
List<List<LatLng>> _obstacleWgsHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个障碍物)
2026-03-02 13:25:21 +08:00
List<LatLng> _currentObstaclePoints = []; // 当前正在绘制的障碍物打点
bool _isObstacleEditing = false; // 是否处于障碍物编辑状态
2026-02-26 09:17:14 +08:00
//打点
double _workDistance = 0.0; // 作业行距(单位:米)
double _angle = 0.0; // 航线方向角(单位:度)
2026-03-10 15:06:28 +08:00
// 大疆地图
bool isDjMapShow = false;
late DJIStation _currentStation;
// 所有可用的场站列表(根据你的实际瓦片目录配置)
final List<DJIStation> _stationList = [
const DJIStation(name: 'liaoning', displayName: '一号场站'),
const DJIStation(name: 'station_b', displayName: '二号场站'),
const DJIStation(name: 'station_c', displayName: '三号场站'),
// 新增场站只需在这里添加,无需修改其他逻辑
];
// 其他
@override
void initState() {
super.initState();
2026-03-10 15:06:28 +08:00
_currentStation = _stationList.first;
2026-02-26 14:08:02 +08:00
_traceManager = TracePoint<PlotPoint>();
_initLocationEnterprise();
2026-03-10 17:04:43 +08:00
// 新增:加载本地存储的绘制数据
_loadSavedData();
2026-02-26 14:08:02 +08:00
// 示例:切换到导航模式
_traceManager.setMode(TPMode.LOCATION);
// 🔥 核心修复:使用 addPostFrameCallback 延迟获取地图中心(渲染完成后执行)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _mapController.camera != null) {
final originalCenter = _mapController.camera!.center;
final highPrecisionCenter = LatLng(originalCenter.latitude, originalCenter.longitude);
setState(() {
_mapCenter = highPrecisionCenter;
});
} else {
// 兜底:使用默认坐标初始化地图中心
final defaultCenter = _getDefaultValidCenter();
setState(() {
_mapCenter = defaultCenter;
});
}
});
2026-02-25 20:09:18 +08:00
// 监听地图移动事件,实时更新连线
_mapController.mapEventStream.listen((event) {
if (event is MapEventMove || event is MapEventMoveEnd) {
2026-02-26 10:44:56 +08:00
if (mounted && _mapController.camera != null) {
final originalCenter = _mapController.camera!.center;
final highPrecisionCenter = LatLng(originalCenter.latitude, originalCenter.longitude);
2026-02-26 10:44:56 +08:00
setState(() {
_mapCenter = highPrecisionCenter;
});
}
2026-02-25 20:09:18 +08:00
}
});
}
2026-03-11 12:54:42 +08:00
2026-03-10 17:04:43 +08:00
// 新增:手动清除本地存储数据
Future<void> _clearLocalData() async {
try {
await LocationUtils.clearCache(LocationUtils.kSavedGcjPathPoints);
await LocationUtils.clearCache(LocationUtils.kSavedGcjOuterPoints);
await LocationUtils.clearCache(LocationUtils.kSavedMarkedPoints);
await LocationUtils.clearCache(LocationUtils.kSavedObstacleHoles);
await LocationUtils.clearCache(LocationUtils.kSavedIsWorkAreaCompleted);
2026-03-10 17:04:43 +08:00
debugPrint('本地存储的绘制数据已清空');
} catch (e) {
debugPrint('清空本地数据失败:$e');
}
}
// 新增:加载本地存储数据方法
Future<void> _loadSavedData() async {
try {
final prefs = await SharedPreferences.getInstance();
// 加载路径点
final pathJson = prefs.getString(kSavedGcjPathPoints);
if (pathJson != null) {
final List<dynamic> pathList = jsonDecode(pathJson);
gcjPathPoints = pathList.map((item) => LatLng(double.parse(item['lat'].toString()), double.parse(item['lng'].toString()))).toList();
}
// 加载外边界点
final outerJson = prefs.getString(kSavedGcjOuterPoints);
if (outerJson != null) {
final List<dynamic> outerList = jsonDecode(outerJson);
gcjOuterPoints = outerList.map((item) => LatLng(double.parse(item['lat'].toString()), double.parse(item['lng'].toString()))).toList();
}
// 加载打点数据
final markedJson = prefs.getString(kSavedMarkedPoints);
if (markedJson != null) {
final List<dynamic> markedList = jsonDecode(markedJson);
_markedPoints = markedList.map((item) => LatLng(double.parse(item['lat'].toString()), double.parse(item['lng'].toString()))).toList().toList();
}
// 加载障碍物数据
final obstacleJson = prefs.getString(kSavedObstacleHoles);
if (obstacleJson != null) {
final List<dynamic> obstacleList = jsonDecode(obstacleJson);
_obstacleHoles = obstacleList.map((hole) {
return (hole as List<dynamic>).map((item) => LatLng(double.parse(item['lat'].toString()), double.parse(item['lng'].toString()))).toList();
}).toList();
}
// 加载作业区域完成状态
_isWorkAreaCompleted = prefs.getBool(kSavedIsWorkAreaCompleted) ?? false;
_saveBoxOpen = _isWorkAreaCompleted;
if (mounted) {
setState(() {});
// 移动地图到绘制内容中心
List<LatLng> allPoints = [];
allPoints.addAll(gcjPathPoints);
allPoints.addAll(gcjOuterPoints);
if (allPoints.isNotEmpty) {
moveMapToPointsCenter(allPoints);
}
}
} catch (e) {
debugPrint('加载本地数据失败:$e');
}
}
// 新增:保存绘制数据到本地
Future<void> _saveDataToLocal() async {
try {
final prefs = await SharedPreferences.getInstance();
// 保存路径点
final pathList = gcjPathPoints.map((point) => {'lat': point.latitude, 'lng': point.longitude}).toList();
prefs.setString(kSavedGcjPathPoints, jsonEncode(pathList));
// 保存外边界点
final outerList = gcjOuterPoints.map((point) => {'lat': point.latitude, 'lng': point.longitude}).toList();
prefs.setString(kSavedGcjOuterPoints, jsonEncode(outerList));
// 保存打点数据
final markedList = _markedPoints.map((point) => {'lat': point.latitude, 'lng': point.longitude}).toList();
prefs.setString(kSavedMarkedPoints, jsonEncode(markedList));
// 保存障碍物数据
final obstacleList = _obstacleHoles.map((hole) {
return hole.map((point) => {'lat': point.latitude, 'lng': point.longitude}).toList();
}).toList();
prefs.setString(kSavedObstacleHoles, jsonEncode(obstacleList));
// 保存作业区域完成状态
prefs.setBool(kSavedIsWorkAreaCompleted, _isWorkAreaCompleted);
} catch (e) {
debugPrint('保存本地数据失败:$e');
}
}
// ========== 新增:计算坐标列表的边界范围 ==========
LatLngBounds? calculateBounds(List<LatLng> points) {
if (points.isEmpty) return null;
// 使用静态方法 fromPoints 创建边界(所有latlong2版本都支持)
return LatLngBounds.fromPoints(points);
}
// ========== 新增:移动地图到坐标中心 ==========
// ========== 移动地图到坐标中心(无需修改) ==========
void moveMapToPointsCenter(List<LatLng> points) {
if (points.isEmpty || !mounted) return;
// 计算边界
final bounds = calculateBounds(points);
if (bounds == null) return;
2026-03-07 20:03:33 +08:00
final currentZoom = _mapController.camera?.zoom ?? 18.0;
_mapController.move(
bounds.center, // 边界中心点
currentZoom, // 保留当前缩放比例
);
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;
}
2026-03-07 20:03:33 +08:00
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;
}
2026-03-06 17:19:26 +08:00
bool _isValidLatLng(double lat, double lng) {
return lat != 0 && lng != 0 && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
}
void _onDeviceData(double lng, double lat, bool obfFlag, int headingStatus, controlMode) {
2026-02-26 14:08:02 +08:00
try {
2026-03-06 17:19:26 +08:00
// 1. 校验坐标有效性
if (!_isValidLatLng(lat, lng)) {
debugPrint('坐标无效:纬度=$lat, 经度=$lng,跳过处理');
return;
}
if (isStartWork && headingStatus == 0) {
if (!_showHeadingWarn) {
// 仅状态变化时更新,避免重复刷新
setState(() => _showHeadingWarn = true);
}
2026-03-06 17:19:26 +08:00
return;
} else if (isStartWork && headingStatus == 1 && _showHeadingWarn) {
// 条件不满足时隐藏提示
setState(() => _showHeadingWarn = false);
2026-03-06 17:19:26 +08:00
}
2026-03-06 17:19:26 +08:00
if (isStartWork && controlMode != "3") {
if (!_showControlModeWarn) {
// 仅状态变化时更新
setState(() => _showControlModeWarn = true);
}
} else if (isStartWork && controlMode == "3" && _showControlModeWarn) {
// 条件不满足时隐藏提示
setState(() => _showControlModeWarn = false);
2026-02-26 14:08:02 +08:00
}
2026-03-06 17:19:26 +08:00
//if (isStartWork && obfFlag) {
// ObsToastWidget.show(context: context, message: "小迈提醒您前方有障碍物哦!");
//} else {
// ObsToastWidget.dismiss();
//}
final wgsPoint = LatLng(lat, lng);
// 2. 转换为高德GCJ02坐标系
// 可选:更新航向角(如果有角度数据)
// _headingAngle = updatedState.status.heading ?? 0.0;
// 可选:插入队列
_traceManager.upsert(wgsPoint, TPAction.UPDATE);
tracePoint = _traceManager.getTracePoint();
gctracePoint = batchWgs84ToGcj02(tracePoint!);
debugPrint("$gctracePoint gctracePoint");
} catch (e) {
debugPrint('处理经纬度数据失败:$e');
2026-02-26 14:08:02 +08:00
}
}
2026-02-26 10:44:56 +08:00
void _showSavePlotDialog() {
2026-02-28 15:50:31 +08:00
showInputConfirmDialog(
2026-02-26 10:44:56 +08:00
context: context,
2026-02-28 15:50:31 +08:00
title: '保存地块', // 自定义标题
hintText: '请输入地块名称(如:北地块、一号田)', // 自定义输入提示
labelText: '地块名称', // 自定义输入框标签
confirmText: '保存', // 确认按钮文字
cancelText: '取消', // 取消按钮文字
// 输入校验器(可选)
inputValidator: (inputText) {
if (inputText.isEmpty) {
return '地块名称不能为空!';
}
if (inputText.length > 20) {
return '地块名称不能超过20个字符!';
}
return null; // 校验通过
},
2026-03-01 15:11:12 +08:00
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)),
),
);
2026-02-26 10:44:56 +08:00
2026-03-01 15:11:12 +08:00
// 2. 执行截图(核心调用)
String? imgBase64 = await _captureMapToJpgBase64(quality: 80);
// 4. 处理截图结果并保存
if (mounted) {
if (imgBase64 != null) {
_savePlotData(plotName, imgBase64); // 传入 Base64
} else {
// 截图失败时,允许空图片保存(可根据业务调整为强制失败)
_savePlotData(plotName, null);
2026-03-03 10:28:50 +08:00
ToastUtils.showWarn(context, '地块保存成功,但地图截图生成失败!');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块保存成功,但地图截图生成失败!'), backgroundColor: Colors.amber));
2026-03-01 15:11:12 +08:00
}
}
2026-02-26 10:44:56 +08:00
},
);
}
2026-03-01 15:11:12 +08:00
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('====================================');
}
2026-02-26 10:44:56 +08:00
// 3. 保存地块数据的核心方法(这里写你的实际保存逻辑)
2026-03-01 15:11:12 +08:00
// 🔥 关键修改:增加 imgBase64 参数
// 🔥 新的保存方法,严格匹配后端结构
Future<void> _savePlotData(String plotName, String? imgBase64) async {
// 1. 获取用户ID
final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
if (userId.isEmpty) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '用户ID为空,无法保存!');
2026-03-01 15:11:12 +08:00
return;
}
2026-03-11 15:49:50 +08:00
debugPrint("===打印生成的路径开始 ");
for (var i = 0; i < typedPathList.length; i++) {
debugPrint(typedPathList[i].toString());
}
debugPrint("===打印生成的路径结束");
2026-03-01 15:11:12 +08:00
// 2. 构造 SavePath 数据 (严格对应你的 JS 结构)
final Map<String, dynamic> savePath = {
2026-03-01 16:13:37 +08:00
//'img': imgBase64 ?? '', // 截图的 Base64 字符串
2026-03-01 15:11:12 +08:00
'name': plotName, // 地块名称
'path': _currentWorkMode == WorkMode.custom
2026-03-11 14:48:48 +08:00
? [] // 自定义模式 path 为空数组
: typedPathList
.map(
(point) => {
// 路径点数组
'lon': point.longitude,
'lat': point.latitude,
},
)
.toList(),
'outer':
// 核心修改:根据机器人模式选择不同的坐标源
(_currentRobotMode == RobotMode.robot ? _robotModeWgsPoints : gcjOuterPoints).map((point) {
// Robot模式:_robotModeWgsPoints本身就是WGS84坐标,无需转换
// Point模式:gcjOuterPoints是GCJ02坐标,需要转换为WGS84
final wgs84 = _currentRobotMode == RobotMode.robot
? point // Robot模式直接使用原始WGS84坐标
: gcj02ToWgs84(point.latitude, point.longitude); // Point模式转换
return {'lng': wgs84.longitude, 'lat': wgs84.latitude};
}).toList(),
2026-03-01 15:11:12 +08:00
'planModel': _currentWorkMode == WorkMode.bow ? 0 : 2, // 作业模式值
};
// 3. 构造 workRecord (包裹一层)
final Map<String, dynamic> 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 类型
),
);
2026-03-01 16:13:37 +08:00
//_debugPrintMultipartRequest(request);
2026-03-01 15:11:12 +08:00
// 7. 发送请求
try {
2026-03-03 10:28:50 +08:00
ToastUtils.showLoading(context, '正在保存地块「$plotName」...');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('正在保存地块...'), backgroundColor: Colors.blue));
2026-03-01 15:11:12 +08:00
final http.StreamedResponse response = await request.send();
final String responseBody = await response.stream.bytesToString();
debugPrint('保存地块响应体: $responseBody');
if (response.statusCode == 200) {
2026-03-03 10:28:50 +08:00
ToastUtils.showSuccess(context, '地块「$plotName」保存成功!');
2026-03-11 15:33:33 +08:00
_clearLocalData();
2026-03-03 10:28:50 +08:00
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块「$plotName」保存成功!'), backgroundColor: Colors.green));
2026-03-01 15:11:12 +08:00
// 保存成功
setState(() {
_markedPoints.clear(); // 清空打点
gcjPathPoints.clear(); // 清空路径
gcjOuterPoints.clear(); // 清空外边界
2026-03-11 08:59:33 +08:00
_robotModeWgsPoints.clear();
_isPanelOpen = false; // 关闭操作面板
_isWorkAreaCompleted = false; // 重置作业区域完成状态
2026-03-01 15:11:12 +08:00
});
} else {
throw Exception('服务器错误: ${response.statusCode}, $responseBody');
}
} catch (e) {
debugPrint('保存失败: $e');
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '保存失败: ${e.toString()}');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'), backgroundColor: Colors.red));
2026-03-01 15:11:12 +08:00
}
2026-02-26 10:44:56 +08:00
}
List<LatLng> _getPolygonPoints() {
if (_markedPoints.isEmpty || _mapController.camera == null) return [];
List<LatLng> polygonPoints = List.from(_markedPoints);
polygonPoints.add(_mapCenter);
return polygonPoints;
}
2026-03-03 10:13:32 +08:00
// ========== 统一撤回方法(适配完成后状态) ==========
void _undoAction() {
setState(() {
// 1. 作业区域模式
if (_currentAreaMode == AreaMode.work) {
2026-03-11 10:50:50 +08:00
if (_markedPoints.isNotEmpty) {
2026-03-07 21:21:31 +08:00
_robotModeWgsPoints.removeLast();
2026-03-03 10:13:32 +08:00
_markedPoints.removeLast();
2026-03-11 10:50:50 +08:00
// 作业点撤回后,若路径已生成,重新生成路径(保持同步)
2026-03-03 10:13:32 +08:00
if (_isWorkAreaCompleted && _markedPoints.length >= 3) {
_generatePath(showTips: false);
}
2026-03-11 10:50:50 +08:00
ToastUtils.showInfo(context, '已撤回作业区域最后一个点,剩余${_markedPoints.length}个点');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回作业区域最后一个点,剩余${_markedPoints.length}个点')));
2026-03-03 10:13:32 +08:00
} else {
2026-03-03 10:28:50 +08:00
ToastUtils.showInfo(context, '暂无作业区域点可撤回');
2026-03-11 10:50:50 +08:00
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可撤回')));
2026-03-03 10:13:32 +08:00
}
return;
}
2026-03-11 10:50:50 +08:00
// 2. 空洞模式(完成后优先撤回已完成的空洞)
2026-03-03 10:13:32 +08:00
if (_currentAreaMode == AreaMode.obstacle) {
if (_currentObstaclePoints.isNotEmpty) {
_currentObstaclePoints.removeLast();
2026-03-07 21:21:31 +08:00
_robotModeObsWgsPoints.removeLast();
2026-03-03 10:13:32 +08:00
if (_currentObstaclePoints.isEmpty) {
_isObstacleEditing = false;
}
2026-03-11 10:50:50 +08:00
ToastUtils.showInfo(context, '已撤回当前空洞最后一个点,剩余${_currentObstaclePoints.length}个点');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回当前空洞最后1个点,剩余${_currentObstaclePoints.length}个点')));
return; // 撤完点就结束,不执行后面的逻辑
2026-03-03 10:13:32 +08:00
}
2026-03-11 10:50:50 +08:00
// 第二步:当前空洞无点可撤 → 再撤回「上一个完整空洞」
2026-03-03 10:13:32 +08:00
if (_obstacleHoles.isNotEmpty) {
_obstacleHoles.removeLast();
2026-03-11 10:50:50 +08:00
// 撤完空洞后重新生成路径(排除该空洞)
2026-03-03 10:13:32 +08:00
if (_isWorkAreaCompleted) {
_generatePath(showTips: false);
}
2026-03-11 10:50:50 +08:00
ToastUtils.showInfo(context, '已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞')));
2026-03-03 10:13:32 +08:00
return;
}
2026-03-11 10:50:50 +08:00
// 第三步:无任何可撤内容
ToastUtils.showInfo(context, '暂无空洞可撤回');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无空洞可撤回')));
2026-03-03 10:13:32 +08:00
}
});
2026-03-10 17:04:43 +08:00
_saveDataToLocal();
2026-02-26 09:17:14 +08:00
}
2026-03-03 10:13:32 +08:00
// ========== 统一删除方法(适配完成后状态) ==========
void _deleteAllAction() {
setState(() {
// 1. 作业区域模式:清空作业点 + 重置路径
if (_currentAreaMode == AreaMode.work) {
2026-03-11 10:50:50 +08:00
if (_markedPoints.isEmpty) {
ToastUtils.showInfo(context, '暂无作业区域点可删除');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可删除')));
return;
2026-03-03 10:13:32 +08:00
}
_markedPoints.clear();
gcjPathPoints.clear();
gcjOuterPoints.clear();
2026-03-11 08:59:33 +08:00
_robotModeWgsPoints.clear();
2026-03-03 10:13:32 +08:00
_isWorkAreaCompleted = false;
_saveBoxOpen = false;
typedPathList.clear();
2026-03-03 10:28:50 +08:00
ToastUtils.showSuccess(context, '已清空所有作业区域点和路径');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已清空所有作业区域点和路径')));
2026-03-03 10:13:32 +08:00
}
2026-03-11 10:50:50 +08:00
// 2. 空洞模式:清空所有空洞 + 恢复UI + 重新生成路径
2026-03-03 10:13:32 +08:00
else if (_currentAreaMode == AreaMode.obstacle) {
if (_obstacleHoles.isEmpty && _currentObstaclePoints.isEmpty) {
2026-03-09 11:06:43 +08:00
ToastUtils.showInfo(context, '暂无障碍物可删除');
2026-03-03 10:13:32 +08:00
return;
}
_obstacleHoles.clear();
_currentObstaclePoints.clear();
2026-03-07 21:21:31 +08:00
_robotModeObsWgsPoints.clear();
2026-03-03 10:13:32 +08:00
_isObstacleEditing = false;
2026-03-11 10:50:50 +08:00
// 清空后重新生成路径(无空洞)
2026-03-03 10:13:32 +08:00
if (_isWorkAreaCompleted) {
_generatePath(showTips: false);
}
2026-03-09 11:06:43 +08:00
ToastUtils.showSuccess(context, '已清空所有障碍物');
2026-03-03 10:13:32 +08:00
}
});
2026-03-10 17:04:43 +08:00
_saveDataToLocal();
2026-02-26 09:17:14 +08:00
}
2026-03-10 08:58:44 +08:00
void _handleGotoLocation() {
_mapController.move(_currentLatLng!, 18);
}
/// ===============================
/// 企业级定位初始化(已修复坐标系)
Future<void> _initLocationEnterprise() async {
2026-03-06 17:19:26 +08:00
// 1. 设置轨迹模式为 LOCATION(核心:确保模式正确)
_traceManager.setMode(TPMode.LOCATION);
}
2026-02-28 14:33:55 +08:00
// ========== 新增:页面刷新初始化方法 ==========
Future<void> _handleRefresh() async {
// 防止重复刷新
if (_isRefreshing) return;
2026-03-09 17:30:26 +08:00
// 🔥 修复1:保存当前有效的地图中心和缩放级别(而非定位坐标)
final savedZoom = _mapController.camera?.zoom ?? 17.0;
2026-02-28 14:33:55 +08:00
setState(() {
_isRefreshing = true; // 标记开始刷新
});
try {
// 2. 重置所有页面状态(恢复初始值)
setState(() {
2026-03-06 17:19:26 +08:00
_traceManager.setMode(TPMode.LOCATION);
2026-02-28 14:33:55 +08:00
// 清空打点和轨迹数据
_markedPoints.clear();
gcjPathPoints.clear();
2026-03-07 21:21:31 +08:00
_robotModeObsWgsPoints.clear();
2026-02-28 14:33:55 +08:00
gcjOuterPoints.clear();
2026-03-11 08:59:33 +08:00
_robotModeWgsPoints.clear();
_isWorkAreaCompleted = false;
_saveBoxOpen = false;
2026-02-28 14:33:55 +08:00
// 重置面板状态
_isPanelOpen = false;
_directionBoxOpen = false;
_isListBoxOpen = false;
_isWorkPanelOpen = false;
2026-03-02 13:25:21 +08:00
_obstacleHoles.clear();
_currentObstaclePoints.clear();
_isObstacleEditing = false;
2026-02-28 14:33:55 +08:00
// 重置选中状态
_selectedPlot = null;
2026-03-03 09:40:29 +08:00
2026-02-28 14:33:55 +08:00
_selectedPlotPath = null;
// 重置作业模式
2026-03-09 17:30:26 +08:00
_currentRobotMode = RobotMode.robot;
2026-02-28 14:33:55 +08:00
_currentAreaMode = AreaMode.work;
_currentWorkMode = WorkMode.bow;
// 重置参数
_workDistance = 0.0;
_angle = 0.0;
2026-03-09 17:30:26 +08:00
// 🔥 修复2:优先恢复地图中心,而非定位坐标
_currentLatLng = null;
_currentWgsLatLng = null;
_mapCenter = _getDefaultValidCenter(); // 仅用兜底坐标初始化地图中心
2026-03-10 15:06:28 +08:00
isDjMapShow = false;
2026-02-28 14:33:55 +08:00
});
2026-03-10 17:04:43 +08:00
_clearLocalData();
2026-02-28 14:33:55 +08:00
// 3. 重新初始化定位
await _initLocationEnterprise();
// 4. 重置轨迹管理器
_traceManager.reset();
2026-03-10 08:58:44 +08:00
if (_currentLatLng != null && _isValidLatLng(_currentLatLng!.latitude, _currentLatLng!.longitude)) {
_mapController.move(_currentLatLng!, savedZoom);
2026-03-09 17:30:26 +08:00
} else {
// 使用兜底的有效坐标(确保在高德瓦片覆盖范围内)
final defaultCenter = _getDefaultValidCenter();
_mapController.move(defaultCenter, 17.0);
_currentLatLng = null;
_currentWgsLatLng = null;
_mapCenter = defaultCenter;
}
2026-02-28 14:33:55 +08:00
// 5. 重新加载作业记录(保持数据最新)
2026-03-09 12:41:55 +08:00
// final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
// if (userId.isNotEmpty) {
// await context.read<DevicesCubit>().loadWorkRecords(userId);
// }
2026-02-28 14:33:55 +08:00
2026-03-09 17:30:26 +08:00
_mapController.move(_getDefaultValidCenter(), savedZoom);
2026-02-28 14:33:55 +08:00
// 刷新成功提示
if (mounted) {
2026-03-03 10:28:50 +08:00
ToastUtils.showSuccess(context, '页面已刷新完成');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('页面已刷新完成'), backgroundColor: Colors.green, duration: Duration(seconds: 2)));
2026-02-28 14:33:55 +08:00
}
} catch (e) {
// 错误处理
debugPrint('刷新失败:$e');
if (mounted) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '刷新失败:${e.toString().substring(0, 50)}');
//ScaffoldMessenger.of(
// context,
//).showSnackBar(SnackBar(content: Text('刷新失败:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red, duration: Duration(seconds: 3)));
2026-02-28 14:33:55 +08:00
}
} finally {
// 标记刷新结束
if (mounted) {
setState(() {
_isRefreshing = false;
});
}
}
}
/// 手动回到当前位置
void _moveToCurrentLocation() {
if (_currentLatLng == null) return;
_mapController.move(_currentLatLng!, 17);
}
2026-03-02 13:25:21 +08:00
// ========== 障碍物模式:完成当前障碍物绘制 ==========
void _completeObstacle() {
if (_currentObstaclePoints.length < 3) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '障碍物区域至少需要3个打点!');
2026-03-02 13:25:21 +08:00
return;
}
setState(() {
2026-03-07 21:21:31 +08:00
// 保存当前障碍物组(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));
}
2026-03-02 13:25:21 +08:00
_currentObstaclePoints.clear(); // 清空当前绘制的障碍物点
2026-03-07 21:21:31 +08:00
_robotModeObsWgsPoints.clear(); // 清空Robot模式下的障碍物WGS84坐标
2026-03-02 13:25:21 +08:00
_isObstacleEditing = false; // 退出编辑状态
});
2026-03-10 17:04:43 +08:00
_saveDataToLocal();
2026-03-02 13:25:21 +08:00
debugPrint('完成障碍物绘制,当前障碍物组数:${_obstacleHoles.length}');
2026-03-03 10:28:50 +08:00
ToastUtils.showSuccess(context, '已添加第${_obstacleHoles.length}个障碍物区域');
2026-03-02 13:25:21 +08:00
}
2026-02-25 20:09:18 +08:00
// ========== 核心方法:打点逻辑 ==========
void _addMarkedPoint() {
2026-03-03 10:13:32 +08:00
if (_currentAreaMode == AreaMode.work && _isWorkAreaCompleted) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '作业区域已完成,无法继续添加打点!');
2026-03-03 10:13:32 +08:00
return;
}
2026-02-25 20:09:18 +08:00
setState(() {
2026-03-07 21:21:31 +08:00
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}');
}
2026-03-02 13:25:21 +08:00
} else {
2026-03-07 21:21:31 +08:00
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}');
}
2026-03-02 13:25:21 +08:00
}
2026-02-25 20:09:18 +08:00
});
2026-03-10 17:04:43 +08:00
_saveDataToLocal();
2026-02-28 16:20:21 +08:00
debugPrint('$_markedPoints,新增打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude.toStringAsFixed(20)}, ${_mapCenter.longitude.toStringAsFixed(20)}');
2026-02-25 20:09:18 +08:00
}
// ========== 新增:返回上一级页面的方法 ==========
void _navigateBack() {
// 关闭当前页面,返回上一级
Navigator.of(context).pop();
}
@override
void dispose() {
2026-02-26 14:08:02 +08:00
_traceManager.reset();
_mapController.dispose();
super.dispose();
}
2026-03-05 15:46:41 +08:00
void _handleVideo() {
setState(() {
_isVideoDialogOpen = true; // 打开视频弹窗
});
}
Widget _buildVideoPopup() {
return Positioned(
left: _videoPopupPos.dx,
top: _videoPopupPos.dy,
child: GestureDetector(
// 拖拽区域(整个弹窗可拖拽)
onPanUpdate: (details) {
setState(() {
_videoPopupPos = Offset(_videoPopupPos.dx + details.delta.dx, _videoPopupPos.dy + details.delta.dy);
});
},
child: WebRTCMapPlayer(
streamUrl: _videoStreamUrl,
width: 320,
height: 180,
2026-03-09 12:41:55 +08:00
showLeftPip: false,
showRightPip: false,
2026-03-05 15:46:41 +08:00
onClose: _closeVideoDialog, // 关闭回调
),
),
);
}
void _closeVideoDialog() {
setState(() {
_isVideoDialogOpen = false;
});
}
// 🔥 改动2:修改 _buildPlotListItem 方法,增加删除时调用Bloc的逻辑
Widget _buildPlotListItem(PlotData plot, Function(PlotData) onDelete) {
2026-02-26 14:08:02 +08:00
return GestureDetector(
// 核心:点击列表项触发选中逻辑
onTap: () async {
// 1. 清空之前的轨迹数据
setState(() {
gcjPathPoints = [];
gcjOuterPoints = [];
});
2026-02-28 09:55:14 +08:00
await context.read<DevicesCubit>().loadSelectedPath(plot.plotName);
final cubitState = context.read<DevicesCubit>().state;
final _loadedPlot = cubitState.pathData;
print('加载地块「${plot.plotName}」的路径数据: $_loadedPlot');
if (_loadedPlot is List) {
final List<dynamic> rawList = _loadedPlot as List<dynamic>;
2026-02-28 16:20:21 +08:00
final pathRecords = rawList.where((item) => item is Map<String, dynamic>).cast<Map<String, dynamic>>().toList();
if (pathRecords.isNotEmpty) {
final selectedPlotPath = PlotDataPath(jsonData: pathRecords.first);
try {
2026-02-28 16:20:21 +08:00
final dynamic nestedJsonRaw = selectedPlotPath.jsonData['jsonData'];
if (nestedJsonRaw != null && nestedJsonRaw is String) {
final String nestedJsonStr = nestedJsonRaw;
final dynamic parsedJson = jsonDecode(nestedJsonStr);
2026-02-28 16:20:21 +08:00
workMode = parsedJson['planModel'] == WorkMode.bow.value ? "弓字模式" : "自定义模式";
2026-03-01 16:13:37 +08:00
debugPrint('解析出的作业模式: ${parsedJson['planModel']},WorkMode.bow.value=${WorkMode.bow.value}, 解析结果: $workMode');
if (parsedJson is Map<String, dynamic>) {
final Map<String, dynamic> nestedJson = parsedJson;
2026-03-01 16:13:37 +08:00
// 替换你原来的 rawPath/rawOuter 解析部分
final dynamic rawPath = nestedJson['path'];
final dynamic rawOuter = nestedJson['outer'];
2026-03-01 16:13:37 +08:00
debugPrint('解析后的路径数据11: pathList=$rawPath,');
debugPrint('解析后的外边界数据22: outerList=$rawOuter,');
// ========== 修复核心:先解码字符串,再处理类型 ==========
List<dynamic> pathList = [];
if (rawPath is String) {
try {
pathList = jsonDecode(rawPath) as List<dynamic>;
} catch (e) {
debugPrint('path 字符串解码失败: $e, rawPath=$rawPath');
pathList = [];
}
} else if (rawPath is List) {
pathList = rawPath;
} else {
pathList = [];
}
List<dynamic> outerList = [];
if (rawOuter is String) {
try {
outerList = jsonDecode(rawOuter) as List<dynamic>;
} catch (e) {
debugPrint('outer 字符串解码失败: $e, rawOuter=$rawOuter');
outerList = [];
}
} else if (rawOuter is List) {
outerList = rawOuter;
} else {
outerList = [];
}
2026-03-11 09:15:44 +08:00
if (_currentWorkMode == WorkMode.bow) {
startWorkList = pathList;
} else {
startWorkList = outerList;
}
2026-03-01 16:13:37 +08:00
// ========== 修复结束 ==========
debugPrint('解析后的路径数据: pathList=$pathList, 长度=${pathList.length}');
debugPrint('解析后的外边界数据: outerList=$outerList, 长度=${outerList.length}');
List<LatLng> newPathPoints = [];
List<LatLng> 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<LatLng> 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;
});
}
2026-02-26 14:08:02 +08:00
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
2026-02-26 14:08:02 +08:00
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
2026-02-28 16:20:21 +08:00
color: _selectedPlot?.id == plot.id ? Colors.blue : Colors.grey[100]!,
2026-02-26 14:08:02 +08:00
width: _selectedPlot?.id == plot.id ? 2 : 1, // 选中项高亮边框
2026-02-26 10:44:56 +08:00
),
2026-02-26 14:08:02 +08:00
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
2026-02-26 14:08:02 +08:00
plot.imageUrl,
width: 30,
height: 30,
2026-02-26 14:08:02 +08:00
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 30,
height: 30,
2026-02-26 14:08:02 +08:00
color: Colors.grey[200],
child: const Icon(Icons.image_outlined, color: Colors.grey),
);
},
),
),
2026-02-26 10:44:56 +08:00
const SizedBox(width: 10),
2026-02-26 10:44:56 +08:00
2026-02-26 14:08:02 +08:00
Expanded(
child: Text(
plot.plotName,
style: const TextStyle(fontSize: 16, color: Colors.black87),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
2026-02-26 10:44:56 +08:00
),
2026-02-26 14:08:02 +08:00
IconButton(
onPressed: () => _showDeleteConfirmDialog(plot, onDelete),
2026-02-28 16:20:21 +08:00
icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20),
2026-02-26 14:08:02 +08:00
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
2026-02-26 10:44:56 +08:00
),
2026-02-26 14:08:02 +08:00
],
),
2026-02-26 10:44:56 +08:00
),
);
}
2026-03-09 17:30:26 +08:00
LatLng _getDefaultValidCenter() {
return const LatLng(30.279651, 120.154871);
}
2026-03-06 17:19:26 +08:00
Widget _buildMap(double lat, double lng, bool obfFlag, int headingStatus, String controlMode) {
if (lat != 0 && lat != 0 && (lat != _currentLatLng?.latitude || lng != _currentLatLng?.longitude)) {
_onDeviceData(lng, lat, obfFlag, headingStatus, controlMode);
}
final gcjPoint = wgs84ToGcj02(lat, lng);
2026-03-09 17:30:26 +08:00
//final initCenter = bd09ToGcj02(
// '120.81992468426961', // 经度
// '32.04532826114155',
//);
// 🔥 修复1:初始化坐标改为直接使用有效GCJ02坐标,避免BD09转换风险
final initCenter = _getDefaultValidCenter();
2026-03-07 21:21:31 +08:00
_currentWgsLatLng = LatLng(lat, lng);
2026-03-07 20:03:33 +08:00
2026-03-09 17:30:26 +08:00
//_currentLatLng = gcjPoint; // ✅ 状态变量在setState内更新
if (_isValidLatLng(gcjPoint.latitude, gcjPoint.longitude)) {
_currentLatLng = gcjPoint;
} else {
_currentLatLng = null;
}
2026-03-06 17:19:26 +08:00
return FlutterMap(
mapController: _mapController,
options: MapOptions(
2026-03-07 20:03:33 +08:00
initialCenter: __isValidLatLng(_currentLatLng) ? _currentLatLng! : initCenter,
2026-03-09 13:42:52 +08:00
initialZoom: 18,
2026-03-11 09:44:08 +08:00
maxZoom: 22,
2026-03-07 18:47:30 +08:00
minZoom: 3,
enableScrollWheel: true,
2026-03-09 13:42:52 +08:00
// 🔥 视觉放大:通过缩放系数实现"伪20级"显示(无空白)
2026-03-06 17:19:26 +08:00
onTap: (_, __) {}, // 空实现,禁用地图点击响应
),
children: [
/// 高德瓦片(GCJ-02)
2026-03-09 17:30:26 +08:00
TileLayer(
2026-03-11 10:50:50 +08:00
//urlTemplate:
// "https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=2&style=7&key=bbb1f0f20eed6bf679eddf2625630aba",
urlTemplate:
'https://webrd02.is.autonavi.com/appmaptile'
'?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1'
'&key=bbb1f0f20eed6bf679eddf2625630aba',
2026-03-09 17:30:26 +08:00
// 🔥 修复:适配高DPI(替代pixelRatio)
// tileSize: 512, // 高清瓦片尺寸(默认256,改为512适配scale=2)
tileProvider: NetworkTileProvider(
// 自定义瓦片请求头(可选,提升兼容性)
headers: {
'Referer': 'https://www.amap.com', // 高德要求的Referer
},
2026-03-09 13:42:52 +08:00
),
2026-03-09 17:30:26 +08:00
maxZoom: 18,
2026-03-06 17:19:26 +08:00
),
2026-03-10 15:06:28 +08:00
//if (isDjMapShow) DJITileLayer(stationName: _currentStation.name, mapController: _mapController),
2026-03-06 17:19:26 +08:00
// 绘制path折线(Line模式)
if (gcjPathPoints.isNotEmpty)
PolylineLayer(
polylines: [
Polyline(
points: gcjPathPoints, // 转换后的GCJ02坐标
color: const Color.fromARGB(255, 255, 204, 0), // 金黄色(更醒目)
2026-03-06 17:19:26 +08:00
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),
],
),
2026-03-07 21:30:34 +08:00
if (_markedPoints.isNotEmpty && !_isWorkAreaCompleted && _currentRobotMode == RobotMode.point)
2026-03-06 17:19:26 +08:00
PolylineLayer(
polylines: [
Polyline(points: [_mapCenter, _markedPoints.last], color: Colors.blue.withOpacity(0.5), strokeWidth: 1.5),
],
),
/// 当前定位 Marker
2026-03-09 16:04:31 +08:00
if (_currentLatLng != null && __isValidLatLng(_currentLatLng))
2026-03-06 17:19:26 +08:00
MarkerLayer(
markers: [
Marker(
point: _currentLatLng!,
width: 40,
height: 40,
child: CustomPaint(
size: const Size(40, 40),
painter: HeadingMarkerPainter(headingAngle: _headingAngle),
),
),
],
),
2026-03-07 21:30:34 +08:00
if (_currentWorkMode == WorkMode.bow && _markedPoints.isNotEmpty && !_isWorkAreaCompleted && _currentRobotMode == RobotMode.point)
2026-03-06 17:19:26 +08:00
PolygonLayer(
polygons: [
Polygon(
points: _getPolygonPoints(),
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green.withOpacity(0.5),
borderStrokeWidth: 1,
isFilled: true,
),
],
),
if (_obstacleHoles.isNotEmpty && _isObstacleEditing)
PolygonLayer(
polygons: _obstacleHoles.map((holePoints) {
return Polygon(
points: holePoints,
color: Colors.red.withOpacity(0.2), // 红色半透明填充
borderColor: Colors.red, // 红色边框
borderStrokeWidth: 1.5,
isFilled: true,
);
}).toList(),
),
2026-03-07 20:03:33 +08:00
//开始作业之后
2026-03-06 17:19:26 +08:00
// 2. 正在绘制的障碍物打点连线(红色虚线)
2026-03-11 10:50:50 +08:00
if (_isObstacleEditing && _currentObstaclePoints.isNotEmpty)
2026-03-06 17:19:26 +08:00
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 && _isObstacleEditing)
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),
),
),
),
],
),
),
],
),
if (!_isWorkAreaCompleted)
/// 历史打点的绿色标记
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(),
),
2026-03-07 21:21:31 +08:00
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, // 虚线区分作业区域
),
],
),
2026-03-06 17:19:26 +08:00
],
);
}
2026-03-02 09:31:46 +08:00
/// 开始作业
void _startWork() async {
debugPrint('🔵 _startWork 方法被调用了!');
debugPrint('📊 startWorkList 类型:${startWorkList.runtimeType}');
debugPrint('📊 startWorkList 长度:${startWorkList.length}');
debugPrint('📊 startWorkList 内容:$startWorkList');
2026-03-11 09:15:44 +08:00
if (startWorkList.isEmpty) {
debugPrint('🚀 作业列表为空');
2026-03-06 17:19:26 +08:00
ToastUtils.showInfo(context, '作业列表为空,请重新选择路径');
return;
}
isStartWork = true;
_traceManager.setMode(TPMode.NAVIGATION);
2026-03-02 09:31:46 +08:00
setState(() {
_workStatus = WorkStatus.working;
});
2026-03-11 09:15:44 +08:00
//// 步骤1:将 List<LatLng> 转换为 List<DeviceAddPathPointModel>
//final List<work_area_model.DeviceAddPathPointModel> pathModels = gcjPathPoints.map((latLng) {
// return work_area_model.DeviceAddPathPointModel(
// latitude: latLng.latitude, // 纬度
// longitude: latLng.longitude, // 经度
// );
//}).toList();
// 步骤2:将 List 转换为 Queue(队列)
debugPrint('⚙️ 开始类型转换...');
2026-03-11 12:54:42 +08:00
final List<work_area_model.DeviceAddPathPointModel> typedList = startWorkList.whereType<Map<String, dynamic>>().map((item) {
return work_area_model.DeviceAddPathPointModel(latitude: (item['lat'] as num).toDouble(), longitude: (item['lon'] as num).toDouble());
}).toList();
2026-03-11 15:49:50 +08:00
debugPrint("===打印发送开始作业的路径开始 ");
for (var i = 0; i < typedList.length; i++) {
debugPrint(typedList[i].toString());
}
debugPrint("===打印发送开始作业的路径结束");
final Queue<work_area_model.DeviceAddPathPointModel> pathQueue = Queue.from(typedList);
debugPrint('🚀 开始作业:${_selectedPlot?.plotName ?? "未命名"},路径点数量:${pathQueue.length}');
debugPrint('第一个点 WGS84: ${pathQueue.first.latitude}, ${pathQueue.first.longitude}');
2026-03-02 09:31:46 +08:00
// 步骤 3:调用 Cubit 方法(类型匹配)
await context.read<DevicesCubit>().startRoutePlanning(pathQueue);
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
2026-03-02 09:31:46 +08:00
// 可选:显示作业提示
2026-03-03 10:28:50 +08:00
ToastUtils.showSuccess(context, '作业已开始');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已开始'), backgroundColor: Colors.green));
2026-03-02 09:31:46 +08:00
}
/// 暂停作业
void _pauseWork() async {
2026-03-02 09:31:46 +08:00
setState(() {
_workStatus = WorkStatus.paused;
});
context.read<DevicesCubit>().updateAppState(AppState.none);
await context.read<DevicesCubit>().pauseRoutePlanning();
2026-03-02 09:31:46 +08:00
debugPrint('暂停作业:${_selectedPlot!.plotName}');
}
void _stopWork() async {
2026-03-02 09:31:46 +08:00
setState(() {
_workStatus = WorkStatus.idle;
});
context.read<DevicesCubit>().updateAppState(AppState.none);
await context.read<DevicesCubit>().stopRoutePlanning();
//修改App的状态
2026-03-02 09:31:46 +08:00
debugPrint('停止作业:${_selectedPlot!.plotName}');
///ToastUtils.showError(context, '作业已停止');
2026-03-02 09:31:46 +08:00
}
/// 继续作业
void _resumeWork() async {
2026-03-02 09:31:46 +08:00
setState(() {
_workStatus = WorkStatus.working;
});
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
await context.read<DevicesCubit>().resumeRoutePlanning();
2026-03-02 09:31:46 +08:00
}
void _showDeleteConfirmDialog(PlotData plot, Function(PlotData) onDelete) {
ConfirmDialog.show(
2026-02-26 10:44:56 +08:00
context: context,
title: '确认删除',
content: '是否删除地块「${plot.plotName}」?删除后不可恢复。',
confirmText: '删除',
confirmTextColor: Colors.red,
onConfirm: () {
onDelete(plot);
2026-02-26 10:44:56 +08:00
},
);
}
2026-02-26 14:08:02 +08:00
Widget _buildWorkPanel() {
2026-02-28 09:55:14 +08:00
return BlocBuilder<DevicesCubit, DevicesState>(
builder: (context, state) {
// 无选中地块或无路径数据时返回空
if (_selectedPlot == null || state.pathData == null) {
return const SizedBox.shrink();
}
2026-02-26 14:08:02 +08:00
2026-02-28 09:55:14 +08:00
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)),
2026-02-28 16:20:21 +08:00
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, -2))],
2026-02-26 14:08:02 +08:00
),
2026-02-28 09:55:14 +08:00
child: Column(
mainAxisSize: MainAxisSize.min,
2026-02-26 14:08:02 +08:00
children: [
2026-02-28 09:55:14 +08:00
// 1. 地块基础信息
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
_selectedPlot!.imageUrl,
2026-02-26 14:08:02 +08:00
width: 80,
height: 80,
2026-02-28 09:55:14 +08:00
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 80,
height: 80,
color: Colors.grey[200],
2026-02-28 16:20:21 +08:00
child: const Icon(Icons.image_outlined, color: Colors.grey, size: 32),
2026-02-28 09:55:14 +08:00
);
},
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_selectedPlot!.plotName,
2026-02-28 16:20:21 +08:00
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
2026-02-28 09:55:14 +08:00
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
2026-02-28 16:20:21 +08:00
Text('作业模式:$workMode', style: const TextStyle(fontSize: 14, color: Colors.grey)),
2026-02-28 09:55:14 +08:00
],
),
),
IconButton(
onPressed: () {
setState(() {
_isWorkPanelOpen = false;
_selectedPlot = null;
gcjPathPoints = [];
gcjOuterPoints = [];
2026-02-28 09:55:14 +08:00
});
},
2026-02-28 16:20:21 +08:00
icon: const Icon(Icons.close, color: Colors.grey, size: 20),
2026-02-28 09:55:14 +08:00
padding: EdgeInsets.zero,
2026-02-28 16:20:21 +08:00
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
2026-02-28 09:55:14 +08:00
),
],
2026-02-26 14:08:02 +08:00
),
2026-02-28 09:55:14 +08:00
const SizedBox(height: 16),
2026-02-26 14:08:02 +08:00
2026-02-28 09:55:14 +08:00
// 2. 错误提示(如果加载失败)
if (state.errorMessage != null)
Container(
2026-02-28 16:20:21 +08:00
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
2026-02-28 09:55:14 +08:00
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: [
2026-02-28 16:20:21 +08:00
const Icon(Icons.error_outline, color: Colors.redAccent, size: 16),
2026-02-28 09:55:14 +08:00
const SizedBox(width: 8),
Expanded(
child: Text(
state.errorMessage!,
2026-02-28 16:20:21 +08:00
style: const TextStyle(fontSize: 14, color: Colors.redAccent),
2026-02-28 09:55:14 +08:00
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
2026-02-26 14:08:02 +08:00
),
),
2026-02-28 09:55:14 +08:00
// 3. 开始作业按钮(加载中禁用)
2026-03-02 09:31:46 +08:00
// 放在 Column/Row 的 children: [ ... ] 内部
_workStatus == WorkStatus.idle
? SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: state.isLoading || startWorkList.isEmpty
2026-03-02 09:31:46 +08:00
? 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)),
// ),
//),
2026-02-26 14:08:02 +08:00
],
),
2026-02-28 09:55:14 +08:00
),
);
},
2026-02-26 14:08:02 +08:00
);
}
2026-03-01 15:11:12 +08:00
Future<String?> _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<int> 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列表
2026-02-28 16:20:21 +08:00
List<PlotData> _convertWorkRecordsToPlotData(List<Map<String, dynamic>> records) {
return records.map((record) {
return PlotData(
2026-02-28 16:20:21 +08:00
id: record['id']?.toString() ?? DateTime.now().microsecondsSinceEpoch.toString(), // 唯一ID
plotName: record['workName'] ?? '未命名地块', // 地块名称(从接口字段取)
imageUrl: record['imgUrl'] ?? '', // 图片URL(从接口字段取,无则为空)
jsonData: record['jsonData'], // 原始数据的JSON字符串(可选,便于调试或后续使用)
);
}).toList();
}
// ========== 抽象:生成路径的核心函数 ==========
Future<void> _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;
}
2026-03-07 21:21:31 +08:00
2026-03-02 13:25:21 +08:00
// 1. 校验必要条件
if (_currentAreaMode == AreaMode.work && _markedPoints.isEmpty) {
if (showTips) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '请先添加作业区域打点!');
}
return;
}
2026-03-02 13:25:21 +08:00
if (_currentAreaMode == AreaMode.work && _markedPoints.length < 3) {
if (showTips) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '作业区域至少需要3个打点才能生成路径!');
}
return;
}
if (_currentWorkMode == null) {
if (showTips) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '请先选择作业模式!');
}
return;
}
try {
// 2. 转换页面数据为 generatePath 所需的参数格式
2026-03-07 21:21:31 +08:00
// 2.1 参考点(区分Robot/Point模式)
LatLng firstPointWgs; // 最终传给接口的WGS84格式参考点
if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isNotEmpty) {
// Robot模式:直接使用原始WGS84坐标(无需转换)
firstPointWgs = _robotModeWgsPoints.first;
2026-03-11 10:50:50 +08:00
} else if (_currentRobotMode == RobotMode.point && _markedPoints.isNotEmpty) {
2026-03-07 21:21:31 +08:00
// Point模式:先取十字准星的GCJ02坐标 → 转换为WGS84
LatLng firstPointGcj = _markedPoints.first;
firstPointWgs = gcj02ToWgs84(firstPointGcj.latitude, firstPointGcj.longitude);
2026-03-11 10:50:50 +08:00
} else {
if (showTips) {
ToastUtils.showError(context, '参考点坐标为空,请先添加作业区域打点!');
}
return; // 终止方法执行,避免后续错误
2026-03-07 21:21:31 +08:00
}
2026-03-11 10:50:50 +08:00
2026-03-07 21:21:31 +08:00
// 参考点使用转换后的WGS84坐标
final referencePoint = work_area_model.ReferencePoint(lat: firstPointWgs.latitude, lon: firstPointWgs.longitude);
2026-03-02 13:25:21 +08:00
// 2.2 航向角
final heading = _angle == -1 ? -1 : _angle.toInt();
2026-03-07 21:21:31 +08:00
// 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 if (_currentRobotMode == RobotMode.point && _markedPoints.isNotEmpty) {
2026-03-07 21:21:31 +08:00
// 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);
2026-03-07 21:21:31 +08:00
}).toList();
}
2026-03-11 09:44:08 +08:00
gcjOuterPoints = outerPositions.map((point) {
double wgs84Lat = point.lat;
double wgs84Lon = point.lon;
final gcjPoint = wgs84ToGcj02(wgs84Lat, wgs84Lon);
return LatLng(gcjPoint.latitude, gcjPoint.longitude);
}).toList();
2026-03-07 21:21:31 +08:00
final outerBoundary = work_area_model.OuterBoundary(position: outerPositions, sideWidth: _workDistance);
2026-03-03 09:40:29 +08:00
2026-03-07 21:21:31 +08:00
work_area_model.HoleBoundary? holeBoundary;
2026-03-11 10:50:50 +08:00
2026-03-02 13:25:21 +08:00
if (_obstacleHoles.isNotEmpty) {
2026-03-11 10:50:50 +08:00
// 1. 构造二维 Position 数组
2026-03-03 09:40:29 +08:00
List<List<work_area_model.Position>> positionList = [];
2026-03-07 21:21:31 +08:00
if (_currentRobotMode == RobotMode.robot && _robotModeObsWgsPoints.isNotEmpty) {
for (var holePoints in _obstacleWgsHoles) {
List<work_area_model.Position> innerPositionList = [];
innerPositionList = holePoints.map((latLng) {
return work_area_model.Position(lat: latLng.latitude, lon: latLng.longitude);
}).toList();
positionList.add(innerPositionList);
}
} else if (_currentRobotMode == RobotMode.point && _obstacleHoles.isNotEmpty) {
for (var holePoints in _obstacleHoles) {
2026-03-07 21:21:31 +08:00
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();
2026-03-07 21:21:31 +08:00
positionList.add(innerPositionList);
}
}
2026-03-11 10:50:50 +08:00
//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);
//}
// 2. 创建 HoleBoundary 对象(无外层 Map)
2026-03-03 09:40:29 +08:00
holeBoundary = work_area_model.HoleBoundary(position: positionList, sideWidth: 0.0);
2026-03-02 13:25:21 +08:00
}
2026-03-03 09:40:29 +08:00
// 🔥 第二步:构造符合方法参数要求的 Map<String, HoleBoundary>
2026-03-11 10:50:50 +08:00
// 用一个临时 Map 来满足类型要求,后续在 Cubit 中取值即可
2026-03-03 09:40:29 +08:00
Map<String, work_area_model.HoleBoundary> holes = {};
if (holeBoundary != null) {
2026-03-11 10:50:50 +08:00
holes = {'tempKey': holeBoundary}; // 键名任意,仅为满足类型要求
2026-03-02 13:25:21 +08:00
}
final workType = _currentWorkMode == WorkMode.bow ? 0 : 2;
// 3. 调用 Cubit 的 generatePath 方法(传入holes参数)
await context.read<DevicesCubit>().generatePath(
reference: referencePoint,
heading: heading,
outer: outerBoundary,
2026-03-11 10:50:50 +08:00
holes: holes.isNotEmpty ? holes : {}, // 无障碍物时传空对象
2026-03-02 13:25:21 +08:00
workType: workType,
);
2026-03-02 13:25:21 +08:00
// 4. 处理生成结果(原有逻辑不变)
final cubitState = context.read<DevicesCubit>().state;
print('${cubitState.generatedPath},路径生成结果');
if (cubitState.errorMessage != null) {
if (showTips) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '路径生成失败:${cubitState.errorMessage}');
2026-03-11 10:50:50 +08:00
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成失败:${cubitState.errorMessage}'), backgroundColor: Colors.red));
}
} else if (cubitState.generatedPath != null) {
if (showTips) {
2026-03-03 10:28:50 +08:00
ToastUtils.showSuccess(context, '路径生成成功!');
}
if (cubitState.generatedPath is List) {
setState(() {
_isWorkAreaCompleted = true;
_saveBoxOpen = true;
final pathList = cubitState.generatedPath as List;
typedPathList = pathList.whereType<work_area_model.DeviceAddPathPointModel>().toList();
gcjPathPoints = typedPathList.map((point) {
double wgs84Lat = point.latitude;
double wgs84Lon = point.longitude;
final gcjPoint = wgs84ToGcj02(wgs84Lat, wgs84Lon);
return LatLng(gcjPoint.latitude, gcjPoint.longitude);
}).toList();
2026-03-07 21:21:31 +08:00
// 外边界保持原有逻辑
2026-03-11 09:44:08 +08:00
//gcjOuterPoints = List.from(_markedPoints);
});
2026-03-10 17:04:43 +08:00
_saveDataToLocal();
moveMapToPointsCenter(gcjPathPoints);
}
}
} catch (e) {
debugPrint('路径生成异常:$e');
if (showTips) {
2026-03-03 10:28:50 +08:00
ToastUtils.showError(context, '路径生成异常:${e.toString().substring(0, 50)}');
}
}
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56
final maxTop = screenHeight - menuHeight;
2026-03-05 15:46:41 +08:00
final userState = context.watch<AppUserCubit>().state;
final deviceId = context.watch<DevicesCubit>().state.selectedDevice?.deviceName;
2026-03-06 17:19:26 +08:00
2026-03-07 21:21:31 +08:00
if (deviceId != null && userState.user != null && userState.user!.token != null) {
2026-03-05 15:46:41 +08:00
_videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
} else {
2026-03-07 21:21:31 +08:00
_videoStreamUrl = '';
2026-03-05 15:46:41 +08:00
print("设备未选中或用户未登录,无法生成视频流地址");
}
2026-03-06 17:19:26 +08:00
return BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
// 初始化默认值
String yaw = '--';
String satelliteCnt = '--';
double currentLat = 0.0;
double currentLng = 0.0;
bool obfFlag = false; //障碍物标志位
int headingStatus = 0;
String controlMode = "0";
DeviceStatusUpdated? updatedState;
if (state is DeviceStatusUpdated) {
// 强转获取具体的状态数据
updatedState = state as DeviceStatusUpdated;
obfFlag = updatedState.status.obstacleFlag.toString() == 1;
headingStatus = updatedState.status.headingStatus;
controlMode = updatedState.status.controlMode;
_headingAngle = updatedState.status.yaw;
2026-03-11 15:33:33 +08:00
//debugPrint("路径规划四十数据 - 经纬度:");
//debugPrint(" RunningStatus纬度:${updatedState.status.latitude}");
//debugPrint(" RunningStatus经度:${updatedState.status.longitude}");
//debugPrint("${updatedState.status.obstacleFlag},${updatedState.status.qual},${updatedState.status.controlMode}");
//debugPrint("${updatedState.status.headingStatus},${updatedState.status.yaw}");
2026-03-06 17:19:26 +08:00
// 提取经纬度并更新本地变量
currentLat = updatedState.status.latitude;
currentLng = updatedState.status.longitude;
} else {}
return Scaffold(
body: SafeArea(
child: Stack(
children: [
2026-03-09 16:04:31 +08:00
RepaintBoundary(key: _mapRepaintKey, child: _buildMap(currentLat, currentLng, obfFlag, headingStatus, controlMode)),
2026-03-06 17:19:26 +08:00
// 地图核心组件
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)],
2026-03-01 15:11:12 +08:00
),
2026-03-06 17:19:26 +08:00
),
2026-03-02 13:25:21 +08:00
),
2026-03-06 17:19:26 +08:00
),
2026-03-02 13:25:21 +08:00
2026-03-06 17:19:26 +08:00
// 悬浮返回按钮(核心修改)
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),
2026-03-02 13:25:21 +08:00
),
),
2026-03-03 09:40:29 +08:00
),
2026-03-06 17:19:26 +08:00
// 2. 图标层(绝对居中)
const Icon(Icons.arrow_back_ios, color: Colors.black87, size: 20),
],
),
2026-02-28 14:33:55 +08:00
),
),
),
2026-03-06 17:19:26 +08:00
/// 核心:地图正中心固定定位标
2026-03-09 11:06:43 +08:00
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(), // 自定义十字画笔
),
),
2026-03-06 17:19:26 +08:00
),
),
2026-03-06 17:19:26 +08:00
//保存按钮
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(),
),
),
2026-02-25 20:36:46 +08:00
),
2026-03-06 17:19:26 +08:00
// 右侧悬浮菜单
Positioned(
height: 336,
right: 16,
top: maxTop > 10 ? 11 : maxTop,
child: FloatingActionButton(
onPressed: _moveToCurrentLocation,
child: VerticalFloatMenu(
onEditTap: (bool isOpen) {
setState(() {
_isPanelOpen = isOpen;
});
},
onListBox: (bool isOpen) {
// 🔥 改动5:加载作业记录(原有逻辑保留)
final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
print('加载作业记录,当前用户ID:$userId');
context.read<DevicesCubit>().loadWorkRecords(userId);
2026-03-06 17:19:26 +08:00
setState(() {
_isListBoxOpen = isOpen;
_isWorkPanelOpen = false;
});
},
2026-03-10 15:06:28 +08:00
onDjShow: (bool isOpen) {
2026-03-10 15:58:52 +08:00
if (isOpen) context.go(RoutePaths.djMap);
2026-03-10 15:06:28 +08:00
setState(() {
isDjMapShow = isOpen;
});
},
2026-03-06 17:19:26 +08:00
onWorkModeSelected: (mode) {
_currentWorkMode = mode;
debugPrint('外部收到作业模式:$mode');
},
2026-03-10 08:58:44 +08:00
handleGotoLocation: _handleGotoLocation,
2026-03-06 17:19:26 +08:00
onRefreshTap: _handleRefresh, // 推荐用局部刷新
onVideoTap: _handleVideo,
),
),
2026-02-25 20:00:33 +08:00
),
2026-02-25 20:09:18 +08:00
2026-03-06 17:19:26 +08:00
// 底部操作面板
if (_isPanelOpen)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: BottomOperationPanel(
2026-03-09 11:06:43 +08:00
initialRobotMode: _currentRobotMode,
initialAreaMode: _currentAreaMode,
2026-03-06 17:19:26 +08:00
initialWorkMode: _currentWorkMode!,
canUndo: _isWorkAreaCompleted && _currentAreaMode == AreaMode.work ? false : true,
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: () {
_undoAction();
},
onDeleteTap: () {
_deleteAllAction();
},
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('外部处理航线标签点击');
},
),
),
2026-03-06 17:19:26 +08:00
// 航线方向面板
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);
},
),
),
2026-03-06 17:19:26 +08:00
// 列表面板 - 使用BlocBuilder监听DevicesCubit状态
if (_isListBoxOpen)
BlocBuilder<DevicesCubit, DevicesState>(
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)),
),
2026-03-06 17:19:26 +08:00
),
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),
2026-02-26 10:44:56 +08:00
),
2026-03-06 17:19:26 +08:00
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<DevicesCubit>().deleteWorkRecord(deletedPlot.plotName);
final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
await context.read<DevicesCubit>().loadWorkRecords(userId);
setState(() {});
});
},
),
),
],
),
2026-03-06 17:19:26 +08:00
),
],
),
2026-03-06 17:19:26 +08:00
);
},
),
if (_showHeadingWarn)
Positioned(
top: 100, // 调整位置,避免遮挡核心内容
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.9),
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4, offset: const Offset(0, 2))],
),
child: const Row(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.white, size: 20),
SizedBox(width: 8),
Expanded(
child: Text('航向角未初始化,无法开始作业', style: TextStyle(color: Colors.white, fontSize: 14)),
),
],
),
),
),
// 2. 远程模式提示
if (_showControlModeWarn)
Positioned(
top: 160, // 与上一个提示错开位置
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.9),
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4, offset: const Offset(0, 2))],
),
child: const Row(
children: [
Icon(Icons.error_rounded, color: Colors.white, size: 20),
SizedBox(width: 8),
Expanded(
child: Text('请切换到远程模式后再开始作业', style: TextStyle(color: Colors.white, fontSize: 14)),
),
],
),
),
),
2026-02-26 14:08:02 +08:00
2026-03-06 17:19:26 +08:00
if (_isWorkPanelOpen) _buildWorkPanel(),
if (_isVideoDialogOpen) _buildVideoPopup(),
],
),
),
);
},
2026-02-25 20:00:33 +08:00
);
}
}
// 转换单个WGS84坐标到GCJ02
LatLng convertWGS84ToGCJ02(double lat, double lon) {
return wgs84ToGcj02(lat, lon);
}
/// =======================================================
/// 坐标转换:WGS84 -> GCJ02(国内高德/腾讯通用)
/// =======================================================
2026-03-11 09:44:08 +08:00
const double _pi = 3.1415926535897932384626;
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);
2026-02-25 20:00:33 +08:00
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);
}
2026-03-06 17:19:26 +08:00
List<LatLng> batchWgs84ToGcj02(List<LatLng> wgs84Points) {
// 遍历每个点,调用你已有的单个转换方法
return wgs84Points.map((point) {
return wgs84ToGcj02(point.latitude, point.longitude);
}).toList();
}
2026-03-07 20:03:33 +08:00
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;
}
2026-03-11 12:54:42 +08:00
LatLng gcj02ToWgs84(double lat, double lon, {int iterations = 10}) {
//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);
2026-02-28 14:58:25 +08:00
if (_outOfChina(lat, lon)) {
return LatLng(lat, lon);
}
2026-03-11 12:54:42 +08:00
double initLat = lat;
double initLon = lon;
double delta = 1e-6; // 迭代精度(约0.1米)
for (int i = 0; i < iterations; i++) {
// 1. 用当前猜测值计算GCJ02坐标
LatLng gcjGuess = wgs84ToGcj02(initLat, initLon);
// 2. 计算与原始GCJ02的差值
double dLat = gcjGuess.latitude - lat;
double dLon = gcjGuess.longitude - lon;
// 3. 修正猜测值
initLat -= dLat;
initLon -= dLon;
// 4. 精度达标则提前退出
if (sqrt(dLat * dLat + dLon * dLon) < delta) {
break;
}
}
return LatLng(initLat, initLon);
2026-02-28 14:58:25 +08:00
}
bool _outOfChina(double lat, double lon) {
2026-02-25 20:00:33 +08:00
return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
}
double _transformLat(double x, double y) {
2026-02-28 16:20:21 +08:00
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(x.abs());
2026-02-25 20:00:33 +08:00
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) {
2026-02-28 16:20:21 +08:00
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(x.abs());
2026-02-25 20:00:33 +08:00
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;
2026-02-28 16:20:21 +08:00
ret += (150.0 * sin(x / 12.0 * _pi) + 300.0 * sin(x / 30.0 * _pi)) * 2.0 / 3.0;
return ret;
}