expo-gaode-map-navigation 1.1.5 → 1.1.7

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
@@ -2,6 +2,7 @@ import ExpoModulesCore
2
2
  import AMapFoundationKit
3
3
  import AMapLocationKit
4
4
  import AMapNaviKit
5
+ import CoreLocation
5
6
 
6
7
  /**
7
8
  * 高德地图 Expo 模块
@@ -11,61 +12,77 @@ import AMapNaviKit
11
12
  * - 定位功能管理
12
13
  * - 权限管理
13
14
  */
14
- public class NaviMapModule: Module {
15
+ public class ExpoGaodeMapModule: Module {
15
16
  /// 定位管理器实例
16
17
  private var locationManager: LocationManager?
17
18
  /// 权限管理器实例
18
19
  private var permissionManager: PermissionManager?
19
- /// 隐私协议是否已同意(模块级别跟踪)
20
- private static var privacyAgreed: Bool = false
21
- /// 隐私同意持久化 Key
22
- private static let privacyDefaultsKey = "expo_gaode_map_privacy_agreed"
23
20
 
24
- public func definition() -> ModuleDefinition {
25
- Name("NaviMap")
26
-
27
- // 模块初始化:尝试从本地缓存恢复隐私同意状态
28
- OnCreate {
29
- // 先确保隐私信息展示状态
30
- MAMapView.updatePrivacyShow(AMapPrivacyShowStatus.didShow, privacyInfo: AMapPrivacyInfoStatus.didContain)
31
-
32
- // UserDefaults 恢复上次的同意状态(默认 false)
33
- let saved = UserDefaults.standard.bool(forKey: NaviMapModule.privacyDefaultsKey)
34
- NaviMapModule.privacyAgreed = saved
35
- if saved {
36
- // 同步到 SDK
37
- MAMapView.updatePrivacyAgree(AMapPrivacyAgreeStatus.didAgree)
21
+ // MARK: - 私有辅助方法
22
+
23
+ /**
24
+ * 尝试从 Info.plist 读取并设置 API Key
25
+ * @return 是否成功设置 API Key
26
+ */
27
+ @discardableResult
28
+ private func trySetupApiKeyFromPlist() -> Bool {
29
+ if AMapServices.shared().apiKey == nil || AMapServices.shared().apiKey?.isEmpty == true {
30
+ if let plistKey = Bundle.main.infoDictionary?["AMapApiKey"] as? String, !plistKey.isEmpty {
31
+ AMapServices.shared().apiKey = plistKey
32
+ AMapServices.shared().enableHTTPS = true
33
+ print("✅ ExpoGaodeMap: 从 Info.plist 读取并设置 AMapApiKey 成功")
34
+ return true
38
35
  } else {
36
+ print("⚠️ ExpoGaodeMap: Info.plist 未找到 AMapApiKey")
37
+ return false
39
38
  }
40
39
  }
41
-
42
- // ==================== 隐私合规管理 ====================
43
-
44
- /**
45
- * 更新隐私合规状态
46
- * 必须在用户同意隐私协议后调用
47
- */
48
- Function("updatePrivacyCompliance") { (hasAgreed: Bool) in
49
- // 更新内存状态
50
- NaviMapModule.privacyAgreed = hasAgreed
51
- // 持久化到本地,供下次启动自动恢复
52
- UserDefaults.standard.set(hasAgreed, forKey: NaviMapModule.privacyDefaultsKey)
53
-
54
- if hasAgreed {
55
- // 同步到 SDK
56
- MAMapView.updatePrivacyAgree(AMapPrivacyAgreeStatus.didAgree)
40
+ return true // 已经设置过了
41
+ }
42
+
43
+ /**
44
+ * 尝试启动预加载(检查 API Key 后)
45
+ * @param delay 延迟时间(秒)
46
+ * @param poolSize 预加载池大小
47
+ */
48
+ private func tryStartPreload(delay: Double = 1.0, poolSize: Int = 1) {
49
+ if let apiKey = AMapServices.shared().apiKey, !apiKey.isEmpty {
50
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
51
+ let status = MapPreloadManager.shared.getStatus()
52
+ let isPreloading = (status["isPreloading"] as? Bool) ?? false
57
53
 
58
- // 在用户同意后,如果尚未设置 API Key,则尝试从 Info.plist 读取并设置
59
- if AMapServices.shared().apiKey == nil || AMapServices.shared().apiKey?.isEmpty == true {
60
- if let plistKey = Bundle.main.infoDictionary?["AMapApiKey"] as? String, !plistKey.isEmpty {
61
- AMapServices.shared().apiKey = plistKey
62
- AMapServices.shared().enableHTTPS = true
63
- } else {
64
- }
54
+ if !MapPreloadManager.shared.hasPreloadedMapView() && !isPreloading {
55
+
56
+ MapPreloadManager.shared.startPreload(poolSize: poolSize)
65
57
  }
66
- } else {
67
- MAMapView.updatePrivacyAgree(AMapPrivacyAgreeStatus.notAgree)
68
58
  }
59
+ } else {
60
+ print("⚠️ ExpoGaodeMap: API Key 未设置,跳过自动预加载")
61
+ }
62
+ }
63
+
64
+ public func definition() -> ModuleDefinition {
65
+ Name("ExpoGaodeMap")
66
+
67
+ // 模块初始化:尝试从本地缓存恢复隐私同意状态
68
+ OnCreate {
69
+
70
+ // 1. 告知 SDK:隐私协议已展示且包含隐私内容
71
+ MAMapView.updatePrivacyShow(
72
+ AMapPrivacyShowStatus.didShow,
73
+ privacyInfo: AMapPrivacyInfoStatus.didContain
74
+ )
75
+
76
+ // 2. 告知 SDK:用户已同意隐私协议(关键)
77
+ MAMapView.updatePrivacyAgree(AMapPrivacyAgreeStatus.didAgree)
78
+
79
+ print("✅ ExpoGaodeMap: 原生侧已默认同意隐私协议")
80
+
81
+ // 3. 自动设置 API Key(Info.plist)
82
+ self.trySetupApiKeyFromPlist()
83
+
84
+ // 4. 自动启动预加载(可选)
85
+ self.tryStartPreload(delay: 2.0, poolSize: 1)
69
86
  }
70
87
 
71
88
  // ==================== SDK 初始化 ====================
@@ -75,22 +92,19 @@ public class NaviMapModule: Module {
75
92
  * @param config 配置字典,包含 iosKey
76
93
  */
77
94
  Function("initSDK") { (config: [String: String]) in
78
- // 检查是否已同意隐私协议
79
- if !NaviMapModule.privacyAgreed {
80
- return
81
- }
82
-
83
95
  // 1) 优先使用传入的 iosKey;2) 否则回退读取 Info.plist 的 AMapApiKey
84
96
  let providedKey = config["iosKey"]?.trimmingCharacters(in: .whitespacesAndNewlines)
85
97
  var finalKey: String? = (providedKey?.isEmpty == false) ? providedKey : nil
86
98
  if finalKey == nil {
87
99
  if let plistKey = Bundle.main.infoDictionary?["AMapApiKey"] as? String, !plistKey.isEmpty {
88
100
  finalKey = plistKey
101
+ print("ℹ️ ExpoGaodeMap: initSDK 未提供 iosKey,已从 Info.plist 使用 AMapApiKey")
89
102
  }
90
103
  }
91
104
 
92
105
  guard let keyToUse = finalKey, !keyToUse.isEmpty else {
93
- return
106
+ print("⚠️ ExpoGaodeMap: 未提供 iosKey 且 Info.plist 中也无 AMapApiKey,无法初始化 SDK")
107
+ throw Exception(name: "INIT_FAILED", description: "未提供 API Key")
94
108
  }
95
109
 
96
110
  // 设置 API Key(若与现有不同或尚未设置)
@@ -102,6 +116,15 @@ public class NaviMapModule: Module {
102
116
  // 初始化定位管理器(触发原生侧懒加载)
103
117
  self.getLocationManager()
104
118
 
119
+ print("✅ ExpoGaodeMap: 已设置 API Key 并完成初始化(来源:\(providedKey != nil ? "入参 iosKey" : "Info.plist" ))")
120
+ }
121
+
122
+ /**
123
+ * 设置是否加载世界向量地图
124
+ * @param enable 是否开启
125
+ */
126
+ Function("setLoadWorldVectorMap") { (enable: Bool) in
127
+ MAMapView.loadWorldVectorMap = enable
105
128
  }
106
129
 
107
130
  /**
@@ -111,19 +134,26 @@ public class NaviMapModule: Module {
111
134
  "iOS SDK Version"
112
135
  }
113
136
 
137
+ /**
138
+ * 检查原生 SDK 是否已配置 API Key
139
+ */
140
+ Function("isNativeSDKConfigured") { () -> Bool in
141
+ if let apiKey = AMapServices.shared().apiKey, !apiKey.isEmpty {
142
+ return true
143
+ }
144
+ return false
145
+ }
146
+
114
147
  // ==================== 定位功能 ====================
115
148
 
116
149
  /**
117
150
  * 开始连续定位
118
151
  */
119
152
  Function("start") {
120
- // 检查隐私协议状态
121
- if !NaviMapModule.privacyAgreed {
122
- return
123
- }
124
153
 
125
154
  // 检查是否已设置 API Key
126
155
  if AMapServices.shared().apiKey == nil || AMapServices.shared().apiKey?.isEmpty == true {
156
+ print("⚠️ ExpoGaodeMap: 未设置 API Key,无法开始定位")
127
157
  return
128
158
  }
129
159
 
@@ -149,11 +179,7 @@ public class NaviMapModule: Module {
149
179
  * 返回位置信息和逆地理编码结果
150
180
  */
151
181
  AsyncFunction("getCurrentLocation") { (promise: Promise) in
152
- // 检查隐私协议状态
153
- if !NaviMapModule.privacyAgreed {
154
- promise.reject("PRIVACY_NOT_AGREED", "用户未同意隐私协议,无法获取位置")
155
- return
156
- }
182
+
157
183
 
158
184
  // 检查是否已设置 API Key
159
185
  if AMapServices.shared().apiKey == nil || AMapServices.shared().apiKey?.isEmpty == true {
@@ -161,7 +187,7 @@ public class NaviMapModule: Module {
161
187
  return
162
188
  }
163
189
 
164
- let status = CLLocationManager.authorizationStatus()
190
+ let status = self.currentAuthorizationStatus()
165
191
 
166
192
  if status == .authorizedAlways || status == .authorizedWhenInUse {
167
193
  let manager = self.getLocationManager()
@@ -205,23 +231,352 @@ public class NaviMapModule: Module {
205
231
  }
206
232
  }
207
233
 
234
+ /**
235
+ * 解析高德折线字符串 (Polyline)
236
+ * @param polylineStr 折线字符串
237
+ * @return 坐标点数组
238
+ */
239
+ Function("parsePolyline") { (polylineStr: String?) -> [[String: Double]] in
240
+ guard let polylineStr = polylineStr, !polylineStr.isEmpty else {
241
+ return []
242
+ }
243
+
244
+ let flatCoords = ClusterNative.parsePolyline(polylineStr: polylineStr)
245
+ var result: [[String: Double]] = []
246
+
247
+ // flatCoords 是 [lat1, lon1, lat2, lon2, ...]
248
+ for i in stride(from: 0, to: flatCoords.count, by: 2) {
249
+ if i + 1 < flatCoords.count {
250
+ result.append([
251
+ "latitude": flatCoords[i].doubleValue,
252
+ "longitude": flatCoords[i+1].doubleValue
253
+ ])
254
+ }
255
+ }
256
+
257
+ return result
258
+ }
259
+
208
260
  /**
209
261
  * 坐标转换
210
- * iOS 高德地图 SDK 使用 GCJ-02 坐标系,不需要转换
262
+ * @param coordinate 原始坐标
263
+ * @param type 坐标类型 (0: GPS/Google, 1: MapBar, 2: Baidu, 3: MapABC/SoSo)
264
+ * @return 转换后的坐标
211
265
  */
212
- AsyncFunction("coordinateConvert") { (coordinate: [String: Double], type: Int, promise: Promise) in
213
- guard let latitude = coordinate["latitude"],
214
- let longitude = coordinate["longitude"] else {
215
- promise.reject("INVALID_ARGUMENT", "无效的坐标参数")
216
- return
266
+ AsyncFunction("coordinateConvert") { (coordinate: [String: Double]?, type: Int, promise: Promise) in
267
+ if let coord = LatLngParser.parseLatLng(coordinate) {
268
+ let coordDict = ["latitude": coord.latitude, "longitude": coord.longitude]
269
+ self.getLocationManager().coordinateConvert(coordDict, type: type, promise: promise)
270
+ } else {
271
+ promise.reject("INVALID_COORDINATE", "Invalid coordinate format")
272
+ }
273
+ }
274
+
275
+ // ==================== 几何计算 ====================
276
+
277
+ /**
278
+ * 计算两点之间的距离
279
+ */
280
+ Function("distanceBetweenCoordinates") { (p1: [String: Double]?, p2: [String: Double]?) -> Double in
281
+ guard let coord1 = LatLngParser.parseLatLng(p1),
282
+ let coord2 = LatLngParser.parseLatLng(p2) else {
283
+ return 0.0
284
+ }
285
+ return ClusterNative.calculateDistance(lat1: coord1.latitude, lon1: coord1.longitude, lat2: coord2.latitude, lon2: coord2.longitude)
286
+ }
287
+
288
+ /**
289
+ * 计算多边形面积
290
+ * @param points 多边形顶点坐标数组,支持嵌套数组(多边形空洞)
291
+ */
292
+ Function("calculatePolygonArea") { (points: [Any]?) -> Double in
293
+ let rings = LatLngParser.parseLatLngListList(points)
294
+ if rings.isEmpty { return 0.0 }
295
+
296
+ // 第一项是外轮廓
297
+ let outerCoords = rings[0]
298
+ var totalArea = ClusterNative.calculatePolygonArea(
299
+ latitudes: outerCoords.map { NSNumber(value: $0.latitude) },
300
+ longitudes: outerCoords.map { NSNumber(value: $0.longitude) }
301
+ )
302
+
303
+ // 后续项是内孔,需要减去面积
304
+ if rings.count > 1 {
305
+ for i in 1..<rings.count {
306
+ let ring = rings[i]
307
+ totalArea -= ClusterNative.calculatePolygonArea(
308
+ latitudes: ring.map { NSNumber(value: $0.latitude) },
309
+ longitudes: ring.map { NSNumber(value: $0.longitude) }
310
+ )
311
+ }
312
+ }
313
+
314
+ return Swift.max(0.0, totalArea)
315
+ }
316
+
317
+ /**
318
+ * 判断点是否在多边形内
319
+ * @param point 待判断点
320
+ * @param polygon 多边形顶点坐标数组,支持嵌套数组(多边形空洞)
321
+ */
322
+ Function("isPointInPolygon") { (point: [String: Double]?, polygon: [Any]?) -> Bool in
323
+ guard let coord = LatLngParser.parseLatLng(point) else {
324
+ return false
217
325
  }
218
326
 
219
- // 高德地图 iOS SDK 使用 GCJ-02 坐标系,不需要转换
220
- let result: [String: Double] = [
221
- "latitude": latitude,
222
- "longitude": longitude
223
- ]
224
- promise.resolve(result)
327
+ let rings = LatLngParser.parseLatLngListList(polygon)
328
+ if rings.isEmpty { return false }
329
+
330
+ // 点必须在外轮廓内
331
+ let outerCoords = rings[0]
332
+ let inOuter = ClusterNative.isPointInPolygon(
333
+ pointLat: coord.latitude,
334
+ pointLon: coord.longitude,
335
+ latitudes: outerCoords.map { NSNumber(value: $0.latitude) },
336
+ longitudes: outerCoords.map { NSNumber(value: $0.longitude) }
337
+ )
338
+
339
+ if !inOuter { return false }
340
+
341
+ // 点不能在任何内孔内
342
+ if rings.count > 1 {
343
+ for i in 1..<rings.count {
344
+ let ring = rings[i]
345
+ let inHole = ClusterNative.isPointInPolygon(
346
+ pointLat: coord.latitude,
347
+ pointLon: coord.longitude,
348
+ latitudes: ring.map { NSNumber(value: $0.latitude) },
349
+ longitudes: ring.map { NSNumber(value: $0.longitude) }
350
+ )
351
+ if inHole { return false }
352
+ }
353
+ }
354
+
355
+ return true
356
+ }
357
+
358
+ /**
359
+ * 判断点是否在圆内
360
+ */
361
+ Function("isPointInCircle") { (point: [String: Double]?, center: [String: Double]?, radius: Double) -> Bool in
362
+ guard let coord = LatLngParser.parseLatLng(point),
363
+ let centerCoord = LatLngParser.parseLatLng(center) else {
364
+ return false
365
+ }
366
+ return ClusterNative.isPointInCircle(pointLat: coord.latitude, pointLon: coord.longitude, centerLat: centerCoord.latitude, centerLon: centerCoord.longitude, radiusMeters: radius)
367
+ }
368
+
369
+ /**
370
+ * 计算矩形面积
371
+ */
372
+ Function("calculateRectangleArea") { (southWest: [String: Double]?, northEast: [String: Double]?) -> Double in
373
+ guard let sw = LatLngParser.parseLatLng(southWest),
374
+ let ne = LatLngParser.parseLatLng(northEast) else {
375
+ return 0.0
376
+ }
377
+ return ClusterNative.calculateRectangleArea(swLat: sw.latitude, swLon: sw.longitude, neLat: ne.latitude, neLon: ne.longitude)
378
+ }
379
+
380
+ /**
381
+ * 计算路径上距离目标点最近的点
382
+ */
383
+ Function("getNearestPointOnPath") { (path: [[String: Double]]?, target: [String: Double]?) -> [String: Any]? in
384
+ guard let targetCoord = LatLngParser.parseLatLng(target) else {
385
+ return nil
386
+ }
387
+
388
+ let coords = LatLngParser.parseLatLngList(path)
389
+ if coords.isEmpty {
390
+ return nil
391
+ }
392
+
393
+ let lats = coords.map { NSNumber(value: $0.latitude) }
394
+ let lons = coords.map { NSNumber(value: $0.longitude) }
395
+
396
+ return ClusterNative.getNearestPointOnPath(latitudes: lats, longitudes: lons, targetLat: targetCoord.latitude, targetLon: targetCoord.longitude) as? [String: Any]
397
+ }
398
+
399
+ /**
400
+ * 计算多边形质心
401
+ * @param polygon 多边形顶点坐标数组,支持嵌套数组(多边形空洞)
402
+ */
403
+ Function("calculateCentroid") { (polygon: [Any]?) -> [String: Double]? in
404
+ let rings = LatLngParser.parseLatLngListList(polygon)
405
+ if rings.isEmpty { return nil }
406
+
407
+ if rings.count == 1 {
408
+ let coords = rings[0]
409
+ let lats = coords.map { NSNumber(value: $0.latitude) }
410
+ let lons = coords.map { NSNumber(value: $0.longitude) }
411
+ return ClusterNative.calculateCentroid(latitudes: lats, longitudes: lons) as? [String: Double]
412
+ }
413
+
414
+ // 带孔多边形的质心计算: Σ(Area_i * Centroid_i) / Σ(Area_i)
415
+ var totalArea = 0.0
416
+ var sumLat = 0.0
417
+ var sumLon = 0.0
418
+
419
+ for i in 0..<rings.count {
420
+ let coords = rings[i]
421
+ let lats = coords.map { NSNumber(value: $0.latitude) }
422
+ let lons = coords.map { NSNumber(value: $0.longitude) }
423
+
424
+ let area = ClusterNative.calculatePolygonArea(latitudes: lats, longitudes: lons)
425
+ if let centroid = ClusterNative.calculateCentroid(latitudes: lats, longitudes: lons) as? [String: Double],
426
+ let cLat = centroid["latitude"], let cLon = centroid["longitude"] {
427
+
428
+ // 第一项是外轮廓(正),后续是内孔(负)
429
+ let factor = (i == 0) ? 1.0 : -1.0
430
+ let signedArea = area * factor
431
+
432
+ totalArea += signedArea
433
+ sumLat += cLat * signedArea
434
+ sumLon += cLon * signedArea
435
+ }
436
+ }
437
+
438
+ if abs(totalArea) > 1e-9 {
439
+ return [
440
+ "latitude": sumLat / totalArea,
441
+ "longitude": sumLon / totalArea
442
+ ]
443
+ }
444
+
445
+ return nil
446
+ }
447
+
448
+ /**
449
+ * 计算路径边界
450
+ * @param points 路径点集合
451
+ * @return 边界信息
452
+ */
453
+ Function("calculatePathBounds") { (points: [Any]?) -> [String: Any]? in
454
+ let coords = LatLngParser.parseLatLngList(points)
455
+ if coords.isEmpty { return nil }
456
+
457
+ return ClusterNative.calculatePathBounds(
458
+ latitudes: coords.map { NSNumber(value: $0.latitude) },
459
+ longitudes: coords.map { NSNumber(value: $0.longitude) }
460
+ ) as? [String: Any]
461
+ }
462
+
463
+ /**
464
+ * GeoHash 编码
465
+ */
466
+ Function("encodeGeoHash") { (coordinate: [String: Double]?, precision: Int) -> String in
467
+ guard let coord = LatLngParser.parseLatLng(coordinate) else {
468
+ return ""
469
+ }
470
+ return ClusterNative.encodeGeoHash(lat: coord.latitude, lon: coord.longitude, precision: Int32(precision))
471
+ }
472
+
473
+ /**
474
+ * 轨迹抽稀 (RDP 算法)
475
+ */
476
+ Function("simplifyPolyline") { (points: [[String: Double]]?, tolerance: Double) -> [[String: Double]] in
477
+ let coords = LatLngParser.parseLatLngList(points)
478
+ let simplified = GeometryUtils.simplifyPolyline(coords, tolerance: tolerance)
479
+
480
+ return simplified.map {
481
+ [
482
+ "latitude": $0.latitude,
483
+ "longitude": $0.longitude
484
+ ]
485
+ }
486
+ }
487
+
488
+ /**
489
+ * 计算路径总长度
490
+ */
491
+ Function("calculatePathLength") { (points: [[String: Double]]?) -> Double in
492
+ let coords = LatLngParser.parseLatLngList(points)
493
+ let lats = coords.map { NSNumber(value: $0.latitude) }
494
+ let lons = coords.map { NSNumber(value: $0.longitude) }
495
+ return ClusterNative.calculatePathLength(latitudes: lats, longitudes: lons)
496
+ }
497
+
498
+ /**
499
+ * 获取路径上指定距离的点
500
+ */
501
+ Function("getPointAtDistance") { (points: [[String: Double]]?, distance: Double) -> [String: Any]? in
502
+ let coords = LatLngParser.parseLatLngList(points)
503
+ let lats = coords.map { NSNumber(value: $0.latitude) }
504
+ let lons = coords.map { NSNumber(value: $0.longitude) }
505
+ return ClusterNative.getPointAtDistance(latitudes: lats, longitudes: lons, distanceMeters: distance) as? [String: Any]
506
+ }
507
+
508
+ // --- 瓦片与坐标转换 ---
509
+
510
+ /**
511
+ * 瓦片坐标转换:经纬度 -> 瓦片坐标
512
+ */
513
+ Function("latLngToTile") { (coordinate: [String: Double]?, zoom: Int) -> [String: Any]? in
514
+ guard let coord = LatLngParser.parseLatLng(coordinate) else { return nil }
515
+ return ClusterNative.latLngToTile(lat: coord.latitude, lon: coord.longitude, zoom: Int32(zoom)) as? [String: Any]
516
+ }
517
+
518
+ /**
519
+ * 瓦片坐标转换:瓦片坐标 -> 经纬度
520
+ */
521
+ Function("tileToLatLng") { (tile: [String: Int]) -> [String: Double]? in
522
+ guard let x = tile["x"], let y = tile["y"], let z = tile["z"] else { return nil }
523
+ return ClusterNative.tileToLatLng(x: Int32(x), y: Int32(y), zoom: Int32(z)) as? [String: Double]
524
+ }
525
+
526
+ /**
527
+ * 像素坐标转换:经纬度 -> 像素坐标
528
+ */
529
+ Function("latLngToPixel") { (coordinate: [String: Double]?, zoom: Int) -> [String: Any]? in
530
+ guard let coord = LatLngParser.parseLatLng(coordinate) else { return nil }
531
+ return ClusterNative.latLngToPixel(lat: coord.latitude, lon: coord.longitude, zoom: Int32(zoom)) as? [String: Any]
532
+ }
533
+
534
+ /**
535
+ * 像素坐标转换:像素坐标 -> 经纬度
536
+ */
537
+ Function("pixelToLatLng") { (pixel: [String: Double], zoom: Int) -> [String: Double]? in
538
+ guard let x = pixel["x"], let y = pixel["y"] else { return nil }
539
+ return ClusterNative.pixelToLatLng(x: x, y: y, zoom: Int32(zoom)) as? [String: Double]
540
+ }
541
+
542
+ // --- 批量地理围栏与热力图 ---
543
+
544
+ /**
545
+ * 批量地理围栏检测
546
+ */
547
+ Function("findPointInPolygons") { (point: [String: Double]?, polygons: [Any]?) -> Int in
548
+ guard let coord = LatLngParser.parseLatLng(point) else { return -1 }
549
+
550
+ // 解析多边形集合,统一处理为 [[String: Any]] 格式供 ClusterNative 遍历
551
+ let rings = LatLngParser.parseLatLngListList(polygons)
552
+ if rings.isEmpty { return -1 }
553
+
554
+ let nsPolygons = rings.map { ring in
555
+ ring.map { ["latitude": $0.latitude, "longitude": $0.longitude] }
556
+ }
557
+
558
+ return Int(ClusterNative.findPointInPolygons(pointLat: coord.latitude, pointLon: coord.longitude, polygons: nsPolygons))
559
+ }
560
+
561
+ /**
562
+ * 生成网格聚合数据
563
+ */
564
+ Function("generateHeatmapGrid") { (points: [[String: Any]]?, gridSizeMeters: Double) -> [[String: Any]] in
565
+ guard let points = points, !points.isEmpty else { return [] }
566
+
567
+ var lats: [NSNumber] = []
568
+ var lons: [NSNumber] = []
569
+ var weights: [NSNumber] = []
570
+
571
+ for p in points {
572
+ if let lat = p["latitude"] as? Double, let lon = p["longitude"] as? Double {
573
+ lats.append(NSNumber(value: lat))
574
+ lons.append(NSNumber(value: lon))
575
+ weights.append(NSNumber(value: (p["weight"] as? Double) ?? 1.0))
576
+ }
577
+ }
578
+
579
+ return (ClusterNative.generateHeatmapGrid(latitudes: lats, longitudes: lons, weights: weights, gridSizeMeters: gridSizeMeters) as? [[String: Any]]) ?? []
225
580
  }
226
581
 
227
582
  // ==================== 定位配置 ====================
@@ -258,6 +613,11 @@ public class NaviMapModule: Module {
258
613
  self.getLocationManager().setPausesLocationUpdatesAutomatically(pauses)
259
614
  }
260
615
 
616
+ Property("isBackgroundLocationEnabled") { () -> Bool in
617
+ let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String]
618
+ return backgroundModes?.contains("location") == true
619
+ }
620
+
261
621
  Function("setAllowsBackgroundLocationUpdates") { (allows: Bool) in
262
622
  self.getLocationManager().setAllowsBackgroundLocationUpdates(allows)
263
623
  }
@@ -339,7 +699,7 @@ public class NaviMapModule: Module {
339
699
  * 检查位置权限状态
340
700
  */
341
701
  AsyncFunction("checkLocationPermission") { (promise: Promise) in
342
- let status = CLLocationManager.authorizationStatus()
702
+ let status = self.currentAuthorizationStatus()
343
703
  let granted = status == .authorizedAlways || status == .authorizedWhenInUse
344
704
 
345
705
  promise.resolve([
@@ -359,7 +719,7 @@ public class NaviMapModule: Module {
359
719
  self.permissionManager?.requestPermission { granted, status in
360
720
  // 无论结果如何,都延迟后再次检查最终状态
361
721
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
362
- let finalStatus = CLLocationManager.authorizationStatus()
722
+ let finalStatus = self.currentAuthorizationStatus()
363
723
  let finalGranted = finalStatus == .authorizedAlways || finalStatus == .authorizedWhenInUse
364
724
  let finalStatusString = self.getAuthorizationStatusString(finalStatus)
365
725
 
@@ -371,21 +731,60 @@ public class NaviMapModule: Module {
371
731
  }
372
732
  }
373
733
 
734
+ /**
735
+ * 请求后台位置权限(iOS)
736
+ * 注意:必须在前台权限已授予后才能请求
737
+ */
738
+ AsyncFunction("requestBackgroundLocationPermission") { (promise: Promise) in
739
+ let status = self.currentAuthorizationStatus()
740
+
741
+ // 检查前台权限是否已授予
742
+ if status != .authorizedWhenInUse && status != .authorizedAlways {
743
+ promise.reject("FOREGROUND_PERMISSION_REQUIRED", "必须先授予前台位置权限才能请求后台位置权限")
744
+ return
745
+ }
746
+
747
+ // iOS 上后台权限通过 Info.plist 配置 + 系统设置
748
+ // 这里返回当前状态
749
+ let hasBackground = status == .authorizedAlways
750
+
751
+ promise.resolve([
752
+ "granted": hasBackground,
753
+ "backgroundLocation": hasBackground,
754
+ "status": self.getAuthorizationStatusString(status),
755
+ "message": hasBackground ? "已授予后台权限" : "需要在系统设置中手动授予'始终'权限"
756
+ ])
757
+ }
758
+
759
+
760
+ /**
761
+ * 打开应用设置页面(引导用户手动授予权限)
762
+ */
763
+ Function("openAppSettings") {
764
+ if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
765
+ UIApplication.shared.open(settingsUrl)
766
+ }
767
+ }
768
+
769
+
770
+
374
771
  Events("onHeadingUpdate")
375
772
  Events("onLocationUpdate")
376
773
 
377
774
  OnDestroy {
378
775
  self.locationManager?.destroy()
379
776
  self.locationManager = nil
777
+ MapPreloadManager.shared.cleanup()
380
778
  }
381
779
  }
382
780
 
383
- // MARK: - 私有方法
781
+ // MARK: - 定位管理器
384
782
 
385
783
  /**
386
784
  * 获取或创建定位管理器实例
387
785
  * 使用懒加载模式,并设置事件回调
388
786
  */
787
+ @discardableResult
389
788
  private func getLocationManager() -> LocationManager {
390
789
  if locationManager == nil {
391
790
  locationManager = LocationManager()
@@ -399,6 +798,17 @@ public class NaviMapModule: Module {
399
798
  return locationManager!
400
799
  }
401
800
 
801
+ /**
802
+ * 获取当前的权限状态(兼容 iOS 14+)
803
+ */
804
+ private func currentAuthorizationStatus() -> CLAuthorizationStatus {
805
+ if #available(iOS 14.0, *) {
806
+ return CLLocationManager().authorizationStatus
807
+ } else {
808
+ return CLLocationManager.authorizationStatus()
809
+ }
810
+ }
811
+
402
812
  /**
403
813
  * 将权限状态转换为字符串
404
814
  */