expo-gaode-map-navigation 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +213 -73
  2. package/android/build.gradle +10 -0
  3. package/android/src/main/cpp/CMakeLists.txt +24 -0
  4. package/android/src/main/cpp/cluster_jni.cpp +848 -0
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +616 -92
  6. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +493 -0
  7. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +230 -14
  8. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapViewModule.kt +37 -27
  9. package/android/src/main/java/expo/modules/gaodemap/map/MapPreloadManager.kt +494 -0
  10. package/android/src/main/java/expo/modules/gaodemap/map/companion/BitmapDescriptorCache.kt +30 -0
  11. package/android/src/main/java/expo/modules/gaodemap/map/companion/IconBitmapCache.kt +37 -0
  12. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +76 -0
  13. package/android/src/main/java/expo/modules/gaodemap/map/modules/LocationManager.kt +15 -3
  14. package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +4 -59
  15. package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleView.kt +9 -12
  16. package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleViewModule.kt +5 -6
  17. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +539 -66
  18. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterViewModule.kt +17 -1
  19. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +165 -33
  20. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +15 -3
  21. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +1249 -672
  22. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +40 -17
  23. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointView.kt +177 -22
  24. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointViewModule.kt +11 -3
  25. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonView.kt +57 -14
  26. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonViewModule.kt +9 -5
  27. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineView.kt +90 -63
  28. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineViewModule.kt +7 -3
  29. package/android/src/main/java/expo/modules/gaodemap/map/services/LocationForegroundService.kt +3 -2
  30. package/android/src/main/java/expo/modules/gaodemap/map/utils/BitmapDescriptorCache.kt +20 -0
  31. package/android/src/main/java/expo/modules/gaodemap/map/utils/ClusterNative.kt +13 -0
  32. package/android/src/main/java/expo/modules/gaodemap/map/utils/ColorParser.kt +20 -0
  33. package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +515 -0
  34. package/android/src/main/java/expo/modules/gaodemap/map/utils/LatLngParser.kt +91 -0
  35. package/android/src/main/java/expo/modules/gaodemap/map/utils/PermissionHelper.kt +248 -0
  36. package/build/ExpoGaodeMapNaviView.d.ts +7 -7
  37. package/build/ExpoGaodeMapNaviView.js +10 -11
  38. package/build/ExpoGaodeMapNavigationModule.d.ts +2 -1
  39. package/build/index.d.ts +35 -33
  40. package/build/index.js +70 -106
  41. package/build/map/ExpoGaodeMapModule.d.ts +2 -201
  42. package/build/map/ExpoGaodeMapModule.js +586 -18
  43. package/build/map/ExpoGaodeMapOfflineModule.d.ts +139 -0
  44. package/build/map/ExpoGaodeMapOfflineModule.js +8 -0
  45. package/build/map/ExpoGaodeMapView.js +66 -58
  46. package/build/map/components/FoldableMapView.d.ts +38 -0
  47. package/build/map/components/FoldableMapView.js +209 -0
  48. package/build/map/components/MapContext.d.ts +12 -0
  49. package/build/map/components/MapContext.js +54 -0
  50. package/build/map/components/MapUI.d.ts +18 -0
  51. package/build/map/components/MapUI.js +29 -0
  52. package/build/map/components/overlays/Circle.js +34 -3
  53. package/build/map/components/overlays/Cluster.d.ts +3 -1
  54. package/build/map/components/overlays/Cluster.js +31 -2
  55. package/build/map/components/overlays/HeatMap.d.ts +3 -1
  56. package/build/map/components/overlays/HeatMap.js +33 -3
  57. package/build/map/components/overlays/Marker.d.ts +1 -1
  58. package/build/map/components/overlays/Marker.js +37 -32
  59. package/build/map/components/overlays/MultiPoint.js +1 -1
  60. package/build/map/components/overlays/Polygon.js +30 -3
  61. package/build/map/components/overlays/Polyline.js +36 -3
  62. package/build/map/index.d.ts +25 -5
  63. package/build/map/index.js +59 -18
  64. package/build/map/types/common.types.d.ts +40 -0
  65. package/build/map/types/common.types.js +0 -4
  66. package/build/map/types/index.d.ts +3 -2
  67. package/build/map/types/map-view.types.d.ts +108 -3
  68. package/build/map/types/native-module.types.d.ts +363 -0
  69. package/build/map/types/native-module.types.js +5 -0
  70. package/build/map/types/offline.types.d.ts +132 -0
  71. package/build/map/types/offline.types.js +5 -0
  72. package/build/map/types/overlays.types.d.ts +137 -24
  73. package/build/map/utils/ErrorHandler.d.ts +110 -0
  74. package/build/map/utils/ErrorHandler.js +421 -0
  75. package/build/map/utils/GeoUtils.d.ts +20 -0
  76. package/build/map/utils/GeoUtils.js +76 -0
  77. package/build/map/utils/OfflineMapManager.d.ts +148 -0
  78. package/build/map/utils/OfflineMapManager.js +217 -0
  79. package/build/map/utils/PermissionUtils.d.ts +91 -0
  80. package/build/map/utils/PermissionUtils.js +255 -0
  81. package/build/map/utils/PlatformDetector.d.ts +102 -0
  82. package/build/map/utils/PlatformDetector.js +186 -0
  83. package/build/types/index.d.ts +1 -0
  84. package/build/types/index.js +1 -0
  85. package/build/types/native-module.types.d.ts +69 -0
  86. package/build/types/native-module.types.js +2 -0
  87. package/build/types/naviview.types.d.ts +1 -1
  88. package/expo-module.config.json +12 -10
  89. package/ios/ExpoGaodeMapNavigation.podspec +9 -0
  90. package/ios/map/ExpoGaodeMapModule.swift +485 -75
  91. package/ios/map/ExpoGaodeMapOfflineModule.swift +479 -0
  92. package/ios/map/ExpoGaodeMapView.swift +611 -62
  93. package/ios/map/ExpoGaodeMapViewModule.swift +48 -26
  94. package/ios/map/MapPreloadManager.swift +348 -0
  95. package/ios/map/cpp/ClusterEngine.cpp +110 -0
  96. package/ios/map/cpp/ClusterEngine.hpp +20 -0
  97. package/ios/map/cpp/ColorParser.cpp +135 -0
  98. package/ios/map/cpp/ColorParser.hpp +14 -0
  99. package/ios/map/cpp/GeometryEngine.cpp +574 -0
  100. package/ios/map/cpp/GeometryEngine.hpp +159 -0
  101. package/ios/map/cpp/QuadTree.cpp +92 -0
  102. package/ios/map/cpp/QuadTree.hpp +42 -0
  103. package/ios/map/cpp/README.md +55 -0
  104. package/ios/map/managers/UIManager.swift +72 -1
  105. package/ios/map/modules/LocationManager.swift +123 -166
  106. package/ios/map/overlays/CircleView.swift +16 -32
  107. package/ios/map/overlays/CircleViewModule.swift +12 -12
  108. package/ios/map/overlays/ClusterAnnotation.swift +32 -0
  109. package/ios/map/overlays/ClusterView.swift +331 -45
  110. package/ios/map/overlays/ClusterViewModule.swift +20 -6
  111. package/ios/map/overlays/HeatMapView.swift +135 -32
  112. package/ios/map/overlays/HeatMapViewModule.swift +20 -8
  113. package/ios/map/overlays/MarkerView.swift +613 -130
  114. package/ios/map/overlays/MarkerViewModule.swift +38 -18
  115. package/ios/map/overlays/MultiPointView.swift +168 -10
  116. package/ios/map/overlays/MultiPointViewModule.swift +27 -5
  117. package/ios/map/overlays/PolygonView.swift +62 -23
  118. package/ios/map/overlays/PolygonViewModule.swift +18 -12
  119. package/ios/map/overlays/PolylineView.swift +21 -13
  120. package/ios/map/overlays/PolylineViewModule.swift +18 -12
  121. package/ios/map/utils/ClusterNative.h +96 -0
  122. package/ios/map/utils/ClusterNative.mm +377 -0
  123. package/ios/map/utils/ColorParser.swift +12 -1
  124. package/ios/map/utils/CppBridging.mm +13 -0
  125. package/ios/map/utils/GeometryUtils.swift +34 -0
  126. package/ios/map/utils/LatLngParser.swift +87 -0
  127. package/ios/map/utils/PermissionManager.swift +135 -6
  128. package/package.json +3 -2
  129. package/shared/cpp/ClusterEngine.cpp +110 -0
  130. package/shared/cpp/ClusterEngine.hpp +20 -0
  131. package/shared/cpp/ColorParser.cpp +135 -0
  132. package/shared/cpp/ColorParser.hpp +14 -0
  133. package/shared/cpp/GeometryEngine.cpp +574 -0
  134. package/shared/cpp/GeometryEngine.hpp +159 -0
  135. package/shared/cpp/QuadTree.cpp +92 -0
  136. package/shared/cpp/QuadTree.hpp +42 -0
  137. package/shared/cpp/README.md +55 -0
  138. package/shared/cpp/tests/benchmark_js.js +41 -0
  139. package/shared/cpp/tests/run.sh +17 -0
  140. package/shared/cpp/tests/test_main.cpp +276 -0
  141. package/build/map/ExpoGaodeMap.types.d.ts +0 -41
  142. package/build/map/ExpoGaodeMap.types.js +0 -24
  143. package/build/map/utils/EventManager.d.ts +0 -10
  144. package/build/map/utils/EventManager.js +0 -26
  145. package/build/map/utils/ModuleLoader.d.ts +0 -73
  146. package/build/map/utils/ModuleLoader.js +0 -112
