867 lines
32 KiB
Dart
867 lines
32 KiB
Dart
import 'dart:async';
|
||
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/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/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';
|
||
|
||
class PlotData {
|
||
final String id; // 唯一ID,用于删除
|
||
final String plotName; // 地块名称
|
||
final String imageUrl; // 图片URL(本地/assets/网络都可)
|
||
|
||
PlotData({required this.id, required this.plotName, required this.imageUrl});
|
||
}
|
||
|
||
class MapPageEnterprise extends StatefulWidget {
|
||
const MapPageEnterprise({Key? key}) : super(key: key);
|
||
|
||
@override
|
||
State<MapPageEnterprise> createState() => _MapPageEnterpriseState();
|
||
}
|
||
|
||
class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||
final MapController _mapController = MapController();
|
||
final TextEditingController _plotNameController = TextEditingController();
|
||
|
||
LatLng? _currentLatLng;
|
||
StreamSubscription<Position>? _positionSub;
|
||
bool _hasMovedOnce = false;
|
||
bool _isPanelOpen = false; // 控制面板显示/隐藏
|
||
bool _directionBoxOpen = false; //航线面板显示/隐藏
|
||
final bool _saveBoxOpen = true; //保存按钮显示/隐藏
|
||
bool _isListBoxOpen = false; //列表面板显示/隐藏
|
||
|
||
RobotMode _currentRobotMode = RobotMode.point; //打点模式
|
||
AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域
|
||
WorkMode? _currentWorkMode = WorkMode.bow; // 当前作业模式
|
||
|
||
// ========== 核心新增状态 ==========
|
||
final List<LatLng> _markedPoints = []; // 存储所有打点坐标
|
||
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
|
||
final double _headingAngle = 0.0; // 当前机器航向角(单位:度)
|
||
|
||
//打点
|
||
double _workDistance = 0.0; // 作业行距(单位:米)
|
||
double _angle = 0.0; // 航线方向角(单位:度)
|
||
// =================================
|
||
//测试数据
|
||
final List<PlotData> _plotList = [
|
||
PlotData(
|
||
id: '1',
|
||
plotName: '北地块(一号田)',
|
||
imageUrl: '', // 本地图片
|
||
// 网络图片示例:imageUrl: 'https://xxx.com/plot1.jpg',
|
||
),
|
||
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: ''),
|
||
];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_initLocationEnterprise();
|
||
// 监听地图移动事件,实时更新连线
|
||
_mapController.mapEventStream.listen((event) {
|
||
if (event is MapEventMove || event is MapEventMoveEnd) {
|
||
if (mounted && _mapController.camera != null) {
|
||
// 核心:直接获取原始经纬度,不做任何截断,保留最大精度
|
||
final originalCenter = _mapController.camera!.center;
|
||
// 显式创建新的 LatLng 对象,确保精度不丢失(避免引用传递导致的隐式截断)
|
||
final highPrecisionCenter = LatLng(
|
||
originalCenter.latitude, // 原始纬度(保留全部小数位)
|
||
originalCenter.longitude, // 原始经度(保留全部小数位)
|
||
);
|
||
|
||
setState(() {
|
||
_mapCenter = highPrecisionCenter;
|
||
});
|
||
|
||
// 可选:打印验证精度(保留15位小数)
|
||
debugPrint(
|
||
'地图中心更新(高精度):纬度=${_mapCenter.latitude.toStringAsFixed(15)}, 经度=${_mapCenter.longitude.toStringAsFixed(15)}',
|
||
);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
void _showSavePlotDialog() {
|
||
// 清空上次的输入内容
|
||
_plotNameController.clear();
|
||
|
||
// 显示弹窗
|
||
showDialog(
|
||
context: context,
|
||
builder: (BuildContext context) {
|
||
return AlertDialog(
|
||
title: const Text('保存地块'),
|
||
content: TextField(
|
||
controller: _plotNameController,
|
||
decoration: const InputDecoration(
|
||
hintText: '请输入地块名称(如:北地块、一号田)',
|
||
border: OutlineInputBorder(),
|
||
labelText: '地块名称',
|
||
),
|
||
),
|
||
actions: [
|
||
// 取消按钮
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop(); // 关闭弹窗
|
||
},
|
||
child: const Text('取消'),
|
||
),
|
||
// 确认保存按钮
|
||
TextButton(
|
||
onPressed: () {
|
||
// 获取输入的地块名称并去除首尾空格
|
||
String plotName = _plotNameController.text.trim();
|
||
|
||
// 验证输入是否为空
|
||
if (plotName.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('地块名称不能为空!'),
|
||
backgroundColor: Colors.red,
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 执行保存逻辑
|
||
_savePlotData(plotName);
|
||
|
||
// 关闭弹窗
|
||
Navigator.of(context).pop();
|
||
|
||
// 保存成功提示
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('地块「$plotName」保存成功!'),
|
||
backgroundColor: Colors.green,
|
||
),
|
||
);
|
||
},
|
||
child: const Text('保存'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
// 3. 保存地块数据的核心方法(这里写你的实际保存逻辑)
|
||
void _savePlotData(String plotName) {
|
||
// 示例:打印保存的信息(实际开发中替换为存数据库/本地存储/接口调用)
|
||
debugPrint('===== 保存地块数据 =====');
|
||
debugPrint('地块名称:$plotName');
|
||
debugPrint('打点数量:${_markedPoints.length}');
|
||
debugPrint('打点坐标:$_markedPoints');
|
||
debugPrint('作业模式:$_currentWorkMode');
|
||
|
||
// 这里可以添加实际的保存逻辑:
|
||
// 1. 存到本地数据库(如Hive/SQLite)
|
||
// 2. 调用API上传到服务器
|
||
// 3. 保存到SharedPreferences(简单数据)
|
||
}
|
||
|
||
List<LatLng> _getPolygonPoints() {
|
||
if (_markedPoints.isEmpty || _mapController.camera == null) return [];
|
||
|
||
List<LatLng> polygonPoints = List.from(_markedPoints);
|
||
polygonPoints.add(_mapCenter);
|
||
return polygonPoints;
|
||
}
|
||
|
||
void _undoLastPoint() {
|
||
if (_markedPoints.isNotEmpty) {
|
||
setState(() {
|
||
_markedPoints.removeLast(); // 删除最后一个打点
|
||
});
|
||
debugPrint('撤回成功,剩余打点数量:${_markedPoints.length}');
|
||
} else {
|
||
debugPrint('无打点可撤回');
|
||
// 可选:提示用户
|
||
ScaffoldMessenger.of(
|
||
context,
|
||
).showSnackBar(const SnackBar(content: Text('暂无打点可撤回')));
|
||
}
|
||
}
|
||
|
||
// ========== 核心:清空所有打点和轨迹 ==========
|
||
void _deleteAllPoints() {
|
||
if (_markedPoints.isNotEmpty) {
|
||
setState(() {
|
||
_markedPoints.clear(); // 清空所有打点
|
||
});
|
||
debugPrint('删除成功,已清空所有打点和轨迹');
|
||
// 可选:提示用户
|
||
ScaffoldMessenger.of(
|
||
context,
|
||
).showSnackBar(const SnackBar(content: Text('已清空所有打点和轨迹')));
|
||
} else {
|
||
debugPrint('无打点可删除');
|
||
ScaffoldMessenger.of(
|
||
context,
|
||
).showSnackBar(const SnackBar(content: Text('暂无打点可删除')));
|
||
}
|
||
}
|
||
|
||
/// ===============================
|
||
/// 企业级定位初始化(已修复坐标系)
|
||
/// ===============================
|
||
Future<void> _initLocationEnterprise() async {
|
||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||
if (!serviceEnabled) return;
|
||
|
||
LocationPermission permission = await Geolocator.checkPermission();
|
||
if (permission == LocationPermission.denied) {
|
||
permission = await Geolocator.requestPermission();
|
||
}
|
||
if (permission != LocationPermission.whileInUse &&
|
||
permission != LocationPermission.always) {
|
||
return;
|
||
}
|
||
|
||
// 1️⃣ 首次强制获取定位
|
||
final position = await Geolocator.getCurrentPosition(
|
||
desiredAccuracy: LocationAccuracy.bestForNavigation,
|
||
);
|
||
|
||
final gcj = wgs84ToGcj02(position.latitude, position.longitude);
|
||
_updateLocation(gcj, moveMap: true);
|
||
|
||
// 2️⃣ 实时监听(不再强制移动地图)
|
||
_positionSub =
|
||
Geolocator.getPositionStream(
|
||
locationSettings: const LocationSettings(
|
||
accuracy: LocationAccuracy.bestForNavigation,
|
||
distanceFilter: 2,
|
||
),
|
||
).listen((pos) {
|
||
final gcj = wgs84ToGcj02(pos.latitude, pos.longitude);
|
||
_updateLocation(gcj, moveMap: false);
|
||
});
|
||
}
|
||
|
||
/// ===============================
|
||
/// 更新位置
|
||
/// ===============================
|
||
void _updateLocation(LatLng latLng, {bool moveMap = false}) {
|
||
setState(() {
|
||
_currentLatLng = latLng;
|
||
});
|
||
|
||
if (moveMap && !_hasMovedOnce) {
|
||
_hasMovedOnce = true;
|
||
_mapController.move(latLng, 17);
|
||
}
|
||
}
|
||
|
||
/// 手动回到当前位置
|
||
void _moveToCurrentLocation() {
|
||
if (_currentLatLng == null) return;
|
||
_mapController.move(_currentLatLng!, 17);
|
||
}
|
||
|
||
// ========== 核心方法:打点逻辑 ==========
|
||
void _addMarkedPoint() {
|
||
setState(() {
|
||
_markedPoints.add(_mapCenter); // 在地图中心打点
|
||
});
|
||
debugPrint(
|
||
'$_markedPoints,新增打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude.toStringAsFixed(20)}, ${_mapCenter.longitude.toStringAsFixed(20)}',
|
||
);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_positionSub?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
// 在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),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
|
||
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(),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// 实现删除确认弹窗
|
||
void _showDeleteConfirmDialog(PlotData plot) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) {
|
||
return AlertDialog(
|
||
title: const Text('确认删除'),
|
||
content: Text('是否删除地块「${plot.plotName}」?删除后不可恢复。'),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
},
|
||
child: const Text('取消'),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
// 执行删除逻辑
|
||
setState(() {
|
||
_plotList.removeWhere((item) => item.id == plot.id);
|
||
});
|
||
|
||
// 关闭弹窗
|
||
Navigator.of(context).pop();
|
||
|
||
// 提示删除成功
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('已删除地块「${plot.plotName}」'),
|
||
backgroundColor: Colors.green,
|
||
),
|
||
);
|
||
},
|
||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final screenHeight = MediaQuery.of(context).size.height;
|
||
final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56
|
||
final maxTop = screenHeight - menuHeight;
|
||
return Scaffold(
|
||
body: SafeArea(
|
||
child: Stack(
|
||
children: [
|
||
// 地图核心组件
|
||
FlutterMap(
|
||
mapController: _mapController,
|
||
options: MapOptions(
|
||
initialCenter:
|
||
_currentLatLng ?? const LatLng(39.9042, 116.4074),
|
||
initialZoom: 15,
|
||
maxZoom: 18,
|
||
// 禁止地图点击事件(避免和中心标冲突)
|
||
onTap: (_, __) {}, // 空实现,禁用地图点击响应
|
||
),
|
||
children: [
|
||
/// 高德瓦片(GCJ-02)
|
||
TileLayer(
|
||
urlTemplate:
|
||
'https://webrd02.is.autonavi.com/appmaptile'
|
||
'?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1'
|
||
'&key=bbb1f0f20eed6bf679eddf2625630aba',
|
||
),
|
||
|
||
/// ========== 新增:中心标与历史打点的虚线连线 ==========
|
||
///
|
||
/// if (_markedPoints.length >= 2)
|
||
PolylineLayer(
|
||
polylines: [
|
||
for (int i = 0; i < _markedPoints.length - 1; i++)
|
||
Polyline(
|
||
points: [_markedPoints[i], _markedPoints[i + 1]],
|
||
color: Colors.orange.withOpacity(0.5),
|
||
strokeWidth: 1.5,
|
||
),
|
||
],
|
||
),
|
||
if (_markedPoints.isNotEmpty)
|
||
PolylineLayer(
|
||
polylines: [
|
||
Polyline(
|
||
points: [_mapCenter, _markedPoints.last],
|
||
color: Colors.blue.withOpacity(0.5),
|
||
strokeWidth: 1.5,
|
||
),
|
||
],
|
||
),
|
||
|
||
/// 当前定位 Marker
|
||
if (_currentLatLng != null)
|
||
MarkerLayer(
|
||
markers: [
|
||
Marker(
|
||
point: _currentLatLng!,
|
||
width: 40,
|
||
height: 40,
|
||
child: CustomPaint(
|
||
size: const Size(40, 40),
|
||
painter: HeadingMarkerPainter(
|
||
headingAngle: _headingAngle,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
if (_currentWorkMode == WorkMode.bow &&
|
||
_markedPoints.isNotEmpty)
|
||
PolygonLayer(
|
||
polygons: [
|
||
Polygon(
|
||
points: _getPolygonPoints(),
|
||
color: Colors.green.withOpacity(0.2),
|
||
borderColor: Colors.green.withOpacity(0.5),
|
||
borderStrokeWidth: 1,
|
||
isFilled: true,
|
||
),
|
||
],
|
||
),
|
||
|
||
/// ========== 新增:历史打点的绿色标记 ==========
|
||
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(),
|
||
),
|
||
],
|
||
),
|
||
|
||
/// ========== 核心:地图正中心固定定位标 ==========
|
||
Positioned(
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
child: Center(
|
||
// 自定义十字标(中心精准对齐地图中心)
|
||
child: SizedBox(
|
||
width: 16, // 十字整体宽度
|
||
height: 16, // 十字整体高度
|
||
child: CustomPaint(
|
||
painter: CrosshairPainter(), // 自定义十字画笔
|
||
),
|
||
),
|
||
),
|
||
),
|
||
//保存按钮
|
||
if (_saveBoxOpen)
|
||
Positioned(
|
||
left: 16,
|
||
top: maxTop > 10 ? 11 : maxTop,
|
||
child: // 改用小尺寸按钮容器,替代默认大尺寸FloatingActionButton
|
||
Container(
|
||
width: 40, // 按钮宽度(默认FAB是56,改为40更小巧)
|
||
height: 40, // 按钮高度
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF00C853), // 保留绿色主题
|
||
shape: BoxShape.circle, // 圆形
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black12,
|
||
blurRadius: 4, // 对应elevation:4的阴影效果
|
||
offset: const Offset(0, 2), // 阴影偏移
|
||
),
|
||
],
|
||
),
|
||
child: IconButton(
|
||
onPressed: () {
|
||
_showSavePlotDialog();
|
||
//// 点击保存按钮的逻辑
|
||
//// 1. 可以直接调用保存方法
|
||
//_saveCurrentPath();
|
||
//// 2. 也可以弹出输入框让用户输入路径名称(推荐)
|
||
//_showSavePathDialog();
|
||
},
|
||
icon: const Icon(
|
||
Icons.save,
|
||
color: Colors.white,
|
||
size: 18, // 图标尺寸缩小(原20→18)
|
||
),
|
||
padding: EdgeInsets.zero, // 移除IconButton默认内边距
|
||
constraints: const BoxConstraints(), // 解除IconButton尺寸限制
|
||
),
|
||
),
|
||
),
|
||
// 右侧悬浮菜单
|
||
Positioned(
|
||
height: 336,
|
||
right: 16,
|
||
top: maxTop > 10 ? 11 : maxTop,
|
||
child: FloatingActionButton(
|
||
onPressed: _moveToCurrentLocation,
|
||
child: new VerticalFloatMenu(
|
||
onEditTap: (bool isOpen) {
|
||
setState(() {
|
||
_isPanelOpen = isOpen;
|
||
});
|
||
},
|
||
onListBox: (bool isOpen) {
|
||
setState(() {
|
||
_isListBoxOpen = isOpen;
|
||
});
|
||
},
|
||
onWorkModeSelected: (mode) {
|
||
_currentWorkMode = mode;
|
||
debugPrint('外部收到作业模式:$mode');
|
||
},
|
||
),
|
||
),
|
||
),
|
||
|
||
// 底部操作面板
|
||
if (_isPanelOpen)
|
||
Positioned(
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
child: BottomOperationPanel(
|
||
initialRobotMode: RobotMode.point,
|
||
initialAreaMode: AreaMode.work,
|
||
onComplete: () {
|
||
debugPrint(
|
||
'操作完成回调:当前机器人模式=$_currentRobotMode,当前区域模式=$_currentAreaMode,当前作业模式:$_currentWorkMode,作业区域点:$_markedPoints,作业行距=$_workDistance,航线方向角=$_angle',
|
||
);
|
||
setState(() => _isPanelOpen = false);
|
||
},
|
||
onUndoTap: _undoLastPoint,
|
||
onDeleteTap: _deleteAllPoints,
|
||
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)}米',
|
||
);
|
||
},
|
||
onSettingTap: () {
|
||
setState(() {
|
||
_directionBoxOpen = true;
|
||
});
|
||
debugPrint('外部处理设置按钮点击');
|
||
},
|
||
onLandTap: () {
|
||
debugPrint('外部处理地块标签点击');
|
||
},
|
||
onRouteTap: () {
|
||
debugPrint('外部处理航线标签点击');
|
||
},
|
||
),
|
||
),
|
||
|
||
// 航线方向面板
|
||
if (_directionBoxOpen)
|
||
Positioned(
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
child: RouteDirectionPanel(
|
||
initialOptimalHeading: true,
|
||
initialDirection: 0.0,
|
||
onConfirm: (result) {
|
||
debugPrint(
|
||
'最优航向:${result['optimalHeading']},角度:${result['direction']}',
|
||
);
|
||
_angle = result['optimalHeading'] == true
|
||
? -1
|
||
: result['direction']; // 更新航线方向角状态
|
||
debugPrint('外部处理航线方向设置,当前角度:${_angle}');
|
||
setState(() => _directionBoxOpen = false);
|
||
},
|
||
onCancel: () {
|
||
setState(() => _directionBoxOpen = false);
|
||
},
|
||
),
|
||
),
|
||
// 在build方法的Stack中添加抽屉组件
|
||
if (_isListBoxOpen)
|
||
// 半透明背景 + 底部抽屉
|
||
Positioned.fill(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
// 1. 半透明遮罩(点击关闭抽屉)
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () {
|
||
setState(() {
|
||
_isListBoxOpen = false;
|
||
});
|
||
},
|
||
child: Container(color: Colors.black.withOpacity(0.3)),
|
||
),
|
||
),
|
||
|
||
// 2. 底部抽屉主体
|
||
Container(
|
||
width: double.infinity,
|
||
decoration: const BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(16), // 顶部圆角
|
||
),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black12,
|
||
blurRadius: 10,
|
||
offset: Offset(0, -2),
|
||
),
|
||
],
|
||
),
|
||
// 限制抽屉高度(可自定义)
|
||
constraints: const BoxConstraints(
|
||
maxHeight: 600,
|
||
minHeight: 200,
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// 顶部标题栏
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 16,
|
||
vertical: 12,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
// 标题:显示数据总数
|
||
Text(
|
||
'地块列表(共${_plotList.length}条)',
|
||
style: const TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
|
||
// 关闭按钮
|
||
IconButton(
|
||
onPressed: () {
|
||
setState(() {
|
||
_isListBoxOpen = false;
|
||
});
|
||
},
|
||
icon: const Icon(
|
||
Icons.close,
|
||
color: Colors.grey,
|
||
size: 20,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
const Divider(height: 1, color: Colors.grey),
|
||
|
||
// 列表内容(可滚动)
|
||
Expanded(
|
||
child: _plotList.isEmpty
|
||
? // 空数据提示
|
||
const Center(
|
||
child: Column(
|
||
mainAxisAlignment:
|
||
MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.inbox_outlined,
|
||
color: Colors.grey,
|
||
size: 48,
|
||
),
|
||
SizedBox(height: 16),
|
||
Text(
|
||
'暂无地块数据',
|
||
style: TextStyle(
|
||
color: Colors.grey,
|
||
fontSize: 16,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: // 列表项
|
||
ListView.builder(
|
||
padding: const EdgeInsets.symmetric(
|
||
vertical: 8,
|
||
),
|
||
itemCount: _plotList.length,
|
||
itemBuilder: (context, index) {
|
||
final plot = _plotList[index];
|
||
return _buildPlotListItem(plot);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// =======================================================
|
||
/// 坐标转换:WGS84 -> GCJ02(国内高德/腾讯通用)
|
||
/// =======================================================
|
||
const double _pi = 3.14159265358979324;
|
||
const double _a = 6378245.0;
|
||
const double _ee = 0.00669342162296594323;
|
||
|
||
LatLng wgs84ToGcj02(double lat, double lon) {
|
||
if (_outOfChina(lat, lon)) {
|
||
return LatLng(lat, lon);
|
||
}
|
||
double dLat = _transformLat(lon - 105.0, lat - 35.0);
|
||
double dLon = _transformLon(lon - 105.0, lat - 35.0);
|
||
double radLat = lat / 180.0 * _pi;
|
||
double magic = sin(radLat);
|
||
magic = 1 - _ee * magic * magic;
|
||
double sqrtMagic = sqrt(magic);
|
||
dLat = (dLat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrtMagic) * _pi);
|
||
dLon = (dLon * 180.0) / (_a / sqrtMagic * cos(radLat) * _pi);
|
||
double mgLat = lat + dLat;
|
||
double mgLon = lon + dLon;
|
||
return LatLng(mgLat, mgLon);
|
||
}
|
||
|
||
bool _outOfChina(double lat, double lon) {
|
||
return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
|
||
}
|
||
|
||
double _transformLat(double x, double y) {
|
||
double ret =
|
||
-100.0 +
|
||
2.0 * x +
|
||
3.0 * y +
|
||
0.2 * y * y +
|
||
0.1 * x * y +
|
||
0.2 * sqrt(x.abs());
|
||
ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0;
|
||
ret += (20.0 * sin(y * _pi) + 40.0 * sin(y / 3.0 * _pi)) * 2.0 / 3.0;
|
||
ret += (160.0 * sin(y / 12.0 * _pi) + 320 * sin(y * _pi / 30.0)) * 2.0 / 3.0;
|
||
return ret;
|
||
}
|
||
|
||
double _transformLon(double x, double y) {
|
||
double ret =
|
||
300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(x.abs());
|
||
ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0;
|
||
ret += (20.0 * sin(x * _pi) + 40.0 * sin(x / 3.0 * _pi)) * 2.0 / 3.0;
|
||
ret +=
|
||
(150.0 * sin(x / 12.0 * _pi) + 300.0 * sin(x / 30.0 * _pi)) * 2.0 / 3.0;
|
||
return ret;
|
||
}
|