高清地图+无人机实时信息显示异常问题修复

This commit is contained in:
2026-08-19 14:57:24 +08:00
parent ab75e8c5b0
commit 83220869da
7 changed files with 397 additions and 146 deletions

View File

@@ -46,19 +46,19 @@ class DroneOsdDataSourceImpl implements DroneOsdDataSource {
}) async {
if (_isDisposed) return;
// 同一设备(SN 相同):只增加引用计数,共享订阅
// 🔥 同一设备(SN 相同):只增加引用计数,不重新订阅
if (_listenerCount > 0 &&
_deviceSn == deviceSn &&
_gatewaySn == gatewaySn &&
_subscription != null) {
_gatewaySn == gatewaySn) {
_listenerCount++;
debugPrint('[DroneOsdDataSource] 引用计数+1: $_listenerCount');
debugPrint('[DroneOsdDataSource] 引用计数+1: $_listenerCount (设备相同,不重新订阅)');
return;
}
// 切换到不同设备:强制清理旧订阅
// 不管引用计数多少,SN 变了就必须先取消旧订阅再订阅新的
if (_deviceSn != null || _gatewaySn != null) {
// 🔥 切换到不同设备或首次订阅时才清理旧订阅
// 重要:只有在设备不同的情况下才调用 stopListening,避免误取消当前订阅
final needCleanup = _deviceSn != null || _gatewaySn != null;
if (needCleanup) {
// 重置引用计数为 1,让 stopListening 能真正取消 MQTT 订阅
_listenerCount = 1;
await stopListening();

View File

@@ -0,0 +1,133 @@
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
/// GeoServer 基地址(公司端)
const String kGeoserverBase = 'http://1.95.137.212:7900/geoserver';
/// 首期苏州区域图层标识
const String kSuzhouLayerName = 'workSpace:suzhou';
/// 苏州图层实际覆盖范围(取自 GetCapabilities,约 250m × 190m 的小地块)
final LatLngBounds kSuzhouBounds = LatLngBounds(
const LatLng(31.082450793459877, 120.83906484264836),
const LatLng(31.084141983135456, 120.84164596446476),
);
/// 场站中心点(真实地块中心)
const LatLng kSuzhouCenter = LatLng(31.083296, 120.840355);
/// 叠加层取瓦片方式开关:
/// - false(当前):WMS GetMap 兜底,不依赖 GWC 缓存
/// - true:WMTS GetTile(公司补配 GWC EPSG:3857 gridset 并 Seed 后切换)
const bool kUseWmtsTile = false;
/// WMS 取图提供器:
/// 实测 GeoServer 对 workSpace:suzhou 只有原生 EPSG:4326 能出图,
/// 请求 EPSG:3857(WMS 1.1.1 / 1.3.0 均验证过)返回纯白图。
/// 因此这里按瓦片 XYZ 反算经纬度 bbox,以 EPSG:4326 请求 WMS。
class GeoWmsProvider extends TileProvider {
GeoWmsProvider({this.maxLevel = 21});
final int maxLevel;
/// Web 墨卡托地球周长(米)
static const double _full = 40075016.685578488;
/// Web 墨卡托 Y(米)→ 纬度(度)
static double _mercatorYToLat(double y) {
final t = y / 6378137.0;
final sinh = (math.exp(t) - math.exp(-t)) / 2; // dart:math 无 sinh,手动实现
return math.atan(sinh) * 180 / math.pi;
}
@override
ImageProvider<Object> getImage(
TileCoordinates coordinates,
TileLayer options,
) {
if (coordinates.z > maxLevel) {
return MemoryImage(TileProvider.transparentImage);
}
final span = _full / math.pow(2, coordinates.z);
final westM = -_full / 2 + coordinates.x * span;
final eastM = westM + span;
final northM = _full / 2 - coordinates.y * span;
final southM = northM - span;
final westLng = westM / _full * 360;
final eastLng = eastM / _full * 360;
final southLat = _mercatorYToLat(southM);
final northLat = _mercatorYToLat(northM);
// 与图层实际范围不相交的瓦片直接返回透明图,不发无谓请求
final intersects = westLng < kSuzhouBounds.east &&
eastLng > kSuzhouBounds.west &&
southLat < kSuzhouBounds.north &&
northLat > kSuzhouBounds.south;
if (!intersects) {
return MemoryImage(TileProvider.transparentImage);
}
final url = Uri.parse('$kGeoserverBase/wms').replace(
queryParameters: {
'SERVICE': 'WMS',
'REQUEST': 'GetMap',
'VERSION': '1.1.1',
'LAYERS': kSuzhouLayerName,
'BBOX': '${westLng.toStringAsFixed(6)},${southLat.toStringAsFixed(6)},'
'${eastLng.toStringAsFixed(6)},${northLat.toStringAsFixed(6)}',
'WIDTH': '256',
'HEIGHT': '256',
'FORMAT': 'image/png',
'TRANSPARENT': 'true',
'SRS': 'EPSG:4326',
'STYLES': '',
},
);
return NetworkImage(url.toString());
}
}
/// GeoServer 苏州无人机高清影像叠加层(WGS84 基准,覆盖在 ESRI 底图上)
///
/// 地块很小(约 250m),zoom 16 以上才有意义,默认 minZoom = 16
class GeoSuzhouOverlay extends StatelessWidget {
const GeoSuzhouOverlay({super.key, this.minZoom = 16, this.maxZoom = 21});
/// 小于该级别不请求瓦片(地块太小,低层级看不见)
final double minZoom;
/// 大于该级别不请求瓦片
final double maxZoom;
@override
Widget build(BuildContext context) {
if (kUseWmtsTile) {
// === WMTS 版:公司把 EPSG:3857 gridset 加入 GWC 并 Seed 后生效 ===
return TileLayer(
urlTemplate: '$kGeoserverBase/gwc/service/wmts'
'?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0'
'&LAYER=$kSuzhouLayerName&TILEMATRIXSET=EPSG:3857'
'&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=image/png',
userAgentPackageName: 'com.example.app',
minZoom: minZoom,
maxZoom: maxZoom,
maxNativeZoom: maxZoom.toInt(),
tileBounds: kSuzhouBounds,
);
}
// === WMS 版(当前兜底):按原生 EPSG:4326 请求,GeoServer 实时渲染 ===
return TileLayer(
tileProvider: GeoWmsProvider(maxLevel: maxZoom.toInt()),
userAgentPackageName: 'com.example.app',
minZoom: minZoom,
maxZoom: maxZoom,
maxNativeZoom: maxZoom.toInt(),
tileBounds: kSuzhouBounds,
);
}
}

View File

@@ -156,11 +156,13 @@ class _AmapFlutterMapPageState extends State<AmapFlutterMapPage> {
interactiveFlags: InteractiveFlag.all,
),
children: [
// ESRI World Imagery(WGS84 坐标系,全球高清卫星图)
// 替代高德地图(GCJ02),统一坐标系,无需坐标转换
TileLayer(
urlTemplate:
"https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}&key=$amapKey",
subdomains: const ['1', '2', '3', '4'],
tileProvider: NetworkTileProvider(),
urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
userAgentPackageName: 'com.example.app',
minZoom: 0,
maxZoom: 19,
),
MarkerLayer(
markers: [

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'dart:math';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -48,6 +49,7 @@ import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/trace
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/map/dji_tile_layer.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/geoserver_suzhou_layer.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/obsToast.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/startpoint_area.dart';
import 'package:image/image.dart' as _image; // 注意命名空间冲突,使用 as img
@@ -490,18 +492,15 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}
// 2. 关键:同步机器人模式坐标(保存时边框不丢失)
// ⚠️ 地图已切换为 WGS84,不再需要坐标转换
if (_currentRobotMode == RobotMode.point && _robotModeWgsPoints.isEmpty) {
_robotModeWgsPoints = _markedPoints.map((p) {
return gcj02ToWgs84(p.latitude, p.longitude);
}).toList();
_robotModeWgsPoints = List.from(_markedPoints); // 直接使用 WGS84 坐标
}
// 兜底:Robot模式下_robotModeWgsPoints缓存未写入时,从_markedPoints恢复
if (_currentRobotMode == RobotMode.robot &&
_robotModeWgsPoints.isEmpty &&
_markedPoints.isNotEmpty) {
_robotModeWgsPoints = _markedPoints.map((p) {
return gcj02ToWgs84(p.latitude, p.longitude);
}).toList();
_robotModeWgsPoints = List.from(_markedPoints); // 直接使用 WGS84 坐标
}
if (mounted) {
@@ -931,7 +930,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
.toList();
}
return _markedPoints.map((point) {
final wgs84 = gcj02ToWgs84(point.latitude, point.longitude);
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
final wgs84 = LatLng(point.latitude, point.longitude);
return {'lng': wgs84.longitude, 'lat': wgs84.latitude};
}).toList();
}
@@ -944,14 +944,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
'outer': _currentWorkMode == WorkMode.custom
? [] // 自定义模式 outer 为空
: _markedPointsToWgs(), // 弓字模式 outer 为打点坐标
// 🔥 对齐 Web 端:弓字模式 path=API返回,自定义模式 path=打点
// 旧逻辑:自定义模式 path 为空数组
'path': _currentWorkMode == WorkMode.custom
? _markedPointsToWgs() // 自定义模式 path 为打点坐标
: typedPathList // 弓字模式 path 为 API 返回的路径
.map((point) => {'lng': point.longitude, 'lat': point.latitude})
.toList(),
// 🔥 统一使用用户手动打点的边界点,不再上传算法清扫轨迹点
'path': _markedPointsToWgs(),
'planModel': _currentWorkMode == WorkMode.bow ? 0 : 2, // 作业模式值
};
@@ -1425,10 +1419,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 保存当前障碍物组(Robot模式下直接用WGS84坐标)
if (_currentRobotMode == RobotMode.robot &&
_robotModeObsWgsPoints.isNotEmpty) {
final gcjPoints = _robotModeObsWgsPoints
.map((wgs) => wgs84ToGcj02(wgs.latitude, wgs.longitude))
.toList();
_obstacleHoles.add(gcjPoints);
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
_obstacleHoles.add(List.from(_robotModeObsWgsPoints));
_obstacleWgsHoles.add(List.from(_robotModeObsWgsPoints));
} else {
_obstacleHoles.add(List.from(_currentObstaclePoints));
@@ -1751,14 +1743,16 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
double? lat = safeToDouble(item['lat']);
double? lon = safeToDouble(item['lng'] ?? item['lon']);
if (lat != null && lon != null) {
newPathPoints.add(convertWGS84ToGCJ02(lat, lon));
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
newPathPoints.add(LatLng(lat, lon));
}
}
for (var item in outerList) {
double? lng = safeToDouble(item['lng'] ?? item['lon']);
double? lat = safeToDouble(item['lat']);
if (lng != null && lat != null) {
newOuterPoints.add(convertWGS84ToGCJ02(lat, lng));
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
newOuterPoints.add(LatLng(lat, lng));
}
}
@@ -1871,26 +1865,28 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
) {
_onDeviceData(lng, lat, obfFlag, headingStatus, controlMode);
final gcjPoint = wgs84ToGcj02(lat, lng);
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
final currentPoint = LatLng(lat, lng);
//final initCenter = bd09ToGcj02(
// '120.81992468426961', // 经度
// '32.04532826114155',
//);
// 🔥 修复1:初始化坐标改为直接使用有效GCJ02坐标,避免BD09转换风险
// 🔥 修复1:初始化坐标改为直接使用有效WGS84坐标
final initCenter = _getDefaultValidCenter();
_currentWgsLatLng = LatLng(lat, lng);
_traceManager.upsert(_currentWgsLatLng as PlotPoint, TPAction.UPDATE);
tracePoint = _traceManager.getTracePoint();
gctracePoint = batchWgs84ToGcj02(tracePoint!);
// ⚠️ 地图已切换为 WGS84,直接使用原始轨迹点
gctracePoint = List.from(tracePoint!);
// _logger.log("[当前轨迹模式][转换后gctracePoint]: ${gctracePoint!.length}");
// _logger.log("[当前轨迹模式]:${_traceManager.getMode()}"); // 这里必须加 ()
//_currentLatLng = gcjPoint; // ✅ 状态变量在setState内更新
if (_isValidLatLng(gcjPoint.latitude, gcjPoint.longitude)) {
_currentLatLng = gcjPoint;
//_currentLatLng = currentPoint; // ✅ 状态变量在setState内更新
if (_isValidLatLng(currentPoint.latitude, currentPoint.longitude)) {
_currentLatLng = currentPoint;
} else {
_currentLatLng = null;
}
@@ -1909,24 +1905,20 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
onTap: (_, __) {}, // 空实现,禁用地图点击响应
),
children: [
/// 高德瓦片(GCJ-02)
/// ESRI World Imagery(WGS84 坐标系,全球高清卫星图)
/// 替代高德地图(GCJ02),统一坐标系,无需坐标转换
TileLayer(
//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',
// 🔥 修复:适配高DPI(替代pixelRatio)
// tileSize: 512, // 高清瓦片尺寸(默认256,改为512适配scale=2)
tileProvider: NetworkTileProvider(
// 自定义瓦片请求头(可选,提升兼容性)
headers: {
'Referer': 'https://www.amap.com', // 高德要求的Referer
},
),
maxZoom: 18,
urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
userAgentPackageName: 'com.example.app',
minZoom: 0,
maxZoom: 19,
),
/// GeoServer 苏州无人机高清影像叠加层(WGS84 基准,无偏移)
/// 实际地块很小(约 250m,中心 31.0833/120.8404),需 zoom >= 16 才能看到
/// 当前 WMS 兜底(原生 EPSG:4326);公司补配 GWC EPSG:3857 后切 WMTS
/// 切换位置:geoserver_suzhou_layer.dart 的 kUseWmtsTile 开关
GeoSuzhouOverlay(),
//if (isDjMapShow) DJITileLayer(stationName: _currentStation.name, mapController: _mapController),
// 绘制path折线(Line模式)
@@ -2512,8 +2504,87 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
return;
}
// 🔥 步骤3: 任务确认成功,更新UI状态
debugPrint('🎯 [开始作业] 步骤3: 任务就绪, currentTaskId=${taskCubit.state.currentTaskId}');
// 🔥 新增:步骤2.5 - 调用 getTaskPath 接口获取完整路径数据
debugPrint('📍 [开始作业] 步骤2.5: 调用 getTaskPath 接口获取路径数据');
final currentTaskId = taskCubit.state.currentTaskId;
bool pathFetchSuccess = false;
List<LatLng>? fetchedPathPoints;
try {
final dio = sl<Dio>();
final response = await dio.get(
'http://1.95.137.212:8081/iot/taskPath/getTaskPath',
queryParameters: {
'taskId': currentTaskId,
},
);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
final code = data['code'];
final codeValue = code is int ? code : int.tryParse(code?.toString() ?? '');
if (codeValue == 200) {
final pathData = data['data'];
debugPrint('✅ [开始作业] getTaskPath 接口调用成功, data类型: ${pathData.runtimeType}');
if (pathData != null && pathData is Map<String, dynamic>) {
final pathList = pathData['path'];
if (pathList != null && pathList is List && pathList.isNotEmpty) {
debugPrint('✅ [开始作业] 获取到路径点数: ${pathList.length}');
// 将路径点渲染到地图上
fetchedPathPoints = pathList.map((point) {
final lat = (point['lat'] as num?)?.toDouble() ?? 0.0;
final lng = (point['lng'] ?? point['lon'] as num?)?.toDouble() ?? 0.0;
return LatLng(lat, lng);
}).toList();
// 渲染路径到地图
setState(() {
gcjPathPoints = fetchedPathPoints!;
typedPathList = fetchedPathPoints!.map((latLng) {
return work_area_model.DeviceAddPathPointModel(
latitude: latLng.latitude,
longitude: latLng.longitude,
);
}).toList();
});
pathFetchSuccess = true;
debugPrint('✅ [开始作业] 路径已渲染到地图');
} else {
debugPrint('⚠️ [开始作业] 路径数据为空');
}
} else {
debugPrint('⚠️ [开始作业] 路径数据结构格式错误');
}
} else {
debugPrint('❌ [开始作业] getTaskPath 接口返回业务失败: $code');
}
} else {
debugPrint('❌ [开始作业] getTaskPath 接口HTTP失败: ${response.statusCode}');
}
} catch (e) {
debugPrint('❌ [开始作业] getTaskPath 接口调用异常: $e');
}
// 🔥 判断路径获取结果
if (!pathFetchSuccess) {
debugPrint('❌ [开始作业] 路径数据获取失败或为空,关闭loading,弹出提示,终止作业启动');
setState(() {
isStopWork = false;
isStartWork = false;
_workStatus = WorkStatus.idle;
});
_showPageToast(
message: '获取路径数据失败,无法启动作业',
type: ToastType.error,
);
return;
}
debugPrint('🎯 [开始作业] 步骤4: 任务就绪, currentTaskId=${taskCubit.state.currentTaskId}');
setState(() {
isStopWork = false;
isStartWork = true;
@@ -2521,8 +2592,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
});
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
// 🔥 步骤4: 刷新任务池(保护 taskId 不被覆盖)
debugPrint('🔄 [开始作业] 步骤4: 刷新任务池');
// 🔥 步骤5: 刷新任务池(保护 taskId 不被覆盖)
debugPrint('🔄 [开始作业] 步骤5: 刷新任务池');
final _confirmedTaskId = taskCubit.state.currentTaskId;
await taskCubit.fetchAndFilterTask(deviceId);
if (_confirmedTaskId != null && taskCubit.state.currentTaskId == null) {
@@ -2530,8 +2601,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
debugPrint('🛡️ [开始作业] taskId 被刷新覆盖,已还原: $_confirmedTaskId');
}
// 🔥 步骤5: 启动MQTT到达点监听
debugPrint('📡 [开始作业] 步骤5: 启动MQTT监听, deviceId: $deviceId, taskId: $_confirmedTaskId');
// 🔥 步骤6: 启动MQTT到达点监听
debugPrint('📡 [开始作业] 步骤6: 启动MQTT监听, deviceId: $deviceId, taskId: $_confirmedTaskId');
try {
await context.read<DevicesCubit>().startListeningMqttArrive(deviceId: deviceId, taskId: _confirmedTaskId!);
debugPrint('✅ [开始作业] MQTT到达点监听已启动');
@@ -2539,8 +2610,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
debugPrint('⚠️ [开始作业] MQTT监听启动失败: $e (非致命,继续)');
}
// 🔥 步骤6: 持久化状态
debugPrint('💾 [开始作业] 步骤6: 持久化状态, taskId=${taskCubit.state.currentTaskId}');
// 🔥 步骤7: 持久化状态
debugPrint('💾 [开始作业] 步骤7: 持久化状态, taskId=${taskCubit.state.currentTaskId}');
_showPageToast(message: "作业已开始", type: ToastType.success);
_saveDataToLocal();
@@ -3511,18 +3582,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
firstPointWgs = _robotModeWgsPoints.first;
} else if (_currentRobotMode == RobotMode.point &&
_markedPoints.isNotEmpty) {
LatLng firstPointGcj = _markedPoints.first;
firstPointWgs = gcj02ToWgs84(
firstPointGcj.latitude,
firstPointGcj.longitude,
);
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
firstPointWgs = _markedPoints.first;
} else if (_markedPoints.isNotEmpty) {
// 兜底:_robotModeWgsPoints 为空但_markedPoints有数据(新缓存key未写入等场景)
LatLng firstPointGcj = _markedPoints.first;
firstPointWgs = gcj02ToWgs84(
firstPointGcj.latitude,
firstPointGcj.longitude,
);
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
firstPointWgs = _markedPoints.first;
} else {
if (showTips) {
_showPageToast(message: '参考点坐标为空,请先添加作业区域打点!', type: ToastType.error);
@@ -3546,20 +3611,19 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
);
}).toList();
} else if (_markedPoints.isNotEmpty) {
// robot模式但_robotModeWgsPoints为空时,兜底用_markedPoints转换
// robot模式但_robotModeWgsPoints为空时,用_markedPoints(已是WGS84)
outerPositions = _markedPoints.map((latLng) {
final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude);
return work_area_model.Position(
lat: wgs84Point.latitude,
lon: wgs84Point.longitude,
lat: latLng.latitude,
lon: latLng.longitude,
);
}).toList();
}
gcjOuterPoints = outerPositions.map((point) {
double wgs84Lat = point.lat;
double wgs84Lon = point.lon;
final gcjPoint = wgs84ToGcj02(wgs84Lat, wgs84Lon);
return LatLng(gcjPoint.latitude, gcjPoint.longitude);
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
return LatLng(wgs84Lat, wgs84Lon);
}).toList();
final outerBoundary = work_area_model.OuterBoundary(
@@ -3588,13 +3652,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
for (var holePoints in _obstacleHoles) {
List<work_area_model.Position> innerPositionList = [];
innerPositionList = holePoints.map((latLng) {
final wgs84Point = gcj02ToWgs84(
latLng.latitude,
latLng.longitude,
);
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
return work_area_model.Position(
lat: wgs84Point.latitude,
lon: wgs84Point.longitude,
lat: latLng.latitude,
lon: latLng.longitude,
);
}).toList();
positionList.add(innerPositionList);
@@ -3625,6 +3686,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
);
}
// 🔥 已废弃:不再调用旧的路径生成API,path只传用户手动打点的边界点
/*
await context.read<DevicesCubit>().generatePath(
reference: referencePoint,
heading: heading,
@@ -3671,6 +3734,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
moveMapToPointsCenter(gcjPathPoints);
}
}
*/
} catch (e) {
debugPrint('路径生成异常:$e');
if (showTips) {
@@ -3950,6 +4014,39 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
),
/// 悬浮"回到苏州"按钮:一键跳转到 GeoServer 苏州地块(zoom 18 才能看清 250m 地块)
Positioned(
top: 60,
left: 10,
child: GestureDetector(
onTap: () => _mapController.move(kSuzhouCenter, 18),
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),
),
],
),
alignment: Alignment.center,
child: const Text(
'苏',
style: TextStyle(
color: Colors.black87,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
/// 核心:地图正中心固定定位标
if (_currentRobotMode == RobotMode.point)
Positioned(
@@ -4428,12 +4525,14 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}
}
// 转换单个WGS84坐标到GCJ02
LatLng convertWGS84ToGCJ02(double lat, double lon) {
return wgs84ToGcj02(lat, lon);
}
// ⚠️ 以下函数已注释 - 地图已切换为 ESRI WGS84,不再需要 GCJ02 转换
// 后续确认无误后可删除
// ⚠️ convertWGS84ToGCJ02 函数已删除 - 地图已切换为 ESRI WGS84,不再需要 GCJ02 转换
// 以下坐标转换函数保留但注释,后续确认无误后可删除
/// =======================================================
// ⚠️ 坐标转换函数已注释 - 地图已切换为 ESRI WGS84,不再需要 GCJ02 转换
// 以下函数保留但注释,后续确认无误后可删除
/*
/// 坐标转换:WGS84 -> GCJ02(国内高德/腾讯通用)
/// =======================================================
const double _pi = 3.1415926535897932384626;
@@ -4560,8 +4659,9 @@ double _transformLon(double x, double y) {
(150.0 * sin(x / 12.0 * _pi) + 300.0 * sin(x / 30.0 * _pi)) * 2.0 / 3.0;
return ret;
}
*/
/// 计算两个经纬度点之间的地面距离(单位:米,GCJ02/WGS84通用)
/// 计算两个经纬度点之间的地面距离(单位:米,GCJ02/WGS84 通用)
String _calculateLatLngDistance(LatLng point1, LatLng point2) {
const double earthRadius = 6371000; // 地球半径(米)

View File

@@ -1501,12 +1501,13 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
},
),
children: [
// 高德地图瓦片(最底层)
// ESRI World Imagery(WGS84 坐标系,全球高清卫星图)
// 替代高德地图(GCJ02),统一坐标系,无需坐标转换
TileLayer(
urlTemplate:
'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
subdomains: ['1', '2', '3', '4'],
urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
userAgentPackageName: 'com.example.app',
minZoom: 0,
maxZoom: 19,
),
// 轨迹线(中间层)

View File

@@ -36,6 +36,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
late DroneOsdDataSource _osdDataSource;
StreamSubscription<DroneOsdEntity>? _stationOsdSubscription;
StreamSubscription<Object?>? _blocStateSubscription;
/// 静态缓存:按 gatewaySn 分区,跨页面实例保留每个机场上次的 host 数据
/// 避免进入新机场时显示上一个机场的缓存数据
@@ -62,23 +63,34 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
);
_osdDataSource = sl<DroneOsdDataSource>();
_startStationOsdListening();
_hasInitialized = true;
}
void _startStationOsdListening() {
// 使用 station 自带的 deviceSn + gatewaySn,与列表页预订阅参数一致
// 避免因参数不同导致 DataSource 重新订阅、打断数据流
_osdDataSource.startListening(
deviceSn: widget.station.deviceSn,
gatewaySn: widget.station.gatewaySn,
);
// 🔥 立即建立 stationOsdStream 的监听(关键!)
// 否则 _parseAndUpdateHostData 永远不会被调用,机场状态卡片一直显示"未知"
_stationOsdSubscription = _osdDataSource.stationOsdStream.listen((osd) {
if (!mounted) return;
_parseAndUpdateHostData(osd);
});
// 🔥 监听 Bloc 状态变化,等数据加载完成后再确保 MQTT 订阅建立
_blocStateSubscription = _bloc.stream.listen((state) {
if (state is UAVDetailLoaded && mounted) {
debugPrint('✅ [DroneStationDetailPage] Bloc 数据加载完成,确保 MQTT 订阅');
debugPrint(' deviceSn: ${state.detail.deviceSn}');
debugPrint(' gatewaySn: ${widget.station.gatewaySn}');
// 确保 MQTT 订阅建立
_osdDataSource.startListening(
deviceSn: state.detail.deviceSn,
gatewaySn: widget.station.gatewaySn,
);
_droneSn = state.detail.deviceSn;
// 取消监听(只执行一次)
_blocStateSubscription?.cancel();
}
});
_hasInitialized = true;
}
void _parseAndUpdateHostData(DroneOsdEntity osd) {
@@ -95,6 +107,18 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
final hostData = data['data'] is Map ? (data['data'] as Map)['host'] : null;
if (hostData == null || hostData is! Map) return;
// 🔥 关键检查:确认是否包含机场状态的关键字段
// 避免将其他类型的OSD消息(如设备状态、电池信息等)覆盖机场状态数据
final hasHostFields = hostData.containsKey('cover_state') ||
hostData.containsKey('drone_in_dock') ||
hostData.containsKey('temperature') ||
hostData.containsKey('humidity');
if (!hasHostFields) {
debugPrint('⚠️ [DroneStationDetailPage] ⚠️ 此消息不包含机场状态字段,跳过更新!');
debugPrint(' hostData keys: ${hostData.keys.toList()}');
return; // 不是机场状态数据,直接返回
}
// 🔥 基于已有数据合并,只更新非 null 字段,避免 Type 2 消息覆盖已有值
final Map<String, dynamic> merged = Map<String, dynamic>.from(
_stationHostData.value,
@@ -111,10 +135,13 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
_put('humidity', (hostData['humidity'] as num?)?.toDouble());
_put('wind_speed', (hostData['wind_speed'] as num?)?.toDouble());
_put('rainfall', hostData['rainfall']?.toString());
_put('cover_state', hostData['cover_state']?.toString());
_put('drone_in_dock', hostData['drone_in_dock']?.toString());
// 🔥 不再做严格校验,直接存储原始值,让UI层自己去解析
_put('cover_state', hostData['cover_state']);
_put('drone_in_dock', hostData['drone_in_dock']);
_put('temperature', (hostData['temperature'] as num?)?.toDouble());
_put('putter_state', hostData['putter_state']?.toString());
_put('putter_state', hostData['putter_state']);
_put(
'supplement_light_state',
hostData['supplement_light_state']?.toString(),
@@ -144,25 +171,17 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
final acData = hostData['air_conditioner'];
if (acData is Map) {
_put('air_conditioner', acData);
// 解析空调开关状态
final acStateVal = int.tryParse(acData['air_conditioner_state']?.toString() ?? '') ?? -1;
if (acStateVal == 0 || acStateVal == 1) {
_put('air_conditioner_state', acStateVal.toString());
}
// 解析空调运行时间
final switchTime = acData['switch_time'];
if (switchTime != null) {
_put('air_conditioner_switch_time', '$switchTime');
}
// 不单独提取air_conditioner_state,让UI层自己解析
}
// 🔥 存储到ValueNotifier触发UI更新
_stationHostData.value = merged;
// 同步写入按 gatewaySn 分区的静态缓存
// 同步写入按gatewaySn分区的静态缓存
_cachedHostDataBySn[widget.station.gatewaySn] = Map<String, dynamic>.from(
merged,
);
debugPrint('[DroneStationDetailPage] 收到机场状态数据');
}
/// 🔥 页面重新激活时调用(从其他页面返回时)
@@ -198,6 +217,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
@override
void dispose() {
_stationOsdSubscription?.cancel();
_blocStateSubscription?.cancel();
// 只调用 stopListening 减少引用计数,不调用 dispose()
// 因为 DroneOsdDataSource 是 LazySingleton,被列表页和详情页共享
// 详情页退出后列表页可能仍需要该订阅
@@ -331,17 +351,6 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
Widget _buildContent(UAVDetailEntity detail) {
_detail = detail;
final newDroneSn = detail.deviceSn;
// 只在 droneSn 真正变化时才重新订阅,避免 Bloc 刷新时重复 startListening
// 导致引用计数虚增、dispose 时无法真正取消订阅
if (newDroneSn != null && newDroneSn.isNotEmpty && newDroneSn != _droneSn) {
_droneSn = newDroneSn;
_osdDataSource.startListening(
deviceSn: newDroneSn,
gatewaySn: widget.station.gatewaySn,
);
}
return RefreshIndicator(
onRefresh: _handleRefresh,
@@ -484,8 +493,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
final rainfall = hostData['rainfall'];
final coverState = hostData['cover_state'];
final droneInDock = hostData['drone_in_dock'];
final acState = hostData['air_conditioner_state'];
final alarmState = hostData['alarm_state'];
// 🔥 从 air_conditioner Map 中解析空调状态
final acDataMap = hostData['air_conditioner'];
final acState = acDataMap is Map ? acDataMap['air_conditioner_state'] : null;
String envTempStr = '未知';
if (envTemp != null) {
@@ -516,7 +528,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
String coverStr = '未知';
Color coverColor = const Color(0xFF86909C);
if (coverState != null) {
final state = int.tryParse(coverState) ?? -1;
// 🔥 像 DroneStationOsdCard 一样解析,支持多种数据格式
final state = coverState is int ? coverState : int.tryParse(coverState.toString()) ?? -1;
if (state == 1) {
coverStr = '开启';
coverColor = const Color(0xFFFF7D00);
@@ -529,7 +542,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
String droneDockStr = '未知';
Color droneDockColor = const Color(0xFF86909C);
if (droneInDock != null) {
final state = int.tryParse(droneInDock) ?? -1;
// 🔥 像 DroneStationOsdCard 一样解析
final state = droneInDock is int ? droneInDock : int.tryParse(droneInDock.toString()) ?? -1;
if (state == 1) {
droneDockStr = '在库内';
droneDockColor = const Color(0xFF00B42A);
@@ -542,7 +556,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
String acStr = '未知';
Color acColor = const Color(0xFF86909C);
if (acState != null) {
// 🔥 增强兼容性:尝试多种数据格式(int、string等)
// 🔥 增强兼容性:支持多种数据格式和更多状态值
final acValue = acState is int ? acState : int.tryParse(acState.toString()) ?? -1;
if (acValue == 1) {
acStr = '开启';
@@ -551,7 +565,6 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
acStr = '关闭';
acColor = const Color(0xFF86909C);
} else if (acValue == 2) {
// 部分设备使用 2 表示待机或其他状态
acStr = '待机';
acColor = const Color(0xFFFF7D00);
}

View File

@@ -1290,12 +1290,13 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
),
),
children: [
// 高德地图瓦片(最底层)
// ESRI World Imagery(WGS84 坐标系,全球高清卫星图)
// 替代高德地图(GCJ02),统一坐标系,无需坐标转换
TileLayer(
urlTemplate:
'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
subdomains: ['1', '2', '3', '4'],
urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
userAgentPackageName: 'com.example.app',
minZoom: 0,
maxZoom: 19,
),
// 🔥 轨迹线(中间层)
@@ -1563,12 +1564,13 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
),
),
children: [
// 高德地图瓦片(最底层)
// ESRI World Imagery(WGS84 坐标系,全球高清卫星图)
// 替代高德地图(GCJ02),统一坐标系,无需坐标转换
TileLayer(
urlTemplate:
'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
subdomains: ['1', '2', '3', '4'],
urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
userAgentPackageName: 'com.example.app',
minZoom: 0,
maxZoom: 19,
),
// 🔥 轨迹线(中间层)