@@ -1,19 +1,17 @@
1
1
  package expo.modules.gaodemap.map
2
2
 
3
- import android.Manifest
4
- import android.content.pm.PackageManager
5
- import android.os.Handler
6
- import android.os.Looper
7
- import android.util.Log
8
- import androidx.core.app.ActivityCompat
9
- import androidx.core.content.ContextCompat
3
+ import com.amap.api.maps.MapsInitializer
4
+ import com.amap.api.maps.model.LatLng
10
5
  import expo.modules.kotlin.modules.Module
11
6
  import expo.modules.kotlin.modules.ModuleDefinition
12
7
  import expo.modules.gaodemap.map.modules.SDKInitializer
13
8
  import expo.modules.gaodemap.map.modules.LocationManager
14
- import expo.modules.kotlin.Promise
15
- import expo.modules.kotlin.exception.CodedException
16
- import java.lang.ref.WeakReference
9
+ import expo.modules.gaodemap.map.utils.GeometryUtils
10
+ import kotlin.math.max
11
+ import kotlin.math.abs
12
+ import expo.modules.gaodemap.map.utils.LatLngParser
13
+
14
+ import expo.modules.gaodemap.map.utils.PermissionHelper
17
15
 
18
16
  /**
19
17
  * 高德地图 Expo 模块
@@ -25,36 +23,54 @@ import java.lang.ref.WeakReference
25
23
  */
