299 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			299 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
	
| // lib/utils/location_helper.dart
 | ||
| 
 | ||
| import 'dart:async';
 | ||
| import 'dart:math';
 | ||
| import 'package:flutter/material.dart';
 | ||
| import 'package:geolocator/geolocator.dart';
 | ||
| import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
 | ||
| import 'package:qhd_prevention/customWidget/toast_util.dart';
 | ||
| import 'package:shared_preferences/shared_preferences.dart';
 | ||
| 
 | ||
| /// ============ 常量与坐标转换相关(WGS84/GCJ02/BD09) ============
 | ||
| 
 | ||
| const double _pi = 3.1415926535897932384626;
 | ||
| const double _xPi = _pi * 3000.0 / 180.0;
 | ||
| const double _a = 6378245.0;
 | ||
| const double _ee = 0.006693421622965943;
 | ||
| 
 | ||
| /// 判断是否在中国境内(仅中国境内需要偏移处理)
 | ||
| bool _outOfChina(double lat, double lon) {
 | ||
|   if (lon < 72.004 || lon > 137.8347) return true;
 | ||
|   if (lat < 0.8293 || lat > 55.8271) return true;
 | ||
|   return false;
 | ||
| }
 | ||
| 
 | ||
| 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.0 * 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;
 | ||
| }
 | ||
| 
 | ||
| /// WGS84 -> GCJ02
 | ||
