加上tracepoint版本
This commit is contained in:
@@ -1,17 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.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/home/presentation/widgets/BottomDirectionLine.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.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 '../path_list_pages.dart';
|
||||
|
||||
// 定义轨迹点类型(经纬度)
|
||||
typedef PlotPoint = LatLng;
|
||||
|
||||
class PlotData {
|
||||
final String id; // 唯一ID,用于删除
|
||||
final String plotName; // 地块名称
|
||||
@@ -28,8 +36,13 @@ class MapPageEnterprise extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
final _dispatcher = sl<NetMessageDispatcher>();
|
||||
late StreamSubscription<RawPacket> _sub;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
final TextEditingController _plotNameController = TextEditingController();
|
||||
// 初始化轨迹管理器(泛型指定为LatLng)
|
||||
late final TracePoint<PlotPoint> _traceManager;
|
||||
|
||||
LatLng? _currentLatLng;
|
||||
StreamSubscription<Position>? _positionSub;
|
||||
@@ -38,6 +51,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
bool _directionBoxOpen = false; //航线面板显示/隐藏
|
||||
final bool _saveBoxOpen = true; //保存按钮显示/隐藏
|
||||
bool _isListBoxOpen = false; //列表面板显示/隐藏
|
||||
// 新增:存储选中的地块(null表示未选中)
|
||||
PlotData? _selectedPlot;
|
||||
// 新增:控制底部作业面板显示
|
||||
bool _isWorkPanelOpen = false;
|
||||
|
||||
RobotMode _currentRobotMode = RobotMode.point; //打点模式
|
||||
AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域
|
||||
@@ -47,6 +64,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
final List<LatLng> _markedPoints = []; // 存储所有打点坐标
|
||||
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
|
||||
final double _headingAngle = 0.0; // 当前机器航向角(单位:度)
|
||||
List<LatLng>? _currentTracePoints; //收到机器的点坐标
|
||||
|
||||
//打点
|
||||
double _workDistance = 0.0; // 作业行距(单位:米)
|
||||
@@ -62,17 +80,34 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
),
|
||||
PlotData(id: '2', plotName: '东地块(二号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '4', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '5', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '6', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '7', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '8', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_traceManager = TracePoint<PlotPoint>();
|
||||
_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) {
|
||||
@@ -98,6 +133,38 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
// 清空上次的输入内容
|
||||
_plotNameController.clear();
|
||||
@@ -290,69 +357,114 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
@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);
|
||||
}
|
||||
|
||||
// 在State类中添加列表项构建方法
|
||||
Widget _buildPlotListItem(PlotData plot) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[100]!),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 1. 地块图片
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
plot.imageUrl,
|
||||
// 网络图片替换为:Image.network(plot.imageUrl, ...)
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
// 图片加载失败占位
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.image_outlined, color: Colors.grey),
|
||||
);
|
||||
return GestureDetector(
|
||||
// 核心:点击列表项触发选中逻辑
|
||||
onTap: () {
|
||||
setState(() {
|
||||
// 1. 选中当前地块
|
||||
_selectedPlot = plot;
|
||||
// 2. 关闭列表抽屉
|
||||
_isListBoxOpen = false;
|
||||
// 3. 打开底部作业面板
|
||||
_isWorkPanelOpen = true;
|
||||
});
|
||||
|
||||
//// 可选:提示选中成功
|
||||
//ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// content: Text('已选中地块:${plot.plotName}'),
|
||||
// duration: const Duration(seconds: 1),
|
||||
// ),
|
||||
//);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
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: [
|
||||
// 1. 地块图片(原有代码)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
plot.imageUrl,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.image_outlined, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 2. 地块名称(原有代码)
|
||||
Expanded(
|
||||
child: Text(
|
||||
plot.plotName,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black87),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 3. 删除按钮(原有代码)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_showDeleteConfirmDialog(plot);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.redAccent,
|
||||
size: 20,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 2. 地块名称(占满剩余空间)
|
||||
Expanded(
|
||||
child: Text(
|
||||
plot.plotName,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black87),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 3. 删除按钮
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// 弹出确认删除对话框
|
||||
_showDeleteConfirmDialog(plot);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.redAccent,
|
||||
size: 20,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -398,6 +510,139 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
);
|
||||
}
|
||||
|
||||
// 注意:方法名改为 _buildWorkPanel(因为实际是作业面板,不是列表,避免混淆)
|
||||
Widget _buildWorkPanel() {
|
||||
// 核心修复1:空安全判断 - 无选中地块时返回空容器
|
||||
if (_selectedPlot == 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.asset(
|
||||
_selectedPlot!.imageUrl,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
// 核心修复2:图片加载错误占位优化
|
||||
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: Text(
|
||||
_selectedPlot!.plotName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 关闭按钮(修复:减少点击区域,避免误触)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isWorkPanelOpen = false;
|
||||
_selectedPlot = null; // 清空选中状态
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.close, color: Colors.grey, size: 20),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
minHeight: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// 核心修复3:再次空安全校验(双重保险)
|
||||
if (_selectedPlot != null) {
|
||||
//_startWork(_selectedPlot!);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00C853), // 绿色主题
|
||||
foregroundColor: Colors.white, // 文字颜色(Flutter 3.0+ 推荐)
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black12,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: 16,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text(
|
||||
'开始作业',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
@@ -607,6 +852,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
),
|
||||
),
|
||||
),
|
||||
//开始作业面板
|
||||
|
||||
// 底部操作面板
|
||||
if (_isPanelOpen)
|
||||
@@ -806,6 +1052,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (_isWorkPanelOpen) _buildWorkPanel(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user