26
24
  class ExpoGaodeMapModule : Module() {
27
25
 
28
-
26
+
29
27
  /** 定位管理器实例 */
30
28
  private var locationManager: LocationManager? = null
31
29
 
32
30
  override fun definition() = ModuleDefinition {
33
- Name("NaviMap")
31
+ Name("ExpoGaodeMap")
34
32
 
35
33
  // 在模块加载时尝试从本地缓存恢复隐私同意状态,避免每次启动都必须 JS 调用
36
34
  try {
37
- SDKInitializer.restorePrivacyState(appContext.reactContext!!)
35
+ val context = appContext.reactContext!!
36
+ SDKInitializer.restorePrivacyState(context)
37
+
38
+ // 初始化预加载管理器(注册内存监听)
39
+ MapPreloadManager.initialize(context)
40
+
41
+ // 尝试从 AndroidManifest.xml 读取并设置 API Key
42
+ val apiKey = context.packageManager
43
+ .getApplicationInfo(context.packageName, android.content.pm.PackageManager.GET_META_DATA)
44
+ .metaData?.getString("com.amap.api.v2.apikey")
45
+
46
+ if (!apiKey.isNullOrEmpty()) {
47
+ try {
48
+ MapsInitializer.setApiKey(apiKey)
49
+ com.amap.api.location.AMapLocationClient.setApiKey(apiKey)
50
+
51
+
52
+ // 只有在 API Key 已设置的情况下才启动预加载
53
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
54
+ android.util.Log.i("ExpoGaodeMap", "🚀 自动启动地图预加载")
55
+ MapPreloadManager.startPreload(context, poolSize = 1)
56
+ }, 2000)
57
+ } catch (e: Exception) {
58
+ android.util.Log.w("ExpoGaodeMap", "设置 API Key 失败: ${e.message}")
59
+ }
60
+ } else {
61
+ android.util.Log.w("ExpoGaodeMap", "⚠️ AndroidManifest.xml 未找到 API Key,跳过自动预加载")
62
+ }
63
+
38
64
  } catch (e: Exception) {
39
65
  android.util.Log.w("ExpoGaodeMap", "恢复隐私状态时出现问题: ${e.message}")
40
66
  }
41
67
 
42
- // ==================== 隐私合规管理 ====================
43
-
44
- /**
45
- * 更新隐私合规状态
46
- * 必须在用户同意隐私协议后调用
47
- * @param hasAgreed 用户是否已同意隐私协议
48
- */
49
- Function("updatePrivacyCompliance") { hasAgreed: Boolean ->
50
- SDKInitializer.updatePrivacyCompliance(appContext.reactContext!!, hasAgreed)
51
- }
52
-
68
+
53
69
  // ==================== SDK 初始化 ====================
54
70
 
55
71
  /**
56
72
  * 初始化 SDK(地图 + 定位)
57
- * @param config 配置对象,包含 androidKey
73
+ * @config 配置对象,包含 androidKey
58
74
  */
59
75
  Function("initSDK") { config: Map<String, String> ->
60
76
  val androidKey = config["androidKey"]
@@ -62,16 +78,27 @@ class ExpoGaodeMapModule : Module() {
62
78
  try {
63
79
  SDKInitializer.initSDK(appContext.reactContext!!, androidKey)
64
80
  getLocationManager() // 初始化定位管理器
81
+
82
+ // 初始化成功后自动触发一次预加载
83
+ MapPreloadManager.startPreload(appContext.reactContext!!, poolSize = 1)
65
84
  } catch (e: SecurityException) {
66
- Log.e("ExpoGaodeMap", "隐私协议未同意: ${e.message}")
67
- throw e
85
+ android.util.Log.e("ExpoGaodeMap", "隐私协议未同意: ${e.message}")
86
+ throw expo.modules.kotlin.exception.CodedException("PRIVACY_NOT_AGREED", e.message ?: "用户未同意隐私协议", e)
68
87
  } catch (e: Exception) {
69
- Log.e("ExpoGaodeMap", "SDK 初始化失败: ${e.message}")
70
- throw e
88
+ android.util.Log.e("ExpoGaodeMap", "SDK 初始化失败: ${e.message}")
89
+ throw expo.modules.kotlin.exception.CodedException("INIT_FAILED", e.message ?: "SDK 初始化失败", e)
71
90
  }
72
91
  }
73
92
  }
74
93
 
94
+ /**
95
+ * 设置是否加载世界向量地图
96
+ * @param enable 是否开启
97
+ */
98
+ Function("setLoadWorldVectorMap") { enable: Boolean ->
99
+ MapsInitializer.loadWorldVectorMap(enable)
100
+ }
101
+
75
102
  /**
76
103
  * 获取 SDK 版本
77
104
  * @return SDK 版本号
@@ -80,21 +107,31 @@ class ExpoGaodeMapModule : Module() {
80
107
  SDKInitializer.getVersion()
81
108
  }
82
109
 
110
+ /**
111
+ * 检查原生 SDK 是否已配置 API Key
112
+ */
113
+ Function("isNativeSDKConfigured") {
114
+ try {
115
+ val context = appContext.reactContext!!
116
+ val apiKey = context.packageManager
117
+ .getApplicationInfo(context.packageName, android.content.pm.PackageManager.GET_META_DATA)
118
+ .metaData?.getString("com.amap.api.v2.apikey")
119
+ !apiKey.isNullOrEmpty()
120
+ } catch (_: Exception) {
121
+ false
122
+ }
123
+ }
124
+
125
+
83
126
  // ==================== 定位功能 ====================
84
127
 
85
128
  /**
86
129
  * 开始连续定位
87
130
  */
88
131
  Function("start") {
89
- // 检查隐私协议状态
90
- if (!SDKInitializer.isPrivacyAgreed()) {
91
- Log.w("ExpoGaodeMap", "用户未同意隐私协议,无法开始定位")
92
- throw CodedException("用户未同意隐私协议,无法开始定位")
93
- }
94
-
95
132
  getLocationManager().start()
96
133
  }