| List<double> wgs84ToGcj02(double lat, double lon) {
 | ||
|   if (_outOfChina(lat, lon)) return [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 [mgLat, mgLon];
 | ||
| }
 | ||
| 
 | ||
| /// GCJ02 -> BD09
 | ||
| List<double> gcj02ToBd09(double lat, double lon) {
 | ||
|   double x = lon;
 | ||
|   double y = lat;
 | ||
|   double z = sqrt(x * x + y * y) + 0.00002 * sin(y * _xPi);
 | ||
|   double theta = atan2(y, x) + 0.000003 * cos(x * _xPi);
 | ||
|   double bdLon = z * cos(theta) + 0.0065;
 | ||
|   double bdLat = z * sin(theta) + 0.006;
 | ||
|   return [bdLat, bdLon];
 | ||
| }
 | ||
| 
 | ||
| /// 直接 WGS84 -> BD09(先 WGS84->GCJ02,再 GCJ02->BD09)
 | ||
| List<double> wgs84ToBd09(double lat, double lon) {
 | ||
|   final gcj = wgs84ToGcj02(lat, lon);
 | ||
|   return gcj02ToBd09(gcj[0], gcj[1]);
 | ||
| }
 | ||
| 
 | ||
| /// ============ 定位相关设置 ============
 | ||
| 
 | ||
| /// 定位请求超时时间(可根据需要调整)
 | ||
| const Duration _locationTimeout = Duration(seconds: 10);
 | ||
| 
 | ||
| /// ============ 新增:获取 Position(带权限/超时/后备策略) ============
 | ||
| /// 返回 Position(WGS84)
 | ||
| Future<Position> getPositionFromGeolocator({
 | ||
|   LocationAccuracy accuracy = LocationAccuracy.high,
 | ||
| }) async {
 | ||
|   // 1. 权限检查与请求(先请求权限)
 | ||
|   LocationPermission permission = await Geolocator.checkPermission();
 | ||
|   if (permission == LocationPermission.denied) {
 | ||
|     permission = await Geolocator.requestPermission();
 | ||
|     if (permission == LocationPermission.denied) {
 | ||
|       // 用户拒绝(不是永久拒绝)
 | ||
|       throw Exception('定位权限被用户拒绝');
 | ||
|     }
 | ||
|   }
 | ||
|   if (permission == LocationPermission.deniedForever) {
 | ||
|     // 永久拒绝(需要用户手动到设置开启)
 | ||
|     throw Exception('定位权限被永久拒绝');
 | ||
|   }
 | ||
| 
 | ||
|   // 2. 检查定位服务是否开启(GPS/定位开关)
 | ||
|   bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
 | ||
|   if (!serviceEnabled) {
 | ||
|     throw Exception('定位服务未开启');
 | ||
|   }
 | ||
| 
 | ||
|   // 3. 尝试获取当前位置(主方案:getCurrentPosition,带超时)
 | ||
|   Position? position;
 | ||
|   try {
 | ||
|     position = await Geolocator.getCurrentPosition(desiredAccuracy: accuracy)
 | ||
|         .timeout(_locationTimeout);
 | ||
|   } on TimeoutException catch (_) {
 | ||
|     position = null;
 | ||
|   } catch (_) {
 | ||
|     position = null;
 | ||
|   }
 | ||
| 
 | ||
|   // 4. 后备:尝试 getLastKnownPosition(可能是旧位置)
 | ||
|   if (position == null) {
 | ||
|     try {
 | ||
|       position = await Geolocator.getLastKnownPosition();
 | ||
|     } catch (_) {
 | ||
|       position = null;
 | ||
|     }
 | ||
|   }
 | ||
|   // 5. 后备:在 Android 上尝试 forceAndroidLocationManager(某些设备/厂商兼容性问题)
 | ||
|   if (position == null) {
 | ||
|     try {
 | ||
|       position = await Geolocator.getCurrentPosition(
 | ||
|         desiredAccuracy: accuracy,
 | ||
|         forceAndroidLocationManager: true,
 | ||
|       ).timeout(_locationTimeout);
 | ||
|     } catch (_) {
 | ||
|       position = null;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // 6. 最终仍无位置 -> 抛异常
 | ||
|   if (position == null) {
 | ||
|     throw Exception('无法获取位置信息,请检查设备定位设置或权限');
 | ||
|   }
 | ||
| 
 | ||
|   return position;
 | ||
| }
 | ||
| 
 | ||
| /// 获取 BD09 坐标(保持向后兼容:仍然返回 [bdLat, bdLon])
 | ||
| /// 内部改为调用 getPositionFromGeolocator 并转换
 | ||
| Future<List<double>> getBd09FromGeolocator({
 | ||
|   LocationAccuracy accuracy = LocationAccuracy.high,
 | ||
| }) async {
 | ||
|   final pos = await getPositionFromGeolocator(accuracy: accuracy);
 | ||
|   return wgs84ToBd09(pos.latitude, pos.longitude);
 | ||
| }
 | ||
| 
 | ||
| /// ============ 错误提示(中文映射) ============
 | ||
| String _mapExceptionToChineseMessage(Object e) {
 | ||
|   final msg = e?.toString() ?? '';
 | ||
| 
 | ||
|   if (msg.contains('定位权限被用户拒绝') || msg.contains('Location permissions are denied') || msg.contains('denied')) {
 | ||
|     return '定位权限被拒绝,请允许应用获取定位权限。';
 | ||
|   }
 | ||
|   if (msg.contains('定位权限被永久拒绝') || msg.contains('deniedForever') || msg.contains('permanently denied')) {
 | ||
|     return '定位权限被永久拒绝,请到系统设置手动开启定位权限。';
 | ||
|   }
 | ||
|   if (msg.contains('定位服务未开启') || msg.contains('Location services are disabled')) {
 | ||
|     return '设备定位功能未开启,请打开系统定位后重试。';
 | ||
|   }
 | ||
|   if (msg.contains('无法获取位置信息') || msg.contains('无法获取位置信息')) {
 | ||
|     return '无法获取有效定位,请检查网络/GPS并重试(可尝试切换到高精度模式)。';
 | ||
|   }
 | ||
|   // 默认返回空字符串,调用方根据空串决定是否显示
 | ||
|   return '定位失败:${msg.replaceAll('Exception: ', '')}';
 | ||
| }
 | ||
| /// 点位数组计算中心点
 | ||
| Map<String, double> geographicCentroid(List<dynamic> points) {
 | ||
|   if (points.isEmpty) {
 | ||
|     throw ArgumentError('points 不能为空');
 | ||
|   }
 | ||
| 
 | ||
|   double x = 0.0, y = 0.0, z = 0.0;
 | ||
|   double altSum = 0.0;
 | ||
|   int altCount = 0;
 | ||
| 
 | ||
|   for (var p in points) {
 | ||
|     final lon = p[0];
 | ||
|     final lat = p[1];
 | ||
| 
 | ||
|     final latR = lat * pi / 180.0;
 | ||
|     final lonR = lon * pi / 180.0;
 | ||
| 
 | ||
|     final cx = cos(latR) * cos(lonR);
 | ||
|     final cy = cos(latR) * sin(lonR);
 | ||
|     final cz = sin(latR);
 | ||
| 
 | ||
|     x += cx;
 | ||
|     y += cy;
 | ||
|     z += cz;
 | ||
| 
 | ||
|     if (p.length > 2 && p[2] != null) {
 | ||
|       altSum += p[2];
 | ||
|       altCount++;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   final cnt = points.length.toDouble();
 | ||
|   x /= cnt;
 | ||
|   y /= cnt;
 | ||
|   z /= cnt;
 | ||
| 
 | ||
|   final hyp = sqrt(x * x + y * y);
 | ||
|   final centroidLat = atan2(z, hyp) * 180.0 / pi;
 | ||
|   final centroidLon = atan2(y, x) * 180.0 / pi;
 | ||
| 
 | ||
|   final result = <String, double>{
 | ||
|     'lon': centroidLon,
 | ||
|     'lat': centroidLat,
 | ||
|   };
 | ||
| 
 | ||
|   if (altCount > 0) {
 | ||
|     result['alt'] = altSum / altCount;
 | ||
|   }
 | ||
| 
 | ||
|   return result;
 | ||
| }
 | ||
| /// ============ 主业务方法:获取并保存 BD09(同时处理 UI 提示/引导) ============
 | ||
| /// 现在会同时保存:WGS84(原始) / GCJ02 / BD09,以及保存时间戳(bd_saved_at)
 | ||
| Future<void> fetchAndSaveBd09(BuildContext context) async {
 | ||
|   try {
 | ||
|     // 获取原始位置(WGS84)
 | ||
|     final Position position = await getPositionFromGeolocator();
 | ||
| 
 | ||
|     final double wgsLat = position.latitude;
 | ||
|     final double wgsLon = position.longitude;
 | ||
| 
 | ||
|     // 计算 GCJ02(在中国境内会偏移,否则返回原始)
 | ||
|     final gcj = wgs84ToGcj02(wgsLat, wgsLon);
 | ||
|     final double gcjLat = gcj[0];
 | ||
|     final double gcjLon = gcj[1];
 | ||
| 
 | ||
|     // 计算 BD09
 | ||
|     final bd = gcj02ToBd09(gcjLat, gcjLon);
 | ||
|     final double bdLat = bd[0];
 | ||
|     final double bdLon = bd[1];
 | ||
| 
 | ||
|     // 保存到 SharedPreferences(同时保存原始 WGS84、GCJ02、BD09,以及保存时间)
 | ||
|     final prefs = await SharedPreferences.getInstance();
 | ||
|     await prefs.setString('wgs84_lat', wgsLat.toString());
 | ||
|     await prefs.setString('wgs84_lon', wgsLon.toString());
 | ||
|     await prefs.setString('gcj02_lat', gcjLat.toString());
 | ||
|     await prefs.setString('gcj02_lon', gcjLon.toString());
 | ||
|     await prefs.setString('bd_lat', bdLat.toString());
 | ||
|     await prefs.setString('bd_lon', bdLon.toString());
 | ||
|     await prefs.setInt('bd_saved_at', DateTime.now().millisecondsSinceEpoch);
 | ||
| 
 | ||
|     // 成功提示(按需开启)
 | ||
|     // ToastUtil.showNormal(context, '定位成功:$bdLat, $bdLon');
 | ||
|   } on Exception catch (e) {
 | ||
|     final msg = e.toString();
 | ||
| 
 | ||
|     // 定位权限被永久拒绝 -> 引导用户打开应用设置
 | ||
|     if (msg.contains('定位权限被永久拒绝') || msg.contains('deniedForever') || msg.contains('permanently denied')) {
 | ||
|       final open = await CustomAlertDialog.showConfirm(
 | ||
|         context,
 | ||
|         title: '定位权限',
 | ||
|         content: '定位权限被永久拒绝,需要手动到应用设置开启定位权限,是否现在打开设置?',
 | ||
|         cancelText: '取消',
 | ||
|         confirmText: '去设置',
 | ||
|       );
 | ||
|       if (open == true) {
 | ||
|         await Geolocator.openAppSettings();
 | ||
|       }
 | ||
|     }
 | ||
|     // 定位服务未开启 -> 引导用户打开系统定位设置
 | ||
|     else if (msg.contains('定位服务未开启') || msg.contains('Location services are disabled')) {
 | ||
|       final open = await CustomAlertDialog.showConfirm(
 | ||
|         context,
 | ||
|         title: '打开定位',
 | ||
|         content: '检测到设备定位服务未开启,是否打开系统定位设置?',
 | ||
|         cancelText: '取消',
 | ||
|         confirmText: '去打开',
 | ||
|       );
 | ||
|       if (open == true) {
 | ||
|         await Geolocator.openLocationSettings();
 | ||
|       }
 | ||
|     }
 | ||
|     // 其它错误 -> 以 toast 显示中文提示(如果映射为空则显示原错误)
 | ||
|     else {
 | ||
|       final userMsg = _mapExceptionToChineseMessage(e);
 | ||
|       if (userMsg.isNotEmpty) {
 | ||
|         // ToastUtil.showError(context, userMsg);
 | ||
|       } else {
 | ||
|         // ToastUtil.showError(context, '定位失败:${e.toString()}');
 | ||
|       }
 | ||
|     }
 | ||
|   } catch (e) {
 | ||
|     // 捕获任何未预期异常
 | ||
|     // ToastUtil.showError(context, '发生未知错误:${e.toString()}');
 | ||
|   } finally {
 | ||
|     // 如需隐藏 loading 可在此处处理
 | ||
|   }
 | ||
| }
 |