蓝牙绑定设备+场站切换支持高清图层的一对一显示+设备实时状态的部分显示
This commit is contained in:
@@ -7,17 +7,46 @@ import 'package:latlong2/latlong.dart';
|
||||
/// GeoServer 基地址(公司端)
|
||||
const String kGeoserverBase = 'http://1.95.137.212:7900/geoserver';
|
||||
|
||||
/// 首期苏州区域图层标识
|
||||
const String kSuzhouLayerName = 'workSpace:suzhou';
|
||||
/// 各场站高清图层的真实范围(取自 GeoServer GetCapabilities,原生 EPSG:4326)
|
||||
/// key = 后端下发的 siteMapName(形如 workSpace:suzhou)
|
||||
final Map<String, LatLngBounds> kStationLayerBounds = {
|
||||
'workSpace:liaoding': LatLngBounds(
|
||||
const LatLng(41.19998369457442, 120.59074514010273),
|
||||
const LatLng(41.202002066385475, 120.59343722827124),
|
||||
),
|
||||
'workSpace:loudi': LatLngBounds(
|
||||
const LatLng(27.846682995656035, 111.52814279952669),
|
||||
const LatLng(27.847939704917945, 111.52928081352842),
|
||||
),
|
||||
'workSpace:neimeng': LatLngBounds(
|
||||
const LatLng(41.321581124009896, 112.62468603332228),
|
||||
const LatLng(41.325573311689986, 112.63034797059984),
|
||||
),
|
||||
'workSpace:result': LatLngBounds(
|
||||
const LatLng(27.36490549568922, 116.84490756003203),
|
||||
const LatLng(27.36673908366302, 116.84661654295478),
|
||||
),
|
||||
'workSpace:sanxiamen': LatLngBounds(
|
||||
const LatLng(34.732748259670494, 111.43051478686061),
|
||||
const LatLng(34.734205267863345, 111.43201022647355),
|
||||
),
|
||||
'workSpace:suzhou': LatLngBounds(
|
||||
const LatLng(31.082450793459877, 120.83906484264836),
|
||||
const LatLng(31.084141983135456, 120.84164596446476),
|
||||
),
|
||||
'workSpace:zhongke': LatLngBounds(
|
||||
const LatLng(32.0406442971753, 120.8057183175354),
|
||||
const LatLng(32.045144217672636, 120.81100452381844),
|
||||
),
|
||||
};
|
||||
|
||||
/// 苏州图层实际覆盖范围(取自 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);
|
||||
/// 取某场站图层的范围中心点(用作进场默认定位);未知/为空返回 null
|
||||
LatLng? stationLayerCenter(String? siteMapName) {
|
||||
if (siteMapName == null) return null;
|
||||
final b = kStationLayerBounds[siteMapName];
|
||||
if (b == null) return null;
|
||||
return LatLng((b.south + b.north) / 2, (b.west + b.east) / 2);
|
||||
}
|
||||
|
||||
/// 叠加层取瓦片方式开关:
|
||||
/// - false(当前):WMS GetMap 兜底,不依赖 GWC 缓存
|
||||
@@ -29,8 +58,14 @@ const bool kUseWmtsTile = false;
|
||||
/// 请求 EPSG:3857(WMS 1.1.1 / 1.3.0 均验证过)返回纯白图。
|
||||
/// 因此这里按瓦片 XYZ 反算经纬度 bbox,以 EPSG:4326 请求 WMS。
|
||||
class GeoWmsProvider extends TileProvider {
|
||||
GeoWmsProvider({this.maxLevel = 21});
|
||||
GeoWmsProvider({
|
||||
required this.layerName,
|
||||
required this.bounds,
|
||||
this.maxLevel = 21,
|
||||
});
|
||||
|
||||
final String layerName;
|
||||
final LatLngBounds bounds;
|
||||
final int maxLevel;
|
||||
|
||||
/// Web 墨卡托地球周长(米)
|
||||
@@ -64,10 +99,10 @@ class GeoWmsProvider extends TileProvider {
|
||||
final northLat = _mercatorYToLat(northM);
|
||||
|
||||
// 与图层实际范围不相交的瓦片直接返回透明图,不发无谓请求
|
||||
final intersects = westLng < kSuzhouBounds.east &&
|
||||
eastLng > kSuzhouBounds.west &&
|
||||
southLat < kSuzhouBounds.north &&
|
||||
northLat > kSuzhouBounds.south;
|
||||
final intersects = westLng < bounds.east &&
|
||||
eastLng > bounds.west &&
|
||||
southLat < bounds.north &&
|
||||
northLat > bounds.south;
|
||||
if (!intersects) {
|
||||
return MemoryImage(TileProvider.transparentImage);
|
||||
}
|
||||
@@ -77,7 +112,7 @@ class GeoWmsProvider extends TileProvider {
|
||||
'SERVICE': 'WMS',
|
||||
'REQUEST': 'GetMap',
|
||||
'VERSION': '1.1.1',
|
||||
'LAYERS': kSuzhouLayerName,
|
||||
'LAYERS': layerName,
|
||||
'BBOX': '${westLng.toStringAsFixed(6)},${southLat.toStringAsFixed(6)},'
|
||||
'${eastLng.toStringAsFixed(6)},${northLat.toStringAsFixed(6)}',
|
||||
'WIDTH': '256',
|
||||
@@ -92,11 +127,18 @@ class GeoWmsProvider extends TileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// GeoServer 苏州无人机高清影像叠加层(WGS84 基准,覆盖在 ESRI 底图上)
|
||||
///
|
||||
/// 地块很小(约 250m),zoom 16 以上才有意义,默认 minZoom = 16
|
||||
/// GeoServer 场站无人机高清影像叠加层(WGS84 基准,覆盖在 ESRI 底图上)
|
||||
/// 按当前选中场站的 siteMapName 动态加载对应图层;地块很小,默认 minZoom = 16
|
||||
class GeoSuzhouOverlay extends StatelessWidget {
|
||||
const GeoSuzhouOverlay({super.key, this.minZoom = 16, this.maxZoom = 21});
|
||||
const GeoSuzhouOverlay({
|
||||
super.key,
|
||||
this.siteMapName,
|
||||
this.minZoom = 16,
|
||||
this.maxZoom = 21,
|
||||
});
|
||||
|
||||
/// 当前选中场站对应的 GeoServer 图层名(形如 workSpace:suzhou)
|
||||
final String? siteMapName;
|
||||
|
||||
/// 小于该级别不请求瓦片(地块太小,低层级看不见)
|
||||
final double minZoom;
|
||||
@@ -106,28 +148,39 @@ class GeoSuzhouOverlay extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final layer = siteMapName;
|
||||
final bounds = layer == null ? null : kStationLayerBounds[layer];
|
||||
// 未选中场站或图层未知 → 不渲染叠加层
|
||||
if (layer == null || bounds == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
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'
|
||||
'&LAYER=$layer&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,
|
||||
tileBounds: bounds,
|
||||
);
|
||||
}
|
||||
// === WMS 版(当前兜底):按原生 EPSG:4326 请求,GeoServer 实时渲染 ===
|
||||
return TileLayer(
|
||||
tileProvider: GeoWmsProvider(maxLevel: maxZoom.toInt()),
|
||||
tileProvider: GeoWmsProvider(
|
||||
layerName: layer,
|
||||
bounds: bounds,
|
||||
maxLevel: maxZoom.toInt(),
|
||||
),
|
||||
userAgentPackageName: 'com.example.app',
|
||||
minZoom: minZoom,
|
||||
maxZoom: maxZoom,
|
||||
maxNativeZoom: maxZoom.toInt(),
|
||||
tileBounds: kSuzhouBounds,
|
||||
tileBounds: bounds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +492,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
// ==================== ✅ 核心修复 ====================
|
||||
// 1. 从缓存恢复后,强制让 _markedPoints 同步给外边框(解决不显示)
|
||||
if (_markedPoints.isNotEmpty && gcjOuterPoints.isEmpty) {
|
||||
gcjOuterPoints = List.from(_markedPoints);
|
||||
// 🔥 覆盖模式:兜底恢复时同样闭合边界,避免重启后 outer 缺一边
|
||||
gcjOuterPoints = _closeBowRing(_markedPoints);
|
||||
}
|
||||
|
||||
// 2. 关键:同步机器人模式坐标(保存时边框不丢失)
|
||||
@@ -924,6 +925,156 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 保存地块:打印即将 POST 给后端的完整请求详情
|
||||
/// 用途:验证覆盖模式下 jsonData.outer / jsonData.path 是否已首尾闭合(A B C → A B C A)
|
||||
/// ⚠️ 只读取已构造好的 Dart 对象和 MultipartFile 的元信息(field/filename/contentType/length),
|
||||
/// 绝不调用 MultipartFile.finalize()——那会消费 ByteStream,导致后续 request.send() 失败。
|
||||
/// (旁边的 _debugPrintMultipartRequest 正是踩了这个坑,故未启用)
|
||||
void _logSaveRequestDetail({
|
||||
required http.MultipartRequest request,
|
||||
required Map<String, dynamic> savePath,
|
||||
required String workRecordJson,
|
||||
required String plotName,
|
||||
required Object? siteId,
|
||||
}) {
|
||||
// debugPrint 单条过长会被 logcat 截断,长文本统一按行拆开输出
|
||||
void printLines(String text) {
|
||||
for (final line in text.split('\n')) {
|
||||
debugPrint(line);
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('📤 [保存地块-请求详情] ================ BEGIN ================');
|
||||
debugPrint('📤 [请求行] ${request.method} ${request.url}');
|
||||
|
||||
// 请求头:Authorization 脱敏,避免 token 明文落日志
|
||||
final Map<String, String> safeHeaders = Map<String, String>.from(
|
||||
request.headers,
|
||||
);
|
||||
final String? auth = safeHeaders['Authorization'];
|
||||
if (auth != null) {
|
||||
safeHeaders['Authorization'] = auth.length > 18
|
||||
? '${auth.substring(0, 14)}...${auth.substring(auth.length - 4)} (已脱敏, 原长${auth.length})'
|
||||
: '(已脱敏)';
|
||||
}
|
||||
debugPrint('📤 [请求头] $safeHeaders');
|
||||
|
||||
debugPrint(
|
||||
'📤 [上下文] workName=$plotName, siteId=$siteId, '
|
||||
'workMode=$_currentWorkMode, robotMode=$_currentRobotMode, '
|
||||
'areaMode=$_currentAreaMode, planModel=${savePath['planModel']}',
|
||||
);
|
||||
|
||||
// multipart 各 part 的元信息(不消费流)
|
||||
debugPrint('📤 [multipart] part 数量: ${request.files.length}');
|
||||
for (var i = 0; i < request.files.length; i++) {
|
||||
final http.MultipartFile f = request.files[i];
|
||||
debugPrint(
|
||||
'📤 [multipart] part[$i] field=${f.field}, '
|
||||
'filename="${f.filename ?? ''}", contentType=${f.contentType}, '
|
||||
'大小=${f.length} 字节 (${(f.length / 1024).toStringAsFixed(1)} KB)',
|
||||
);
|
||||
}
|
||||
|
||||
// 逐点打印 outer / path,并标注首尾,肉眼即可确认是否闭合
|
||||
void printPoints(String field) {
|
||||
final List<dynamic> list = (savePath[field] as List?) ?? <dynamic>[];
|
||||
debugPrint('📤 [jsonData.$field] 点数=${list.length}');
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
final Map<dynamic, dynamic> p = list[i] as Map;
|
||||
final String tag = list.length == 1
|
||||
? ' ← 唯一的点'
|
||||
: (i == 0
|
||||
? ' ← 第一个点'
|
||||
: (i == list.length - 1 ? ' ← 最后一个点' : ''));
|
||||
debugPrint(
|
||||
'📤 [jsonData.$field] [$i] lat=${p['lat']}, lng=${p['lng']}$tag',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
printPoints('outer');
|
||||
printPoints('path');
|
||||
|
||||
// 闭环校验结论
|
||||
bool isClosed(String field) {
|
||||
final List<dynamic> list = (savePath[field] as List?) ?? <dynamic>[];
|
||||
if (list.length < 4) return false;
|
||||
final Map<dynamic, dynamic> first = list.first as Map;
|
||||
final Map<dynamic, dynamic> last = list.last as Map;
|
||||
return first['lat'] == last['lat'] && first['lng'] == last['lng'];
|
||||
}
|
||||
|
||||
// 🔥 首尾并排比对:把 [0] 和 [最后一个] 的实际坐标上下两行打出来,
|
||||
// 直接肉眼确认“第一个点被当作最后一个点追加到了末尾”
|
||||
void printHeadTail(String field) {
|
||||
final List<dynamic> list = (savePath[field] as List?) ?? <dynamic>[];
|
||||
if (list.isEmpty) {
|
||||
debugPrint('🔒 [首尾比对] $field:空数组,无比对对象');
|
||||
return;
|
||||
}
|
||||
if (list.length == 1) {
|
||||
debugPrint('🔒 [首尾比对] $field:只有 1 个点,不构成环');
|
||||
return;
|
||||
}
|
||||
final Map<dynamic, dynamic> f = list.first as Map;
|
||||
final Map<dynamic, dynamic> l = list.last as Map;
|
||||
final int lastIdx = list.length - 1;
|
||||
final bool same = f['lat'] == l['lat'] && f['lng'] == l['lng'];
|
||||
debugPrint('🔒 [首尾比对] ---- $field (共 ${list.length} 个点) ----');
|
||||
debugPrint('🔒 [首尾比对] $field[0] lat=${f['lat']}, lng=${f['lng']} ← 第一个点');
|
||||
debugPrint('🔒 [首尾比对] $field[$lastIdx] lat=${l['lat']}, lng=${l['lng']} ← 最后一个点');
|
||||
debugPrint(
|
||||
'🔒 [首尾比对] 结论:$field[$lastIdx] ${same ? "==" : "!="} $field[0] → '
|
||||
'${same ? "✅ 最后一个点就是第一个点(已闭环 ABCA)" : "❌ 两点不同(未闭环 ABC)"}',
|
||||
);
|
||||
if (same) {
|
||||
debugPrint(
|
||||
'🔒 [首尾比对] 即:用户实际打了 $lastIdx 个不同顶点,'
|
||||
'末尾第 $lastIdx 位是自动追加的闭合点(与[0]完全相同)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentWorkMode == WorkMode.bow) {
|
||||
printHeadTail('outer');
|
||||
printHeadTail('path');
|
||||
|
||||
final bool outerOk = isClosed('outer');
|
||||
final bool pathOk = isClosed('path');
|
||||
debugPrint(
|
||||
'🔒 [闭环校验] 覆盖模式要求首尾同点 → '
|
||||
'outer=${outerOk ? "✅已闭合" : "❌未闭合"}, '
|
||||
'path=${pathOk ? "✅已闭合" : "❌未闭合"}',
|
||||
);
|
||||
if (!outerOk || !pathOk) {
|
||||
debugPrint(
|
||||
'🔒 [闭环校验] ⚠️ 未闭合原因排查:打点数=${_markedPoints.length}(需≥3), '
|
||||
'robotMode打点数=${_robotModeWgsPoints.length}, '
|
||||
'workMode=$_currentWorkMode(需为 WorkMode.bow)'
|
||||
'${outerOk || pathOk ? "" : ";若两者都❌,说明 _closeBowRing 未执行追加,往上翻 🔗 [闭合] 日志看跳过原因"}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'🔒 [闭环校验] 当前 workMode=$_currentWorkMode 非覆盖模式,'
|
||||
'outer 应为空、path 不闭合(符合预期)',
|
||||
);
|
||||
}
|
||||
|
||||
// 完整 JSON:这就是后端 /iot/workRecord/add 收到的 workRecord 内容
|
||||
debugPrint('📤 [workRecord 完整JSON] ↓↓↓ 后端实际接收内容 ↓↓↓');
|
||||
try {
|
||||
printLines(
|
||||
const JsonEncoder.withIndent(' ').convert(jsonDecode(workRecordJson)),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('📤 [workRecord 完整JSON] 格式化失败($e),输出原始串:');
|
||||
printLines(workRecordJson);
|
||||
}
|
||||
debugPrint('📤 [保存地块-请求详情] ================= END =================');
|
||||
}
|
||||
|
||||
Future<void> _savePlotData(String plotName, String? imgBase64) async {
|
||||
debugPrint('🔍 [保存地块] ====== _savePlotData 被调用, plotName=$plotName ======');
|
||||
final loc = AppLocalizations.of(context);
|
||||
@@ -944,15 +1095,14 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
// 辅助方法:将打点转为 WGS84 格式 {lng, lat}
|
||||
List<Map<String, double>> _markedPointsToWgs() {
|
||||
if (_currentRobotMode == RobotMode.robot) {
|
||||
return _robotModeWgsPoints
|
||||
.map((p) => {'lng': p.longitude, 'lat': p.latitude})
|
||||
.toList();
|
||||
}
|
||||
return _markedPoints.map((point) {
|
||||
final List<LatLng> source = _currentRobotMode == RobotMode.robot
|
||||
? _robotModeWgsPoints
|
||||
: _markedPoints;
|
||||
// 🔥 覆盖模式:上传前闭合边界,首尾同点(A B C → A B C A)
|
||||
// 下位机与后端要求第一个点与最后一个点相同
|
||||
return _closeBowRing(source).map((point) {
|
||||
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
|
||||
final wgs84 = LatLng(point.latitude, point.longitude);
|
||||
return {'lng': wgs84.longitude, 'lat': wgs84.latitude};
|
||||
return {'lng': point.longitude, 'lat': point.latitude};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -1036,21 +1186,19 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
);
|
||||
|
||||
debugPrint('🔍 [保存地块] 构造请求完成,即将进入try发送...');
|
||||
|
||||
// 🔥 发送前打印完整请求详情:URL、请求头、multipart 各 part、
|
||||
// outer/path 逐点坐标(标注首尾)、闭环校验结论、workRecord 完整 JSON
|
||||
_logSaveRequestDetail(
|
||||
request: request,
|
||||
savePath: savePath,
|
||||
workRecordJson: workRecordJson,
|
||||
plotName: plotName,
|
||||
siteId: siteId,
|
||||
);
|
||||
|
||||
// 7. 发送请求
|
||||
try {
|
||||
// debugPrint('📤 [保存地块] ========== 请求参数 BEGIN ==========');
|
||||
// debugPrint('📤 [保存地块] plotName: $plotName');
|
||||
// debugPrint('📤 [保存地块] siteId: $siteId');
|
||||
// debugPrint('📤 [保存地块] workMode: $_currentWorkMode');
|
||||
// debugPrint('📤 [保存地块] planModel: ${savePath['planModel']}');
|
||||
// debugPrint('📤 [保存地块] savePath.name: ${savePath['name']}');
|
||||
// debugPrint('📤 [保存地块] path长度: ${(savePath['path'] as List?)?.length ?? 0}');
|
||||
// debugPrint('📤 [保存地块] outer长度: ${(savePath['outer'] as List?)?.length ?? 0}');
|
||||
// debugPrint('📤 [保存地块] 是否有图片文件: ${imgBase64 != null && imgBase64.isNotEmpty}');
|
||||
// debugPrint('📤 [保存地块] 请求URL: ${HttpApiConsts.baseUrl}/iot/workRecord/add');
|
||||
// debugPrint('📤 [保存地块] workRecord(完整JSON): $workRecordJson');
|
||||
// debugPrint('📤 [保存地块] savePath(原始对象): $savePath');
|
||||
// debugPrint('📤 [保存地块] ========== 请求参数 END ==========');
|
||||
_showPageToast(
|
||||
message: loc.translate('route_planning.saving') + "「$plotName」...",
|
||||
type: ToastType.loading,
|
||||
@@ -1928,7 +2076,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
}
|
||||
|
||||
LatLng _getDefaultValidCenter() {
|
||||
return const LatLng(30.279651, 120.154871);
|
||||
// 优先进场定位到“当前选中场站”的高清图层中心;无有效场站时兜底到苏州地块中心
|
||||
final center = stationLayerCenter(
|
||||
sl<SiteCubit>().state.selectedSite?.siteMapName,
|
||||
);
|
||||
return center ?? const LatLng(31.083296, 120.840355);
|
||||
}
|
||||
|
||||
Widget _buildMap(
|
||||
@@ -1990,11 +2142,13 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
maxZoom: 19,
|
||||
),
|
||||
|
||||
/// GeoServer 苏州无人机高清影像叠加层(WGS84 基准,无偏移)
|
||||
/// 实际地块很小(约 250m,中心 31.0833/120.8404),需 zoom >= 16 才能看到
|
||||
/// GeoServer 场站无人机高清影像叠加层(WGS84 基准,无偏移)
|
||||
/// 按当前选中场站的 siteMapName 动态加载对应图层;需 zoom >= 16 才能看到
|
||||
/// 当前 WMS 兜底(原生 EPSG:4326);公司补配 GWC EPSG:3857 后切 WMTS
|
||||
/// 切换位置:geoserver_suzhou_layer.dart 的 kUseWmtsTile 开关
|
||||
GeoSuzhouOverlay(),
|
||||
GeoSuzhouOverlay(
|
||||
siteMapName: sl<SiteCubit>().state.selectedSite?.siteMapName,
|
||||
),
|
||||
//if (isDjMapShow) DJITileLayer(stationName: _currentStation.name, mapController: _mapController),
|
||||
|
||||
// 绘制path折线(Line模式)
|
||||
@@ -3776,6 +3930,50 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 覆盖模式下闭合打点集合:把第一个点追加到末尾(A B C → A B C A)
|
||||
/// 仅覆盖模式(WorkMode.bow)生效;点数不足 3 或首尾已重合时原样返回,避免重复追加
|
||||
/// ⚠️ 只用于派生数据/出口数据,禁止写入 _markedPoints、_robotModeWgsPoints 源集合
|
||||
/// 🔥 每个分支都打日志,便于确认“第一个点到底有没有被追加到末尾”
|
||||
List<LatLng> _closeBowRing(List<LatLng> points) {
|
||||
if (_currentWorkMode != WorkMode.bow) {
|
||||
debugPrint(
|
||||
'🔗 [闭合] ⏭️ 跳过追加:workMode=$_currentWorkMode 不是覆盖模式,'
|
||||
'点数=${points.length} 原样返回(不闭合)',
|
||||
);
|
||||
return List<LatLng>.from(points);
|
||||
}
|
||||
if (points.length < 3) {
|
||||
debugPrint(
|
||||
'🔗 [闭合] ⏭️ 跳过追加:点数=${points.length} 不足 3 个,无法成环,'
|
||||
'原样返回(避免造出退化多边形)',
|
||||
);
|
||||
return List<LatLng>.from(points);
|
||||
}
|
||||
final LatLng first = points.first;
|
||||
final LatLng last = points.last;
|
||||
if (first.latitude == last.latitude && first.longitude == last.longitude) {
|
||||
debugPrint(
|
||||
'🔗 [闭合] ⏭️ 跳过追加:首尾已经重合'
|
||||
'(lat=${first.latitude}, lng=${first.longitude}),'
|
||||
'点数=${points.length} 保持不变(幂等保护,不重复叠加)',
|
||||
);
|
||||
return List<LatLng>.from(points);
|
||||
}
|
||||
// ✅ 真正执行追加的地方
|
||||
debugPrint(
|
||||
'🔗 [闭合] ✅ 已把第一个点追加到末尾!'
|
||||
'追加的坐标=第[0]个点(lat=${first.latitude}, lng=${first.longitude});'
|
||||
'它成为新的第[${points.length}]号点(即最后一个点);'
|
||||
'点数 ${points.length} → ${points.length + 1};'
|
||||
'原本的第[${points.length - 1}]号点(lat=${last.latitude}, lng=${last.longitude}) 往前让一位',
|
||||
);
|
||||
debugPrint(
|
||||
'🔗 [闭合] ✅ 追加后首尾关系:新[0]=(lat=${first.latitude}, lng=${first.longitude}) '
|
||||
'== 新[${points.length}]=(lat=${first.latitude}, lng=${first.longitude}) → 完全相同',
|
||||
);
|
||||
return [...points, LatLng(first.latitude, first.longitude)];
|
||||
}
|
||||
|
||||
// ========== 抽象:生成路径的核心函数(打点函数) ==========
|
||||
Future<void> _generatePath({bool showTips = true}) async {
|
||||
debugPrint(
|
||||
@@ -3847,7 +4045,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
List<work_area_model.Position> outerPositions = [];
|
||||
if (_currentRobotMode == RobotMode.robot &&
|
||||
_robotModeWgsPoints.isNotEmpty) {
|
||||
outerPositions = _robotModeWgsPoints.map((latLng) {
|
||||
outerPositions = _closeBowRing(_robotModeWgsPoints).map((latLng) {
|
||||
return work_area_model.Position(
|
||||
lat: latLng.latitude,
|
||||
lon: latLng.longitude,
|
||||
@@ -3855,7 +4053,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
}).toList();
|
||||
} else if (_markedPoints.isNotEmpty) {
|
||||
// robot模式但_robotModeWgsPoints为空时,用_markedPoints(已是WGS84)
|
||||
outerPositions = _markedPoints.map((latLng) {
|
||||
outerPositions = _closeBowRing(_markedPoints).map((latLng) {
|
||||
return work_area_model.Position(
|
||||
lat: latLng.latitude,
|
||||
lon: latLng.longitude,
|
||||
@@ -3988,7 +4186,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
_isWorkAreaCompleted = true;
|
||||
_currentObstaclePoints.clear();
|
||||
_robotModeObsWgsPoints.clear();
|
||||
gcjPathPoints = List.from(_markedPoints);
|
||||
// 🔥 覆盖模式:闭合边界(A B C → A B C A),保证地图显示与保存截图一致
|
||||
gcjPathPoints = _closeBowRing(_markedPoints);
|
||||
typedPathList = [];
|
||||
});
|
||||
_saveDataToLocal();
|
||||
@@ -4276,12 +4475,13 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
),
|
||||
),
|
||||
|
||||
/// 悬浮"回到苏州"按钮:一键跳转到 GeoServer 苏州地块(zoom 18 才能看清 250m 地块)
|
||||
/// 悬浮“回到当前场站”按钮:一键跳转到选中场站高清图层(zoom 18 才看得清小地块)
|
||||
Positioned(
|
||||
top: 60,
|
||||
left: 10,
|
||||
child: GestureDetector(
|
||||
onTap: () => _mapController.move(kSuzhouCenter, 18),
|
||||
onTap: () =>
|
||||
_mapController.move(_getDefaultValidCenter(), 18),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -4298,7 +4498,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'苏',
|
||||
'站',
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 16,
|
||||
|
||||
Reference in New Issue
Block a user