97
-
134
+
98
135
  /**
99
136
  * 停止定位
100
137
  */
@@ -106,7 +143,7 @@ class ExpoGaodeMapModule : Module() {
106
143
  * 检查是否正在定位
107
144
  * @return 是否正在定位
108
145
  */
109
- AsyncFunction("isStarted") { promise: Promise ->
146
+ AsyncFunction("isStarted") { promise: expo.modules.kotlin.Promise ->
110
147
  promise.resolve(getLocationManager().isStarted())
111
148
  }
112
149
 
@@ -114,13 +151,7 @@ class ExpoGaodeMapModule : Module() {
114
151
  * 获取当前位置(单次定位)
115
152
  * @return 位置信息对象
116
153
  */
117
- AsyncFunction("getCurrentLocation") { promise: Promise ->
118
- // 检查隐私协议状态
119
- if (!SDKInitializer.isPrivacyAgreed()) {
120
- promise.reject("PRIVACY_NOT_AGREED", "用户未同意隐私协议,无法获取位置", null)
121
- return@AsyncFunction
122
- }
123
-
154
+ AsyncFunction("getCurrentLocation") { promise: expo.modules.kotlin.Promise ->
124
155
  getLocationManager().getCurrentLocation(promise)
125
156
  }
126
157
 
@@ -130,15 +161,416 @@ class ExpoGaodeMapModule : Module() {
130
161
  * @param type 坐标类型
131
162
  * @return 转换后的坐标
132
163
  */
133
- AsyncFunction("coordinateConvert") { coordinate: Map<String, Double>, type: Int, promise: Promise ->
134
- getLocationManager().coordinateConvert(coordinate, type, promise)
164
+ AsyncFunction("coordinateConvert") { coordinate: Map<String, Any>?, type: Int, promise: expo.modules.kotlin.Promise ->
165
+ val latLng = LatLngParser.parseLatLng(coordinate)
166
+ if (latLng != null) {
167
+ val coordMap = mapOf("latitude" to latLng.latitude, "longitude" to latLng.longitude)
168
+ getLocationManager().coordinateConvert(coordMap, type, promise)
169
+ } else {
170
+ promise.reject("INVALID_COORDINATE", "Invalid coordinate format", null)
171
+ }
172
+ }
173
+
174
+ // ==================== 几何计算 ====================
175
+
176
+ /**
177
+ * 计算两个坐标点之间的距离
178
+ * @param coordinate1 第一个坐标点
179
+ * @param coordinate2 第二个坐标点
180
+ * @returns 两点之间的距离(单位:米)
181
+ */
182
+ Function("distanceBetweenCoordinates") { p1: Map<String, Any>?, p2: Map<String, Any>? ->
183
+ val coord1 = LatLngParser.parseLatLng(p1)
184
+ val coord2 = LatLngParser.parseLatLng(p2)
185
+ if (coord1 != null && coord2 != null) {
186
+ GeometryUtils.calculateDistance(coord1, coord2)
187
+ } else {
188
+ 0.0
189
+ }
190
+ }
191
+
192
+ /**
193
+ * 计算多边形面积
194
+ * @param points 多边形顶点坐标数组,支持嵌套数组(多边形空洞)
195
+ * @return 面积(平方米)
196
+ */
197
+ Function("calculatePolygonArea") { points: List<Any>? ->
198
+ val rings = LatLngParser.parseLatLngListList(points)
199
+ if (rings.isEmpty()) return@Function 0.0
200
+
201
+ // 第一项是外轮廓
202
+ var totalArea = GeometryUtils.calculatePolygonArea(rings[0])
203
+
204
+ // 后续项是内孔,需要减去面积
205
+ if (rings.size > 1) {
206
+ for (i in 1 until rings.size) {
207
+ totalArea -= GeometryUtils.calculatePolygonArea(rings[i])
208
+ }
209
+ }
210
+
211
+ // 确保面积不为负数
212
+ max(0.0, totalArea)
213
+ }
214
+
215
+ /**
216
+ * 判断点是否在多边形内
217
+ * @param point 待判断点
218
+ * @param polygon 多边形顶点坐标数组,支持嵌套数组(多边形空洞)
219
+ * @return 是否在多边形内
220
+ */
221
+ Function("isPointInPolygon") { point: Map<String, Any>?, polygon: List<Any>? ->
222
+ val pt = LatLngParser.parseLatLng(point) ?: return@Function false
223
+ val rings = LatLngParser.parseLatLngListList(polygon)
224
+ if (rings.isEmpty()) return@Function false
225
+
226
+ // 点必须在外轮廓内
227
+ val inOuter = GeometryUtils.isPointInPolygon(pt, rings[0])
228
+ if (!inOuter) return@Function false
229
+
230
+ // 点不能在任何内孔内
231
+ if (rings.size > 1) {
232
+ for (i in 1 until rings.size) {
233
+ if (GeometryUtils.isPointInPolygon(pt, rings[i])) {
234
+ return@Function false
235
+ }
236
+ }
237
+ }
238
+
239
+ true
240
+ }
241
+
242
+ /**
243
+ * 判断点是否在圆内
244
+ * @param point 待判断点
245
+ * @param center 圆心坐标
246
+ * @param radius 圆半径(米)
247
+ * @return 是否在圆内
248
+ */
249
+ Function("isPointInCircle") { point: Map<String, Any>?, center: Map<String, Any>?, radius: Double ->
250
+ val pt = LatLngParser.parseLatLng(point)
251
+ val cn = LatLngParser.parseLatLng(center)
252
+ if (pt != null && cn != null) {
253
+ GeometryUtils.isPointInCircle(pt, cn, radius)
254
+ } else {
255
+ false
256
+ }
257
+ }
258
+
259
+ /**
260
+ * 计算矩形面积
261
+ * @param southWest 西南角
262
+ * @param northEast 东北角
263
+ * @return 面积(平方米)
264
+ */
265
+ Function("calculateRectangleArea") { southWest: Map<String, Any>?, northEast: Map<String, Any>? ->
266
+ val sw = LatLngParser.parseLatLng(southWest)
267
+ val ne = LatLngParser.parseLatLng(northEast)
268
+ if (sw != null && ne != null) {
269
+ GeometryUtils.calculateRectangleArea(sw, ne)
270
+ } else {
271
+ 0.0
272
+ }
273
+ }
274
+
275
+ /**
276
+ * 获取路径上距离目标点最近的点
277
+ * @param path 路径点集合
278
+ * @param target 目标点
279
+ * @return 最近点结果
280
+ */
281
+ Function("getNearestPointOnPath") { path: List<Any>?, target: Map<String, Any>? ->
282
+ val pathPoints = LatLngParser.parseLatLngList(path)
283
+ val targetPoint = LatLngParser.parseLatLng(target)
284
+
285
+ if (targetPoint != null && pathPoints.isNotEmpty()) {
286
+ val result = GeometryUtils.getNearestPointOnPath(pathPoints, targetPoint)
287
+ if (result != null) {
288
+ mapOf(
289
+ "latitude" to result.point.latitude,
290
+ "longitude" to result.point.longitude,
291
+ "index" to result.index,
292
+ "distanceMeters" to result.distanceMeters
293
+ )
294
+ } else {
295
+ null
296
+ }
297
+ } else {
298
+ null
299
+ }
300
+ }
301
+
302
+ /**
303
+ * 计算多边形质心
304
+ * @param polygon 多边形顶点坐标数组,支持嵌套数组(多边形空洞)
305
+ * @return 质心坐标
306
+ */
307
+ Function("calculateCentroid") { polygon: List<Any>? ->
308
+ val rings = LatLngParser.parseLatLngListList(polygon)
309
+ if (rings.isEmpty()) return@Function null
310
+
311
+ if (rings.size == 1) {
312
+ val result = GeometryUtils.calculateCentroid(rings[0])
313
+ return@Function result?.let {
314
+ mapOf(
315
+ "latitude" to it.latitude,
316
+ "longitude" to it.longitude
317
+ )
318
+ }
319
+ }
320
+
321
+ // 带孔多边形的质心计算: Σ(Area_i * Centroid_i) / Σ(Area_i)
322
+ // 注意: 这里的 Area 是带符号的,或者我们手动减去孔的贡献
323
+ var totalArea = 0.0
324
+ var sumLat = 0.0
325
+ var sumLon = 0.0
326
+
327
+ for (i in rings.indices) {
328
+ val ring = rings[i]
329
+ val area = GeometryUtils.calculatePolygonArea(ring)
330
+ val centroid = GeometryUtils.calculateCentroid(ring)
331
+
332
+ if (centroid != null) {
333
+ // 第一项是外轮廓(正),后续是内孔(负)
334
+ val factor = if (i == 0) 1.0 else -1.0
335
+ val signedArea = area * factor
336
+
337
+ totalArea += signedArea
338
+ sumLat += centroid.latitude * signedArea
339
+ sumLon += centroid.longitude * signedArea
340
+ }
341
+ }
342
+
343
+ if (abs(totalArea) > 1e-9) {
344
+ mapOf(
345
+ "latitude" to sumLat / totalArea,
346
+ "longitude" to sumLon / totalArea
347
+ )
348
+ } else {
349
+ null
350
+ }
351
+ }
352
+
353
+ /**
354
+ * 计算路径边界
355
+ * @param pointsList 路径点集合
356
+ * @return 边界信息
357
+ */
358
+ Function("calculatePathBounds") { pointsList: List<Any>? ->
359
+ val points = LatLngParser.parseLatLngList(pointsList)
360
+ if (points.isEmpty()) return@Function null
361
+
362
+ val result = GeometryUtils.calculatePathBounds(points)
363
+ result?.let {
364
+ mapOf(
365
+ "north" to it.north,
366
+ "south" to it.south,
367
+ "east" to it.east,
368
+ "west" to it.west,
369
+ "center" to mapOf(
370
+ "latitude" to it.centerLat,
371
+ "longitude" to it.centerLon
372
+ )
373
+ )
374
+ }
375
+ }
376
+
377
+ /**
378
+ * GeoHash 编码
379
+ * @param coordinate 坐标点
380
+ * @param precision 精度 (1-12)
381
+ * @return GeoHash 字符串
382
+ */
383
+ Function("encodeGeoHash") { coordinate: Map<String, Any>?, precision: Int ->
384
+ val latLng = LatLngParser.parseLatLng(coordinate)
385
+ if (latLng != null) {
386
+ GeometryUtils.encodeGeoHash(latLng, precision)
387
+ } else {
388
+ ""
389
+ }
390
+ }
391
+
392
+ /**
393
+ * 轨迹抽稀 (RDP 算法)
394
+ * @param points 原始轨迹点
395
+ * @param tolerance 允许误差(米)
396
+ * @return 简化后的轨迹点
397
+ */
398
+ Function("simplifyPolyline") { points: List<Any>?, tolerance: Double ->
399
+ val poly = LatLngParser.parseLatLngList(points)
400
+ val simplified = GeometryUtils.simplifyPolyline(poly, tolerance)
401
+ simplified.map {
402
+ mapOf(
403
+ "latitude" to it.latitude,
404
+ "longitude" to it.longitude
405
+ )
406
+ }
407
+ }
408
+
409
+ /**
410
+ * 计算路径总长度
411
+ * @param points 路径点
412
+ * @return 长度(米)
413
+ */
414
+ Function("calculatePathLength") { points: List<Any>? ->
415
+ val poly = LatLngParser.parseLatLngList(points)
416
+ GeometryUtils.calculatePathLength(poly)
417
+ }
418
+
419
+ /**
420
+ * 解析高德地图 API 返回的 Polyline 字符串
421
+ * @param polylineStr 高德原始 polyline 字符串
422
+ * @return 坐标点列表
423
+ */
424
+ Function("parsePolyline") { polylineStr: String? ->
425
+ val result = GeometryUtils.parsePolyline(polylineStr)
426
+ result.map {
427
+ mapOf(
428
+ "latitude" to it.latitude,
429
+ "longitude" to it.longitude
430
+ )
431
+ }
432
+ }
433
+
434
+ /**
435
+ * 获取路径上指定距离的点
436
+ * @param points 路径点
437
+ * @param distance 距离起点的米数
438
+ * @return 点信息(坐标+角度)
439
+ */
440
+ Function("getPointAtDistance") { points: List<Any>?, distance: Double ->
441
+ val poly = LatLngParser.parseLatLngList(points)
442
+ val result = GeometryUtils.getPointAtDistance(poly, distance)
443
+ if (result != null) {
444
+ mapOf(
445
+ "latitude" to result.point.latitude,
446
+ "longitude" to result.point.longitude,
447
+ "angle" to result.angle
448
+ )
449
+ } else {
450
+ null
451
+ }
452
+ }
453
+
454
+ /**
455
+ * 经纬度转瓦片坐标
456
+ * @param coordinate 坐标
457
+ * @param zoom 缩放级别
458
+ * @return 瓦片坐标 [x, y]
459
+ */
460
+ Function("latLngToTile") { coordinate: Map<String, Any>?, zoom: Int ->
461
+ val latLng = LatLngParser.parseLatLng(coordinate)
462
+ if (latLng != null) {
463
+ val result = GeometryUtils.latLngToTile(latLng, zoom)
464
+ if (result != null && result.size >= 2) {
465
+ mapOf("x" to result[0], "y" to result[1])
466
+ } else {
467
+ null
468
+ }
469
+ } else {
470
+ null
471
+ }
472
+ }
473
+
474
+ /**
475
+ * 瓦片坐标转经纬度
476
+ * @param tile 瓦片坐标 {x, y, z}
477
+ * @return 坐标
478
+ */
479
+ Function("tileToLatLng") { tile: Map<String, Any>? ->
480
+ val x = (tile?.get("x") as? Number)?.toInt() ?: 0
481
+ val y = (tile?.get("y") as? Number)?.toInt() ?: 0
482
+ val zoom = (tile?.get("z") as? Number)?.toInt() ?: (tile?.get("zoom") as? Number)?.toInt() ?: 0
483
+ val result = GeometryUtils.tileToLatLng(x, y, zoom)
484
+ result?.let {
485
+ mapOf("latitude" to it.latitude, "longitude" to it.longitude)
486
+ }
487
+ }
488
+
489
+ /**
490
+ * 经纬度转像素坐标
491
+ * @param coordinate 坐标
492
+ * @param zoom 缩放级别
493
+ * @return 像素坐标 [x, y]
494
+ */
495
+ Function("latLngToPixel") { coordinate: Map<String, Any>?, zoom: Int ->
496
+ val latLng = LatLngParser.parseLatLng(coordinate)
497
+ if (latLng != null) {
498
+ val result = GeometryUtils.latLngToPixel(latLng, zoom)
499
+ if (result != null && result.size >= 2) {
500
+ mapOf("x" to result[0], "y" to result[1])
501
+ } else {
502
+ null
503
+ }
504
+ } else {
505
+ null
506
+ }
507
+ }
508
+
509
+ /**
510
+ * 像素坐标转经纬度
511
+ * @param pixel 像素坐标 {x, y}
512
+ * @param zoom 缩放级别
513
+ * @return 坐标
514
+ */
515
+ Function("pixelToLatLng") { pixel: Map<String, Any>?, zoom: Int ->
516
+ val x = (pixel?.get("x") as? Number)?.toDouble() ?: 0.0
517
+ val y = (pixel?.get("y") as? Number)?.toDouble() ?: 0.0
518
+ val result = GeometryUtils.pixelToLatLng(x, y, zoom)
519
+ result?.let {
520
+ mapOf("latitude" to it.latitude, "longitude" to it.longitude)
521
+ }
522
+ }
523
+
524
+ /**
525
+ * 批量判断点在哪个多边形内
526
+ * @param point 待判断点
527
+ * @param polygons 多边形列表
528
+ * @return 所在多边形的索引,不在任何多边形内返回 -1
529
+ */
530
+ Function("findPointInPolygons") { point: Map<String, Any>?, polygons: List<List<Any>>? ->
531
+ val pt = LatLngParser.parseLatLng(point)
532
+ val polys = polygons?.map { LatLngParser.parseLatLngList(it) }
533
+ if (pt != null && polys != null) {
534
+ GeometryUtils.findPointInPolygons(pt, polys)
535
+ } else {
536
+ -1
537
+ }
538
+ }
539
+
540
+ /**
541
+ * 生成网格聚合数据 (常用于展示网格聚合图或大规模点数据处理)
542
+ * @param points 包含经纬度和权重的点数组
543
+ * @param gridSizeMeters 网格大小(米)
544
+ */
545
+ Function("generateHeatmapGrid") { points: List<Map<String, Any>>?, gridSizeMeters: Double ->
546
+ if (points == null || points.isEmpty()) return@Function emptyList<Map<String, Any>>()
547
+
548
+ val count = points.size
549
+ val latitudes = DoubleArray(count)
550
+ val longitudes = DoubleArray(count)
551
+ val weights = DoubleArray(count)
552
+
553
+ points.forEachIndexed { index, map ->
554
+ latitudes[index] = (map["latitude"] as? Number)?.toDouble() ?: 0.0
555
+ longitudes[index] = (map["longitude"] as? Number)?.toDouble() ?: 0.0
556
+ weights[index] = (map["weight"] as? Number)?.toDouble() ?: 1.0
557
+ }
558
+
559
+ val result = GeometryUtils.generateHeatmapGrid(latitudes, longitudes, weights, gridSizeMeters)
560
+ result.map {
561
+ mapOf(
562
+ "latitude" to it.latitude,
563
+ "longitude" to it.longitude,
564
+ "intensity" to it.intensity
565
+ )
566
+ }
135
567
  }
136
568
 
137
569
  // ==================== 定位配置 ====================
138
570
 
139
571
  /**
140
572
  * 设置是否返回逆地理信息
141
- * @param isReGeocode 是否返回逆地理信息
573
+ * @param isReGeocode 是否返回逆地理信息+
142
574
  */
143
575
  Function("setLocatingWithReGeocode") { isReGeocode: Boolean ->
144
576
  getLocationManager().setLocatingWithReGeocode(isReGeocode)
@@ -281,54 +713,70 @@ class ExpoGaodeMapModule : Module() {
281
713
  // 未实现
282
714
  }
283
715
 
716
+ /**
717
+ * 开始更新设备方向 (iOS 专用,Android 空实现)
718
+ * Android 不支持此功能
719
+ */
720
+ Function("startUpdatingHeading") {
721
+ // Android 不支持罗盘方向更新
722
+ android.util.Log.d("ExpoGaodeMap", "startUpdatingHeading: iOS 专用功能,Android 不支持")
723
+ }
724
+
725
+ /**
726
+ * 停止更新设备方向 (iOS 专用,Android 空实现)
727
+ * Android 不支持此功能
728
+ */
729
+ Function("stopUpdatingHeading") {
730
+ // Android 不支持罗盘方向更新
731
+ android.util.Log.d("ExpoGaodeMap", "stopUpdatingHeading: iOS 专用功能,Android 不支持")
732
+ }
733
+
284
734
  // ==================== 权限管理 ====================
285
-
735
+
286
736
  /**
287
- * 检查位置权限状态
288
- * @return 权限状态对象
737
+ * 检查位置权限状态(增强版,支持 Android 14+ 适配)
738
+ * @return 权限状态对象,包含详细的权限信息
289
739
  */
290
- AsyncFunction("checkLocationPermission") { promise: Promise ->
740
+ AsyncFunction("checkLocationPermission") { promise: expo.modules.kotlin.Promise ->
291
741
  val context = appContext.reactContext!!
292
- val fineLocation = Manifest.permission.ACCESS_FINE_LOCATION
293
- val coarseLocation = Manifest.permission.ACCESS_COARSE_LOCATION
294
-
295
- val hasFine = ContextCompat.checkSelfPermission(context, fineLocation) ==
296
- PackageManager.PERMISSION_GRANTED
297
- val hasCoarse = ContextCompat.checkSelfPermission(context, coarseLocation) ==
298
- PackageManager.PERMISSION_GRANTED
299
-
742
+
743
+ // 使用增强的权限检查
744
+ val foregroundStatus = PermissionHelper.checkForegroundLocationPermission(context)
745
+ val backgroundStatus = PermissionHelper.checkBackgroundLocationPermission(context)
746
+
300
747
  promise.resolve(mapOf(
301
- "granted" to (hasFine && hasCoarse),
302
- "fineLocation" to hasFine,
303
- "coarseLocation" to hasCoarse
748
+ "granted" to foregroundStatus.granted,
749
+ "status" to if (foregroundStatus.granted) "granted" else if (foregroundStatus.isPermanentlyDenied) "denied" else "notDetermined",
750
+ "fineLocation" to foregroundStatus.fineLocation,
751
+ "coarseLocation" to foregroundStatus.coarseLocation,
752
+ "backgroundLocation" to backgroundStatus.backgroundLocation,
753
+ "shouldShowRationale" to foregroundStatus.shouldShowRationale,
754
+ "isPermanentlyDenied" to foregroundStatus.isPermanentlyDenied,
755
+ "isAndroid14Plus" to PermissionHelper.isAndroid14Plus()
304
756
  ))
305
757
  }
306
-
758
+
307
759
  /**
308
- * 请求位置权限
760
+ * 请求前台位置权限(增强版,支持 Android 14+ 适配)
309
761
  * 注意: Android 权限请求是异步的,使用轮询方式检查权限状态
310
762
  * @return 权限请求结果
311
763
  */
312
- AsyncFunction("requestLocationPermission") { promise: Promise ->
764
+ AsyncFunction("requestLocationPermission") { promise: expo.modules.kotlin.Promise ->
313
765
  val activity = appContext.currentActivity
314
766
  if (activity == null) {
315
767
  promise.reject("NO_ACTIVITY", "Activity not available", null)
316
768
  return@AsyncFunction
317
769
  }
318
-
319
- val permissions = arrayOf(
320
- Manifest.permission.ACCESS_FINE_LOCATION,
321
- Manifest.permission.ACCESS_COARSE_LOCATION
322
- )
323
-
324
- ActivityCompat.requestPermissions(activity, permissions, 1001)
325
-
770
+
771
+ // 使用增强的权限请求方法
772
+ PermissionHelper.requestForegroundLocationPermission(activity, 1001)
773
+
326
774
  // 使用 WeakReference 避免内存泄露
327
- val contextRef = WeakReference(appContext.reactContext)
328
- val handler = Handler(Looper.getMainLooper())
329
- var attempts = 0
330
- val maxAttempts = 30 // 3 秒 / 100ms
331
-
775
+ val contextRef = java.lang.ref.WeakReference(appContext.reactContext)
776
+ val handler = android.os.Handler(android.os.Looper.getMainLooper())
777
+ val attempts = 0
778
+ val maxAttempts = 50 // 增加到 5 秒 / 100ms,给用户足够时间操作
779
+
332
780
  val checkPermission = object : Runnable {
333
781
  override fun run() {
334
782
  val context = contextRef.get()
@@ -336,37 +784,113 @@ class ExpoGaodeMapModule : Module() {
336
784
  promise.reject("CONTEXT_LOST", "Context was garbage collected", null)
337
785
  return
338
786
  }
339
-
340
- val hasFine = ContextCompat.checkSelfPermission(
341
- context, Manifest.permission.ACCESS_FINE_LOCATION
342
- ) == PackageManager.PERMISSION_GRANTED
343
- val hasCoarse = ContextCompat.checkSelfPermission(
344
- context, Manifest.permission.ACCESS_COARSE_LOCATION
345
- ) == PackageManager.PERMISSION_GRANTED
346
-
787
+
788
+ val status = PermissionHelper.checkForegroundLocationPermission(context)
789
+
347
790
  // 如果权限已授予或达到最大尝试次数,返回结果并清理 Handler
348
- if ((hasFine && hasCoarse) || attempts >= maxAttempts) {
791
+ if (status.granted || attempts >= maxAttempts) {
349
792
  handler.removeCallbacks(this)
350
793
  promise.resolve(mapOf(
351
- "granted" to (hasFine && hasCoarse),
352
- "fineLocation" to hasFine,
353
- "coarseLocation" to hasCoarse
794
+ "granted" to status.granted,
795
+ "status" to if (status.granted) "granted" else if (status.isPermanentlyDenied) "denied" else "notDetermined",
796
+ "fineLocation" to status.fineLocation,
797
+ "coarseLocation" to status.coarseLocation,
798
+ "shouldShowRationale" to status.shouldShowRationale,
799
+ "isPermanentlyDenied" to status.isPermanentlyDenied
354
800
  ))
355
801
  } else {
356
- attempts++
802
+
357
803
  handler.postDelayed(this, 100)
358
804
  }
359
805
  }
360
806
  }
361
-
807
+
808
+ // 延迟更长时间开始轮询,给权限对话框弹出的时间
809
+ handler.postDelayed(checkPermission, 500)
810
+ }
811
+
812
+ /**
813
+ * 请求后台位置权限(Android 10+ 支持)
814
+ * 注意: 必须在前台权限已授予后才能请求
815
+ * @return 权限请求结果
816
+ */
817
+ AsyncFunction("requestBackgroundLocationPermission") { promise: expo.modules.kotlin.Promise ->
818
+ val activity = appContext.currentActivity
819
+ if (activity == null) {
820
+ promise.reject("NO_ACTIVITY", "Activity not available", null)
821
+ return@AsyncFunction
822
+ }
823
+
824
+ // 检查是否支持后台位置权限
825
+ if (!PermissionHelper.isAndroid10Plus()) {
826
+ promise.resolve(mapOf(
827
+ "granted" to true,
828
+ "backgroundLocation" to true,
829
+ "message" to "Android 10 以下不需要单独请求后台位置权限"
830
+ ))
831
+ return@AsyncFunction
832
+ }
833
+
834
+ // 尝试请求后台位置权限
835
+ val canRequest = PermissionHelper.requestBackgroundLocationPermission(activity, 1002)
836
+ if (!canRequest) {
837
+ promise.reject(
838
+ "FOREGROUND_PERMISSION_REQUIRED",
839
+ "必须先授予前台位置权限才能请求后台位置权限",
840
+ null
841
+ )
842
+ return@AsyncFunction
843
+ }
844
+
845
+ // 轮询检查权限状态
846
+ val contextRef = java.lang.ref.WeakReference(appContext.reactContext)
847
+ val handler = android.os.Handler(android.os.Looper.getMainLooper())
848
+ val attempts = 0
849
+ val maxAttempts = 30
850
+
851
+ val checkPermission = object : Runnable {
852
+ override fun run() {
853
+ val context = contextRef.get()
854
+ if (context == null) {
855
+ promise.reject("CONTEXT_LOST", "Context was garbage collected", null)
856
+ return
857
+ }
858
+
859
+ val status = PermissionHelper.checkBackgroundLocationPermission(context)
860
+
861
+ if (status.granted || attempts >= maxAttempts) {
862
+ handler.removeCallbacks(this)
863
+ promise.resolve(mapOf(
864
+ "granted" to status.granted,
865
+ "status" to if (status.granted) "granted" else if (status.isPermanentlyDenied) "denied" else "notDetermined",
866
+ "backgroundLocation" to status.backgroundLocation,
867
+ "shouldShowRationale" to status.shouldShowRationale,
868
+ "isPermanentlyDenied" to status.isPermanentlyDenied
869
+ ))
870
+ } else {
871
+
872
+ handler.postDelayed(this, 100)
873
+ }
874
+ }
875
+ }
876
+
362
877
  handler.postDelayed(checkPermission, 100)
363
878
  }
364
879
 
880
+ /**
881
+ * 打开应用设置页面(引导用户手动授予权限)
882
+ */
883
+ Function("openAppSettings") {
884
+ val context = appContext.reactContext!!
885
+ PermissionHelper.openAppSettings(context)
886
+ }
887
+
365
888
  Events("onLocationUpdate")
366
889
 
367
890
  OnDestroy {
368
891
  locationManager?.destroy()
369
892
  locationManager = null
893
+ MapPreloadManager.cleanup()
370
894
  }
371
895
  }
372
896